
Sf Data
- 35 installs
- 423 repo stars
- Updated April 27, 2026
- jaganpro/claude-code-sfskills
This is a copy of sf-data by jaganpro - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
sf-data is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- sf-data
- AI & Agent Building
- AI-coding skill
Sf Data by the numbers
- 35 all-time installs (skills.sh)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jaganpro/claude-code-sfskills --skill sf-dataAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 423 |
| Last updated | April 27, 2026 |
| Repository | jaganpro/claude-code-sfskills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Salesforce Data Operations Expert (sf-data)
Use this skill when the user needs Salesforce data work: record CRUD, bulk import/export, test data generation, cleanup scripts, or data factory patterns for validating Apex, Flow, or integration behavior.
When This Skill Owns the Task
Use sf-data when the work involves:
sf dataCLI commands- record creation, update, delete, upsert, export, or tree import/export
- realistic test data generation
- bulk data operations and cleanup
- Apex anonymous scripts for data seeding / rollback
Delegate elsewhere when the user is:
- writing SOQL only → sf-soql
- running or repairing Apex tests → sf-testing
- deploying metadata first → sf-deploy
- discovering schema / field definitions → sf-metadata
---
Important Mode Decision
Confirm which mode the user wants:
| Mode | Use when |
|---|---|
| Script generation | they want reusable .apex, CSV, or JSON assets without touching an org yet |
| Remote execution | they want records created / changed in a real org now |
Do not assume remote execution if the user may only want scripts.
---
Required Context to Gather First
Ask for or infer:
- target object(s)
- org alias, if remote execution is required
- operation type: query, create, update, delete, upsert, import, export, cleanup
- expected volume
- whether this is test data, migration data, or one-off troubleshooting data
- any parent-child relationships that must exist first
---
Core Operating Rules
sf-dataacts on remote org data unless the user explicitly wants local script generation.- Objects and fields must already exist before data creation.
- For automation testing, prefer 251+ records when bulk behavior matters.
- Always think about cleanup before creating large or noisy datasets.
- Never use real PII in generated test data.
- Prefer CLI-first for straightforward CRUD; use anonymous Apex when the operation truly needs server-side orchestration.
If metadata is missing, stop and hand off to:
- sf-metadata or sf-deploy
---
Recommended Workflow
1. Verify prerequisites
Confirm object / field availability, org auth, and required parent records.
2. Run describe-first pre-flight validation when schema is uncertain
Before creating or updating records, use object describe data to validate:
- required fields
- createable vs non-createable fields
- picklist values
- relationship fields and parent requirements
Example pattern:
sf sobject describe --sobject ObjectName --target-org <alias> --jsonHelpful filters:
# Required + createable fields
jq '.result.fields[] | select(.nillable==false and .createable==true) | {name, type}'
# Valid picklist values for one field
jq '.result.fields[] | select(.name=="StageName") | .picklistValues[].value'
# Fields that cannot be set on create
jq '.result.fields[] | select(.createable==false) | .name'3. Choose the smallest correct mechanism
| Need | Default approach |
|---|---|
| small one-off CRUD | sf data single-record commands |
| large import/export | Bulk API 2.0 via sf data ... bulk |
| parent-child seed set | tree import/export |
| reusable test dataset | factory / anonymous Apex script |
| reversible experiment | cleanup script or savepoint-based approach |
4. Execute or generate assets
Use the built-in templates under assets/ when they fit:
assets/factories/assets/bulk/assets/cleanup/assets/soql/assets/csv/assets/json/
5. Verify results
Check counts, relationships, and record IDs after creation or update.
6. Apply a bounded retry strategy
If creation fails: 1. try the primary CLI shape once 2. retry once with corrected parameters 3. re-run describe / validate assumptions 4. pivot to a different mechanism or provide a manual workaround
Do not repeat the same failing command indefinitely.
7. Leave cleanup guidance
Provide exact cleanup commands or rollback assets whenever data was created.
---
High-Signal Rules
Bulk safety
- use bulk operations for large volumes
- test automation-sensitive behavior with 251+ records where appropriate
- avoid one-record-at-a-time patterns for bulk scenarios
Data integrity
- include required fields
- validate picklist values before creation
- verify parent IDs and relationship integrity
- account for validation rules and duplicate constraints
- exclude non-createable fields from input payloads
Cleanup discipline
Prefer one of:
- delete-by-ID
- delete-by-pattern
- delete-by-created-date window
- rollback / savepoint patterns for script-based test runs
---
Common Failure Patterns
| Error | Likely cause | Default fix direction |
|---|---|---|
INVALID_FIELD | wrong field API name or FLS issue | verify schema and access |
REQUIRED_FIELD_MISSING | mandatory field omitted | include required values from describe data |
INVALID_CROSS_REFERENCE_KEY | bad parent ID | create / verify parent first |
FIELD_CUSTOM_VALIDATION_EXCEPTION | validation rule blocked the record | use valid test data or adjust setup |
| invalid picklist value | guessed value instead of describe-backed value | inspect picklist values first |
| non-writeable field error | field is not createable / updateable | remove it from the payload |
| bulk limits / timeouts | wrong tool for the volume | switch to bulk / staged import |
---
Output Format
When finishing, report in this order: 1. Operation performed 2. Objects and counts 3. Target org or local artifact path 4. Record IDs / output files 5. Verification result 6. Cleanup instructions
Suggested shape:
Data operation: <create / update / delete / export / seed>
Objects: <object + counts>
Target: <org alias or local path>
Artifacts: <record ids / csv / apex / json files>
Verification: <passed / partial / failed>
Cleanup: <exact delete or rollback guidance>---
Cross-Skill Integration
| Need | Delegate to | Reason |
|---|---|---|
| discover object / field structure | sf-metadata | accurate schema grounding |
| run bulk-sensitive Apex validation | sf-testing | test execution and coverage |
| deploy missing schema first | sf-deploy | metadata readiness |
| implement production logic consuming the data | sf-apex or sf-flow | behavior implementation |
---
Reference Map
Start here
- references/sf-cli-data-commands.md
- references/test-data-best-practices.md
- references/orchestration.md
- references/test-data-patterns.md
- references/test-data-factory-usage.md
Query / bulk / cleanup
- references/soql-relationship-guide.md
- references/relationship-query-examples.md
- references/bulk-operations-guide.md
- references/cleanup-rollback-guide.md
- references/cleanup-rollback-example.md
Examples / limits
- references/crud-workflow-example.md
- references/bulk-testing-example.md
- references/anonymous-apex-guide.md
- references/governor-limits-reference.md
- assets/
---
Score Guide
| Score | Meaning |
|---|---|
| 117+ | strong production-safe data workflow |
| 104–116 | good operation with minor improvements possible |
| 91–103 | acceptable but review advised |
| 78–90 | partial / risky patterns present |
| < 78 | blocked until corrected |
/**
* ═══════════════════════════════════════════════════════════════════════════════
* BULK INSERT 10,000+ RECORDS
* For Bulk API and large data volume testing
* ═══════════════════════════════════════════════════════════════════════════════
*
* ⚠️ IMPORTANT: This script is designed to be run via sf CLI Bulk API
* NOT via Anonymous Apex (which has governor limits)
*
* PURPOSE:
* Generate large datasets for:
* • Bulk API 2.0 testing
* • Performance testing
* • Data migration validation
* • Report/Dashboard testing with realistic volumes
*
* APPROACHES:
* 1. Use sf data import bulk with CSV files (recommended)
* 2. Use Data Loader for very large datasets
* 3. Use batch Apex to create in smaller chunks
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// APPROACH 1: BATCH APEX FOR LARGE DATA CREATION
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Batch class to create large number of Account records
* Execute with: Database.executeBatch(new BulkAccountCreator(10000), 200);
*/
public class BulkAccountCreator implements Database.Batchable<Integer>, Database.Stateful {
private Integer targetCount;
private Integer createdCount = 0;
private String namePrefix;
private List<String> industries;
private DateTime startTime;
public BulkAccountCreator(Integer count) {
this.targetCount = count;
this.namePrefix = 'BulkData';
this.industries = new List<String>{
'Technology', 'Healthcare', 'Finance', 'Manufacturing',
'Retail', 'Education', 'Energy', 'Media'
};
this.startTime = DateTime.now();
}
public Iterable<Integer> start(Database.BatchableContext bc) {
// Create list of indices to process
List<Integer> indices = new List<Integer>();
for (Integer i = 0; i < targetCount; i++) {
indices.add(i);
}
return indices;
}
public void execute(Database.BatchableContext bc, List<Integer> indices) {
List<Account> accounts = new List<Account>();
for (Integer i : indices) {
accounts.add(new Account(
Name = namePrefix + '_' + String.valueOf(i).leftPad(7, '0'),
Industry = industries[Math.mod(i, industries.size())],
Type = Math.mod(i, 3) == 0 ? 'Customer' : 'Prospect',
AnnualRevenue = 50000 + (Math.mod(i, 1000) * 1000),
NumberOfEmployees = 10 + Math.mod(i, 1000),
BillingCity = 'San Francisco',
BillingState = 'CA',
BillingCountry = 'USA',
Description = 'Bulk created record ' + i
));
}
insert accounts;
createdCount += accounts.size();
}
public void finish(Database.BatchableContext bc) {
DateTime endTime = DateTime.now();
Long durationMs = endTime.getTime() - startTime.getTime();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('BULK CREATION COMPLETE');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Records Created: ' + createdCount);
System.debug('Duration: ' + durationMs + 'ms (' + (durationMs / 1000) + ' seconds)');
System.debug('Rate: ' + (createdCount * 1000 / durationMs) + ' records/second');
System.debug('═══════════════════════════════════════════════════════════════');
}
}
// Execute with:
// Database.executeBatch(new BulkAccountCreator(10000), 200);
// ═══════════════════════════════════════════════════════════════════════════════
// APPROACH 2: GENERATE CSV FOR BULK API IMPORT
// Run this to generate CSV, then use sf data import bulk
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Generate CSV content for bulk import
* Note: For very large files, generate externally with Python/Node.js
*/
public class BulkCsvGenerator {
public static String generateAccountCsv(Integer count) {
List<String> rows = new List<String>();
// Header
rows.add('Name,Industry,Type,AnnualRevenue,NumberOfEmployees,BillingCity,BillingState,BillingCountry,Description');
List<String> industries = new List<String>{
'Technology', 'Healthcare', 'Finance', 'Manufacturing',
'Retail', 'Education', 'Energy', 'Media'
};
List<String> types = new List<String>{'Prospect', 'Customer', 'Partner'};
// Data rows
for (Integer i = 0; i < count; i++) {
String name = 'BulkImport_' + String.valueOf(i).leftPad(7, '0');
String industry = industries[Math.mod(i, industries.size())];
String type = types[Math.mod(i, types.size())];
Decimal revenue = 50000 + (Math.mod(i, 1000) * 1000);
Integer employees = 10 + Math.mod(i, 1000);
rows.add(String.join(new List<String>{
name,
industry,
type,
String.valueOf(revenue),
String.valueOf(employees),
'San Francisco',
'CA',
'USA',
'Bulk imported record ' + i
}, ','));
}
return String.join(rows, '\n');
}
}
// Usage:
// String csv = BulkCsvGenerator.generateAccountCsv(1000);
// System.debug(csv);
// Then save to file and use: sf data import bulk --file accounts.csv --sobject Account --target-org myorg
// ═══════════════════════════════════════════════════════════════════════════════
// SF CLI BULK IMPORT COMMANDS
// ═══════════════════════════════════════════════════════════════════════════════
/*
STEP 1: Create CSV file with headers and data
Example accounts.csv:
Name,Industry,Type,AnnualRevenue,BillingCity,BillingState,BillingCountry
BulkTest_0000001,Technology,Prospect,100000,San Francisco,CA,USA
BulkTest_0000002,Healthcare,Customer,150000,San Francisco,CA,USA
...
STEP 2: Import using Bulk API 2.0
# Import with wait for completion
sf data import bulk \
--file accounts.csv \
--sobject Account \
--target-org myorg \
--wait 30
# Import async (returns job ID)
sf data import bulk \
--file accounts.csv \
--sobject Account \
--target-org myorg
# Check job status
sf data bulk results \
--job-id 7500000000XXXXX \
--target-org myorg
*/
// ═══════════════════════════════════════════════════════════════════════════════
// PYTHON SCRIPT TO GENERATE LARGE CSV
// ═══════════════════════════════════════════════════════════════════════════════
/*
Save as generate_bulk_data.py:
import csv
import random
industries = ['Technology', 'Healthcare', 'Finance', 'Manufacturing',
'Retail', 'Education', 'Energy', 'Media']
types = ['Prospect', 'Customer', 'Partner']
cities = ['San Francisco', 'New York', 'Chicago', 'Los Angeles', 'Seattle']
states = ['CA', 'NY', 'IL', 'CA', 'WA']
def generate_accounts(count, filename='accounts.csv'):
with open(filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Name', 'Industry', 'Type', 'AnnualRevenue',
'NumberOfEmployees', 'BillingCity', 'BillingState',
'BillingCountry', 'Description'])
for i in range(count):
city_idx = i % len(cities)
writer.writerow([
f'BulkTest_{i:07d}',
industries[i % len(industries)],
types[i % len(types)],
50000 + (i % 1000) * 1000,
10 + (i % 500),
cities[city_idx],
states[city_idx],
'USA',
f'Bulk test record {i}'
])
print(f'Generated {count} records to {filename}')
if __name__ == '__main__':
generate_accounts(10000)
Run: python generate_bulk_data.py
Then: sf data import bulk --file accounts.csv --sobject Account --target-org myorg --wait 30
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CLEANUP FOR LARGE DATASETS
// ═══════════════════════════════════════════════════════════════════════════════
/*
CLEANUP via Bulk API:
# Step 1: Export IDs to delete
sf data query \
--query "SELECT Id FROM Account WHERE Name LIKE 'BulkTest%' OR Name LIKE 'BulkData%' OR Name LIKE 'BulkImport%'" \
--target-org myorg \
--result-format csv \
> delete-accounts.csv
# Step 2: Bulk delete
sf data delete bulk \
--file delete-accounts.csv \
--sobject Account \
--target-org myorg \
--wait 30
# For hard delete (permanent, requires permission):
sf data delete bulk \
--file delete-accounts.csv \
--sobject Account \
--target-org myorg \
--hard-delete \
--wait 30
*/
// ═══════════════════════════════════════════════════════════════════════════════
// BATCH APEX CLEANUP
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Batch class to delete large number of records
*/
public class BulkRecordDeleter implements Database.Batchable<SObject> {
private String query;
public BulkRecordDeleter(String soqlQuery) {
this.query = soqlQuery;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<SObject> records) {
delete records;
}
public void finish(Database.BatchableContext bc) {
System.debug('Bulk delete complete');
}
}
// Execute with:
// String query = 'SELECT Id FROM Account WHERE Name LIKE \'BulkTest%\'';
// Database.executeBatch(new BulkRecordDeleter(query), 200);
/**
* ═══════════════════════════════════════════════════════════════════════════════
* BULK INSERT 200+ RECORDS
* For testing trigger and flow bulkification
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Test that triggers and flows handle bulk operations correctly.
* Salesforce processes up to 200 records per transaction in normal contexts.
*
* WHY 251 RECORDS:
* • 200 is the batch size for triggers (this tests the boundary)
* • 250+ ensures multiple batches in batch Apex contexts
* • 251 specifically crosses the 200 boundary
*
* WHAT TO VERIFY:
* • No governor limit exceptions (SOQL, DML, CPU)
* • All records processed correctly
* • No N+1 query patterns
* • Trigger/Flow logic executes correctly for all records
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════
Integer recordCount = 251; // Intentionally over 200 to test batch boundaries
String namePrefix = 'BulkTest';
Boolean enableDebugLimits = true;
// ═══════════════════════════════════════════════════════════════════════════════
// TRACKING COLLECTIONS (for cleanup)
// ═══════════════════════════════════════════════════════════════════════════════
Set<Id> createdAccountIds = new Set<Id>();
Set<Id> createdContactIds = new Set<Id>();
Set<Id> createdOpportunityIds = new Set<Id>();
// ═══════════════════════════════════════════════════════════════════════════════
// CAPTURE INITIAL LIMITS
// ═══════════════════════════════════════════════════════════════════════════════
Integer startQueries = Limits.getQueries();
Integer startDml = Limits.getDmlStatements();
Integer startCpu = Limits.getCpuTime();
Long startHeap = Limits.getHeapSize();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Starting Bulk Insert Test: ' + recordCount + ' records');
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 1: CREATE ACCOUNTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Account> accounts = new List<Account>();
List<String> industries = new List<String>{
'Technology', 'Healthcare', 'Finance', 'Manufacturing',
'Retail', 'Education', 'Energy', 'Media'
};
for (Integer i = 0; i < recordCount; i++) {
accounts.add(new Account(
Name = namePrefix + '_Account_' + String.valueOf(i).leftPad(5, '0'),
Industry = industries[Math.mod(i, industries.size())],
Type = Math.mod(i, 3) == 0 ? 'Customer' : 'Prospect',
AnnualRevenue = 100000 + (i * 10000),
NumberOfEmployees = 50 + (i * 5),
BillingCity = 'San Francisco',
BillingState = 'CA',
BillingCountry = 'USA',
Description = 'Bulk test account ' + i
));
}
try {
insert accounts;
for (Account acc : accounts) {
createdAccountIds.add(acc.Id);
}
System.debug('✓ Created ' + accounts.size() + ' Accounts');
} catch (DmlException e) {
System.debug('✗ Account creation failed: ' + e.getMessage());
throw e;
}
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 2: CREATE CONTACTS (2 per Account = 502 Contacts)
// ═══════════════════════════════════════════════════════════════════════════════
List<Contact> contacts = new List<Contact>();
Integer contactIndex = 0;
for (Account acc : accounts) {
for (Integer i = 0; i < 2; i++) {
String idx = String.valueOf(contactIndex++).leftPad(5, '0');
contacts.add(new Contact(
FirstName = namePrefix,
LastName = 'Contact_' + idx,
AccountId = acc.Id,
Email = namePrefix.toLowerCase() + '.contact' + idx + '@bulktest.example.com',
Phone = '(555) 100-' + String.valueOf(1000 + contactIndex),
Title = Math.mod(contactIndex, 2) == 0 ? 'Manager' : 'Director'
));
}
}
try {
insert contacts;
for (Contact con : contacts) {
createdContactIds.add(con.Id);
}
System.debug('✓ Created ' + contacts.size() + ' Contacts');
} catch (DmlException e) {
System.debug('✗ Contact creation failed: ' + e.getMessage());
throw e;
}
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 3: CREATE OPPORTUNITIES (1 per Account = 251 Opportunities)
// ═══════════════════════════════════════════════════════════════════════════════
List<Opportunity> opportunities = new List<Opportunity>();
List<String> stages = new List<String>{
'Prospecting', 'Qualification', 'Needs Analysis', 'Proposal/Price Quote'
};
Integer oppIndex = 0;
for (Account acc : accounts) {
String idx = String.valueOf(oppIndex++).leftPad(5, '0');
opportunities.add(new Opportunity(
Name = namePrefix + '_Opportunity_' + idx,
AccountId = acc.Id,
StageName = stages[Math.mod(oppIndex, stages.size())],
CloseDate = Date.today().addDays(30 + oppIndex),
Amount = 10000 + (oppIndex * 1000),
Type = 'New Customer',
LeadSource = 'Web'
));
}
try {
insert opportunities;
for (Opportunity opp : opportunities) {
createdOpportunityIds.add(opp.Id);
}
System.debug('✓ Created ' + opportunities.size() + ' Opportunities');
} catch (DmlException e) {
System.debug('✗ Opportunity creation failed: ' + e.getMessage());
throw e;
}
// ═══════════════════════════════════════════════════════════════════════════════
// CAPTURE FINAL LIMITS
// ═══════════════════════════════════════════════════════════════════════════════
Integer endQueries = Limits.getQueries();
Integer endDml = Limits.getDmlStatements();
Integer endCpu = Limits.getCpuTime();
Long endHeap = Limits.getHeapSize();
// ═══════════════════════════════════════════════════════════════════════════════
// REPORT RESULTS
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('BULK INSERT TEST COMPLETE');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('');
System.debug('📊 RECORDS CREATED:');
System.debug(' Accounts: ' + createdAccountIds.size());
System.debug(' Contacts: ' + createdContactIds.size());
System.debug(' Opportunities: ' + createdOpportunityIds.size());
System.debug(' TOTAL: ' + (createdAccountIds.size() + createdContactIds.size() + createdOpportunityIds.size()));
System.debug('');
System.debug('📈 GOVERNOR LIMITS USED:');
System.debug(' SOQL Queries: ' + (endQueries - startQueries) + ' / 100');
System.debug(' DML Statements: ' + (endDml - startDml) + ' / 150');
System.debug(' CPU Time (ms): ' + (endCpu - startCpu) + ' / 10000');
System.debug(' Heap Size (bytes): ' + endHeap + ' / 6000000');
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// CLEANUP SCRIPT (Run separately after testing)
// ═══════════════════════════════════════════════════════════════════════════════
/*
CLEANUP - Copy and run this separately after testing:
// Delete in correct order (children first)
DELETE [SELECT Id FROM Opportunity WHERE Name LIKE 'BulkTest%'];
DELETE [SELECT Id FROM Contact WHERE LastName LIKE 'Contact_%' AND Email LIKE '%bulktest.example.com'];
DELETE [SELECT Id FROM Account WHERE Name LIKE 'BulkTest%'];
System.debug('Cleanup complete');
*/
// ═══════════════════════════════════════════════════════════════════════════════
// OUTPUT RECORD IDS FOR MANUAL CLEANUP
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Record IDs for cleanup (first 10 of each):');
System.debug('Account IDs: ' + new List<Id>(createdAccountIds).subList(0, Math.min(10, createdAccountIds.size())));
System.debug('Contact IDs: ' + new List<Id>(createdContactIds).subList(0, Math.min(10, createdContactIds.size())));
System.debug('Opportunity IDs: ' + new List<Id>(createdOpportunityIds).subList(0, Math.min(10, createdOpportunityIds.size())));
/**
* ═══════════════════════════════════════════════════════════════════════════════
* BULK INSERT 500+ RECORDS
* For testing batch Apex and queueable job bulkification
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Test that batch Apex classes and queueable jobs handle larger datasets.
* Default batch size is 200, so 500+ tests multiple batch iterations.
*
* WHY 500 RECORDS:
* • Tests 2-3 batch iterations (200 records per batch)
* • Validates stateful processing across batches
* • Ensures aggregate operations work correctly
*
* BATCH APEX CONTEXT:
* • Default scope is 200 records per execute()
* • Maximum scope is 2000 records per execute()
* • 50 million records can be processed total
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════
Integer accountCount = 100; // 100 accounts
Integer contactsPerAccount = 3; // 300 contacts total
Integer oppsPerAccount = 2; // 200 opportunities total
// Total: ~600 records
String namePrefix = 'BatchTest';
DateTime testStartTime = DateTime.now();
// ═══════════════════════════════════════════════════════════════════════════════
// TRACKING COLLECTIONS
// ═══════════════════════════════════════════════════════════════════════════════
Map<String, Set<Id>> createdRecords = new Map<String, Set<Id>>{
'Account' => new Set<Id>(),
'Contact' => new Set<Id>(),
'Opportunity' => new Set<Id>()
};
// ═══════════════════════════════════════════════════════════════════════════════
// LIMIT TRACKING
// ═══════════════════════════════════════════════════════════════════════════════
Integer startQueries = Limits.getQueries();
Integer startDml = Limits.getDmlStatements();
Integer startDmlRows = Limits.getDmlRows();
Integer startCpu = Limits.getCpuTime();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Starting Batch-Size Test: ' + accountCount + ' accounts');
System.debug('Expected total records: ~' + (accountCount + accountCount * contactsPerAccount + accountCount * oppsPerAccount));
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 1: CREATE ACCOUNTS IN CHUNKS
// ═══════════════════════════════════════════════════════════════════════════════
List<Account> accounts = new List<Account>();
List<String> industries = new List<String>{
'Technology', 'Healthcare', 'Finance', 'Manufacturing',
'Retail', 'Education', 'Energy', 'Media', 'Government', 'Nonprofit'
};
List<String> types = new List<String>{'Prospect', 'Customer', 'Partner'};
for (Integer i = 0; i < accountCount; i++) {
accounts.add(new Account(
Name = namePrefix + '_Account_' + String.valueOf(i).leftPad(5, '0'),
Industry = industries[Math.mod(i, industries.size())],
Type = types[Math.mod(i, types.size())],
AnnualRevenue = 100000 + (i * 25000),
NumberOfEmployees = 25 + (i * 10),
BillingCity = 'San Francisco',
BillingState = 'CA',
BillingCountry = 'USA',
Description = 'Batch test account created at ' + testStartTime
));
}
insert accounts;
for (Account acc : accounts) {
createdRecords.get('Account').add(acc.Id);
}
System.debug('✓ Created ' + accounts.size() + ' Accounts');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 2: CREATE CONTACTS IN CHUNKS (to stay under DML row limit)
// ═══════════════════════════════════════════════════════════════════════════════
List<Contact> allContacts = new List<Contact>();
Integer contactIndex = 0;
List<String> titles = new List<String>{
'CEO', 'CFO', 'CTO', 'VP Sales', 'VP Marketing',
'Director', 'Manager', 'Analyst', 'Developer', 'Consultant'
};
for (Account acc : accounts) {
for (Integer i = 0; i < contactsPerAccount; i++) {
String idx = String.valueOf(contactIndex++).leftPad(5, '0');
allContacts.add(new Contact(
FirstName = namePrefix,
LastName = 'Contact_' + idx,
AccountId = acc.Id,
Email = namePrefix.toLowerCase() + '.contact' + idx + '@batchtest.example.com',
Phone = '(555) 200-' + String.valueOf(1000 + Math.mod(contactIndex, 9000)),
Title = titles[Math.mod(contactIndex, titles.size())],
Department = 'Department ' + Math.mod(contactIndex, 5),
MailingCity = 'San Francisco',
MailingState = 'CA'
));
}
}
// Insert contacts
insert allContacts;
for (Contact con : allContacts) {
createdRecords.get('Contact').add(con.Id);
}
System.debug('✓ Created ' + allContacts.size() + ' Contacts');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 3: CREATE OPPORTUNITIES
// ═══════════════════════════════════════════════════════════════════════════════
List<Opportunity> allOpportunities = new List<Opportunity>();
Integer oppIndex = 0;
List<String> stages = new List<String>{
'Prospecting', 'Qualification', 'Needs Analysis',
'Value Proposition', 'Proposal/Price Quote', 'Negotiation/Review'
};
List<String> leadSources = new List<String>{
'Web', 'Phone Inquiry', 'Partner Referral', 'Trade Show', 'Other'
};
for (Account acc : accounts) {
for (Integer i = 0; i < oppsPerAccount; i++) {
String idx = String.valueOf(oppIndex++).leftPad(5, '0');
allOpportunities.add(new Opportunity(
Name = namePrefix + '_Opportunity_' + idx,
AccountId = acc.Id,
StageName = stages[Math.mod(oppIndex, stages.size())],
CloseDate = Date.today().addDays(15 + Math.mod(oppIndex, 90)),
Amount = 5000 + (oppIndex * 500),
Probability = 10 + Math.mod(oppIndex, 80),
Type = 'New Customer',
LeadSource = leadSources[Math.mod(oppIndex, leadSources.size())]
));
}
}
insert allOpportunities;
for (Opportunity opp : allOpportunities) {
createdRecords.get('Opportunity').add(opp.Id);
}
System.debug('✓ Created ' + allOpportunities.size() + ' Opportunities');
// ═══════════════════════════════════════════════════════════════════════════════
// REPORT RESULTS
// ═══════════════════════════════════════════════════════════════════════════════
Integer endQueries = Limits.getQueries();
Integer endDml = Limits.getDmlStatements();
Integer endDmlRows = Limits.getDmlRows();
Integer endCpu = Limits.getCpuTime();
Integer totalRecords = createdRecords.get('Account').size() +
createdRecords.get('Contact').size() +
createdRecords.get('Opportunity').size();
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('BATCH-SIZE TEST COMPLETE');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('');
System.debug('📊 RECORDS CREATED:');
System.debug(' Accounts: ' + createdRecords.get('Account').size());
System.debug(' Contacts: ' + createdRecords.get('Contact').size());
System.debug(' Opportunities: ' + createdRecords.get('Opportunity').size());
System.debug(' ────────────────────────────────');
System.debug(' TOTAL: ' + totalRecords);
System.debug('');
System.debug('📈 GOVERNOR LIMITS USED:');
System.debug(' SOQL Queries: ' + (endQueries - startQueries) + ' / 100 (' + ((endQueries - startQueries) * 100 / 100) + '%)');
System.debug(' DML Statements: ' + (endDml - startDml) + ' / 150 (' + ((endDml - startDml) * 100 / 150) + '%)');
System.debug(' DML Rows: ' + endDmlRows + ' / 10000 (' + (endDmlRows * 100 / 10000) + '%)');
System.debug(' CPU Time (ms): ' + (endCpu - startCpu) + ' / 10000 (' + ((endCpu - startCpu) * 100 / 10000) + '%)');
System.debug('');
System.debug('🔄 BATCH RECOMMENDATIONS:');
System.debug(' Records can be processed in batches of 200 (default)');
System.debug(' This dataset would require ' + Math.ceil((Decimal)totalRecords / 200) + ' batch iterations');
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// CLEANUP SCRIPT
// ═══════════════════════════════════════════════════════════════════════════════
/*
CLEANUP - Run this after testing:
DELETE [SELECT Id FROM Opportunity WHERE Name LIKE 'BatchTest%'];
DELETE [SELECT Id FROM Contact WHERE LastName LIKE 'Contact_%' AND Email LIKE '%batchtest.example.com'];
DELETE [SELECT Id FROM Account WHERE Name LIKE 'BatchTest%'];
System.debug('Cleanup complete');
*/
// ═══════════════════════════════════════════════════════════════════════════════
// OUTPUT FOR CLEANUP VIA SF CLI
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('CLI Cleanup Commands:');
System.debug('sf data query --query "SELECT Id FROM Account WHERE Name LIKE \'BatchTest%\'" --target-org [alias] --result-format csv > cleanup-accounts.csv');
System.debug('sf data delete bulk --file cleanup-accounts.csv --sobject Account --target-org [alias] --wait 10');
/**
* ═══════════════════════════════════════════════════════════════════════════════
* BULK UPSERT WITH EXTERNAL ID
* For data synchronization and migration scenarios
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Upsert (insert or update) records using an External ID field.
* This is essential for:
* • Data synchronization with external systems
* • Data migration from legacy systems
* • Ongoing integrations where records may or may not exist
*
* HOW IT WORKS:
* • If External ID matches → UPDATE existing record
* • If External ID not found → INSERT new record
* • External ID field must be marked as "External ID" in Salesforce
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// APEX UPSERT EXAMPLE
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Example: Upsert Accounts using External_Id__c field
*
* Prerequisites:
* 1. Create a custom field on Account: External_Id__c (Text, External ID, Unique)
* 2. Ensure field is marked as "External ID" in field settings
*/
// Sample data simulating external system records
List<Map<String, Object>> externalData = new List<Map<String, Object>>{
new Map<String, Object>{
'externalId' => 'EXT-001',
'name' => 'External Account One',
'industry' => 'Technology',
'revenue' => 1000000
},
new Map<String, Object>{
'externalId' => 'EXT-002',
'name' => 'External Account Two',
'industry' => 'Healthcare',
'revenue' => 2000000
},
new Map<String, Object>{
'externalId' => 'EXT-003',
'name' => 'External Account Three',
'industry' => 'Finance',
'revenue' => 3000000
}
};
// Build Account records with External ID
List<Account> accountsToUpsert = new List<Account>();
for (Map<String, Object> data : externalData) {
accountsToUpsert.add(new Account(
External_Id__c = (String) data.get('externalId'),
Name = (String) data.get('name'),
Industry = (String) data.get('industry'),
AnnualRevenue = (Decimal) data.get('revenue'),
Type = 'Prospect',
Description = 'Synced from external system'
));
}
// Upsert using External ID field
Schema.SObjectField externalIdField = Account.External_Id__c;
List<Database.UpsertResult> results = Database.upsert(accountsToUpsert, externalIdField, false);
// Process results
Integer insertedCount = 0;
Integer updatedCount = 0;
Integer errorCount = 0;
for (Integer i = 0; i < results.size(); i++) {
Database.UpsertResult result = results[i];
Account acc = accountsToUpsert[i];
if (result.isSuccess()) {
if (result.isCreated()) {
insertedCount++;
System.debug('✓ INSERTED: ' + acc.External_Id__c + ' -> ' + result.getId());
} else {
updatedCount++;
System.debug('✓ UPDATED: ' + acc.External_Id__c + ' -> ' + result.getId());
}
} else {
errorCount++;
for (Database.Error err : result.getErrors()) {
System.debug('✗ ERROR: ' + acc.External_Id__c + ' -> ' + err.getMessage());
}
}
}
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('UPSERT COMPLETE');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Inserted: ' + insertedCount);
System.debug('Updated: ' + updatedCount);
System.debug('Errors: ' + errorCount);
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// SF CLI BULK UPSERT
// ═══════════════════════════════════════════════════════════════════════════════
/*
STEP 1: Create CSV with External ID column
Example accounts-upsert.csv:
External_Id__c,Name,Industry,Type,AnnualRevenue,BillingCity,BillingState
EXT-001,External Account One,Technology,Prospect,1000000,San Francisco,CA
EXT-002,External Account Two,Healthcare,Customer,2000000,New York,NY
EXT-003,External Account Three,Finance,Partner,3000000,Chicago,IL
STEP 2: Run bulk upsert
sf data upsert bulk \
--file accounts-upsert.csv \
--sobject Account \
--external-id External_Id__c \
--target-org myorg \
--wait 30
NOTES:
• The External_Id__c field must exist and be marked as External ID in Salesforce
• To use standard Id field as the external ID: --external-id Id
• CSV must include the external ID column
*/
// ═══════════════════════════════════════════════════════════════════════════════
// EXAMPLE: UPSERT CONTACTS WITH ACCOUNT RELATIONSHIP
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Upsert Contacts and establish Account relationship via External ID
*
* Prerequisites:
* 1. Account.External_Id__c exists and has values
* 2. Contact.External_Id__c exists
*/
/*
List<Contact> contactsToUpsert = new List<Contact>();
// Create Contacts referencing Accounts by External ID
for (Map<String, Object> data : externalContactData) {
Contact con = new Contact(
External_Id__c = (String) data.get('contactExternalId'),
FirstName = (String) data.get('firstName'),
LastName = (String) data.get('lastName'),
Email = (String) data.get('email')
);
// Set Account relationship via External ID
// This creates the relationship without querying for the Account first
con.Account = new Account(
External_Id__c = (String) data.get('accountExternalId')
);
contactsToUpsert.add(con);
}
// Upsert Contacts
Database.upsert(contactsToUpsert, Contact.External_Id__c, false);
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CSV FOR CONTACT UPSERT WITH ACCOUNT RELATIONSHIP
// ═══════════════════════════════════════════════════════════════════════════════
/*
Example contacts-upsert.csv:
External_Id__c,FirstName,LastName,Email,Account.External_Id__c
CON-001,John,Smith,john.smith@example.com,EXT-001
CON-002,Jane,Doe,jane.doe@example.com,EXT-001
CON-003,Bob,Johnson,bob.johnson@example.com,EXT-002
Note: Use "Account.External_Id__c" to reference parent by External ID
sf data upsert bulk \
--file contacts-upsert.csv \
--sobject Contact \
--external-id External_Id__c \
--target-org myorg \
--wait 30
*/
// ═══════════════════════════════════════════════════════════════════════════════
// EXAMPLE: UPSERT WITH STANDARD ID (Update existing records)
// ═══════════════════════════════════════════════════════════════════════════════
/*
When you have Salesforce IDs (from export), use Id as the external ID:
Example updates.csv:
Id,Name,Industry,AnnualRevenue
001XXXXXXXXXXXX1,Updated Account One,Technology,1500000
001XXXXXXXXXXXX2,Updated Account Two,Healthcare,2500000
sf data upsert bulk \
--file updates.csv \
--sobject Account \
--external-id Id \
--target-org myorg \
--wait 30
*/
// ═══════════════════════════════════════════════════════════════════════════════
// COMPLETE SYNC WORKFLOW EXAMPLE
// ═══════════════════════════════════════════════════════════════════════════════
/*
FULL SYNC WORKFLOW:
1. Export current data from Salesforce
sf data query \
--query "SELECT External_Id__c, Name, Industry FROM Account WHERE External_Id__c != null" \
--target-org myorg \
--result-format csv \
> current-accounts.csv
2. Generate sync file from external system (your process)
- Include External_Id__c for each record
- Include all fields to update/insert
3. Upsert the sync file
sf data upsert bulk \
--file sync-accounts.csv \
--sobject Account \
--external-id External_Id__c \
--target-org myorg \
--wait 30
4. Check results
sf data bulk results \
--job-id [job-id-from-step-3] \
--target-org myorg
*/
// ═══════════════════════════════════════════════════════════════════════════════
// BATCH APEX FOR COMPLEX UPSERT LOGIC
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Batch class for upserting with custom logic
*/
public class BulkUpsertProcessor implements Database.Batchable<SObject>, Database.Stateful {
private String query;
private Integer insertedCount = 0;
private Integer updatedCount = 0;
private Integer errorCount = 0;
public BulkUpsertProcessor(String soqlQuery) {
this.query = soqlQuery;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<SObject> records) {
// Transform and prepare records for upsert
List<Account> accountsToUpsert = new List<Account>();
for (SObject record : records) {
Account source = (Account) record;
// Create upsert record
Account target = new Account(
External_Id__c = source.External_Id__c,
Name = source.Name,
Industry = source.Industry,
AnnualRevenue = source.AnnualRevenue,
LastModifiedDate = DateTime.now() // Will be auto-updated
);
// Apply transformation logic
if (target.AnnualRevenue != null && target.AnnualRevenue > 1000000) {
target.Type = 'Enterprise';
}
accountsToUpsert.add(target);
}
// Upsert with partial success
List<Database.UpsertResult> results = Database.upsert(
accountsToUpsert,
Account.External_Id__c,
false // Allow partial success
);
// Track results
for (Database.UpsertResult result : results) {
if (result.isSuccess()) {
if (result.isCreated()) {
insertedCount++;
} else {
updatedCount++;
}
} else {
errorCount++;
}
}
}
public void finish(Database.BatchableContext bc) {
System.debug('Batch Upsert Complete:');
System.debug(' Inserted: ' + insertedCount);
System.debug(' Updated: ' + updatedCount);
System.debug(' Errors: ' + errorCount);
}
}
// Execute with:
// String query = 'SELECT External_Id__c, Name, Industry, AnnualRevenue FROM Account WHERE External_Id__c != null';
// Database.executeBatch(new BulkUpsertProcessor(query), 200);
/**
* ═══════════════════════════════════════════════════════════════════════════════
* DELETE RECORDS BY CREATED DATE
* Clean up test records created within a specific time window
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Delete records created during a testing session or time period.
* Useful when you know the approximate time test data was created.
*
* ⚠️ WARNING:
* Always PREVIEW records before deleting!
* Be careful with date ranges - you might delete production data!
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════
// Time window for records to delete
DateTime startTime = DateTime.now().addHours(-2); // 2 hours ago
DateTime endTime = DateTime.now(); // Now
// Optional: Add name pattern for extra safety
String namePattern = 'Test%'; // Set to null to skip name filter
Boolean useNameFilter = true; // Set to false to delete ALL records in time window
Boolean previewOnly = true; // Set to false to actually delete
Integer maxRecords = 10000; // Safety limit per object
// ═══════════════════════════════════════════════════════════════════════════════
// BUILD DYNAMIC QUERY CONDITIONS
// ═══════════════════════════════════════════════════════════════════════════════
String dateCondition = 'CreatedDate >= :startTime AND CreatedDate <= :endTime';
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('DELETE BY CREATED DATE');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Time Window:');
System.debug(' Start: ' + startTime.format('yyyy-MM-dd HH:mm:ss'));
System.debug(' End: ' + endTime.format('yyyy-MM-dd HH:mm:ss'));
if (useNameFilter) {
System.debug('Name Filter: ' + namePattern);
}
System.debug('Mode: ' + (previewOnly ? 'PREVIEW ONLY' : '⚠️ DELETE MODE'));
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 1: PREVIEW ACCOUNTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Account> accountsToDelete;
if (useNameFilter) {
accountsToDelete = [
SELECT Id, Name, CreatedDate, CreatedBy.Name
FROM Account
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
AND Name LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
} else {
accountsToDelete = [
SELECT Id, Name, CreatedDate, CreatedBy.Name
FROM Account
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
}
System.debug('📋 ACCOUNTS in time window: ' + accountsToDelete.size());
if (!accountsToDelete.isEmpty()) {
System.debug(' Records:');
for (Integer i = 0; i < Math.min(10, accountsToDelete.size()); i++) {
Account acc = accountsToDelete[i];
System.debug(' - ' + acc.Name + ' (Created: ' + acc.CreatedDate.format('HH:mm:ss') + ' by ' + acc.CreatedBy.Name + ')');
}
if (accountsToDelete.size() > 10) {
System.debug(' ... and ' + (accountsToDelete.size() - 10) + ' more');
}
}
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 2: PREVIEW CONTACTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Contact> contactsToDelete;
if (useNameFilter) {
contactsToDelete = [
SELECT Id, FirstName, LastName, Account.Name, CreatedDate, CreatedBy.Name
FROM Contact
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
AND (LastName LIKE :namePattern OR Account.Name LIKE :namePattern)
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
} else {
contactsToDelete = [
SELECT Id, FirstName, LastName, Account.Name, CreatedDate, CreatedBy.Name
FROM Contact
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
}
System.debug('📋 CONTACTS in time window: ' + contactsToDelete.size());
if (!contactsToDelete.isEmpty()) {
System.debug(' Records:');
for (Integer i = 0; i < Math.min(5, contactsToDelete.size()); i++) {
Contact con = contactsToDelete[i];
System.debug(' - ' + con.FirstName + ' ' + con.LastName + ' (Created: ' + con.CreatedDate.format('HH:mm:ss') + ')');
}
}
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 3: PREVIEW OPPORTUNITIES
// ═══════════════════════════════════════════════════════════════════════════════
List<Opportunity> opportunitiesToDelete;
if (useNameFilter) {
opportunitiesToDelete = [
SELECT Id, Name, Account.Name, StageName, Amount, CreatedDate
FROM Opportunity
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
AND (Name LIKE :namePattern OR Account.Name LIKE :namePattern)
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
} else {
opportunitiesToDelete = [
SELECT Id, Name, Account.Name, StageName, Amount, CreatedDate
FROM Opportunity
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
}
System.debug('📋 OPPORTUNITIES in time window: ' + opportunitiesToDelete.size());
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 4: PREVIEW TASKS
// ═══════════════════════════════════════════════════════════════════════════════
List<Task> tasksToDelete;
if (useNameFilter) {
tasksToDelete = [
SELECT Id, Subject, Status, CreatedDate
FROM Task
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
AND Subject LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
} else {
tasksToDelete = [
SELECT Id, Subject, Status, CreatedDate
FROM Task
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
}
System.debug('📋 TASKS in time window: ' + tasksToDelete.size());
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 5: PREVIEW LEADS
// ═══════════════════════════════════════════════════════════════════════════════
List<Lead> leadsToDelete;
if (useNameFilter) {
leadsToDelete = [
SELECT Id, FirstName, LastName, Company, Status, CreatedDate
FROM Lead
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
AND (LastName LIKE :namePattern OR Company LIKE :namePattern)
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
} else {
leadsToDelete = [
SELECT Id, FirstName, LastName, Company, Status, CreatedDate
FROM Lead
WHERE CreatedDate >= :startTime
AND CreatedDate <= :endTime
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
}
System.debug('📋 LEADS in time window: ' + leadsToDelete.size());
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// SUMMARY
// ═══════════════════════════════════════════════════════════════════════════════
Integer totalRecords = accountsToDelete.size() + contactsToDelete.size() +
opportunitiesToDelete.size() + tasksToDelete.size() +
leadsToDelete.size();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('SUMMARY - Records in time window');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug(' Accounts: ' + accountsToDelete.size());
System.debug(' Contacts: ' + contactsToDelete.size());
System.debug(' Opportunities: ' + opportunitiesToDelete.size());
System.debug(' Tasks: ' + tasksToDelete.size());
System.debug(' Leads: ' + leadsToDelete.size());
System.debug(' ─────────────────────────────────');
System.debug(' TOTAL: ' + totalRecords);
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 6: DELETE (if not preview mode)
// ═══════════════════════════════════════════════════════════════════════════════
if (!previewOnly && totalRecords > 0) {
System.debug('');
System.debug('⚠️ DELETING RECORDS...');
Integer deletedCount = 0;
// Delete in correct order (children first, then parents)
if (!tasksToDelete.isEmpty()) {
delete tasksToDelete;
deletedCount += tasksToDelete.size();
System.debug(' ✓ Deleted ' + tasksToDelete.size() + ' Tasks');
}
if (!opportunitiesToDelete.isEmpty()) {
delete opportunitiesToDelete;
deletedCount += opportunitiesToDelete.size();
System.debug(' ✓ Deleted ' + opportunitiesToDelete.size() + ' Opportunities');
}
if (!contactsToDelete.isEmpty()) {
delete contactsToDelete;
deletedCount += contactsToDelete.size();
System.debug(' ✓ Deleted ' + contactsToDelete.size() + ' Contacts');
}
if (!leadsToDelete.isEmpty()) {
delete leadsToDelete;
deletedCount += leadsToDelete.size();
System.debug(' ✓ Deleted ' + leadsToDelete.size() + ' Leads');
}
if (!accountsToDelete.isEmpty()) {
delete accountsToDelete;
deletedCount += accountsToDelete.size();
System.debug(' ✓ Deleted ' + accountsToDelete.size() + ' Accounts');
}
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('DELETE COMPLETE: ' + deletedCount + ' records moved to Recycle Bin');
System.debug('═══════════════════════════════════════════════════════════════');
} else if (previewOnly) {
System.debug('');
System.debug('ℹ️ PREVIEW MODE - No records deleted');
System.debug(' To delete, set previewOnly = false and run again');
}
// ═══════════════════════════════════════════════════════════════════════════════
// SF CLI ALTERNATIVE WITH DATE LITERALS
// ═══════════════════════════════════════════════════════════════════════════════
/*
Using SOQL date literals:
# Records created today
sf data query \
--query "SELECT Id FROM Account WHERE CreatedDate = TODAY AND Name LIKE 'Test%'" \
--target-org myorg \
--result-format csv \
> delete-today.csv
# Records created in the last hour
sf data query \
--query "SELECT Id FROM Account WHERE CreatedDate = LAST_N_HOURS:1 AND Name LIKE 'Test%'" \
--target-org myorg \
--result-format csv \
> delete-lasthour.csv
# Records created this week
sf data query \
--query "SELECT Id FROM Account WHERE CreatedDate = THIS_WEEK AND Name LIKE 'Test%'" \
--target-org myorg \
--result-format csv \
> delete-thisweek.csv
# Then bulk delete
sf data delete bulk \
--file delete-today.csv \
--sobject Account \
--target-org myorg \
--wait 30
*/
/**
* ═══════════════════════════════════════════════════════════════════════════════
* DELETE RECORDS BY NAME PATTERN
* Clean up test records matching a specific naming pattern
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Delete test records that follow a naming convention.
* Common patterns: 'Test%', 'BulkTest%', 'Demo%', etc.
*
* ⚠️ WARNING:
* Always PREVIEW records before deleting!
* Records go to Recycle Bin by default (soft delete)
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════
String namePattern = 'Test%'; // LIKE pattern for Name field
Boolean previewOnly = true; // Set to false to actually delete
Integer maxRecords = 10000; // Safety limit
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 1: PREVIEW ACCOUNTS
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('DELETE BY NAME PATTERN: ' + namePattern);
System.debug('Mode: ' + (previewOnly ? 'PREVIEW ONLY' : '⚠️ DELETE MODE'));
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('');
// Query Accounts
List<Account> accountsToDelete = [
SELECT Id, Name, CreatedDate, CreatedBy.Name
FROM Account
WHERE Name LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
System.debug('📋 ACCOUNTS matching "' + namePattern + '": ' + accountsToDelete.size());
if (!accountsToDelete.isEmpty()) {
System.debug(' Sample records:');
for (Integer i = 0; i < Math.min(10, accountsToDelete.size()); i++) {
Account acc = accountsToDelete[i];
System.debug(' - ' + acc.Name + ' (Created: ' + acc.CreatedDate.format() + ' by ' + acc.CreatedBy.Name + ')');
}
if (accountsToDelete.size() > 10) {
System.debug(' ... and ' + (accountsToDelete.size() - 10) + ' more');
}
}
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 2: PREVIEW CONTACTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Contact> contactsToDelete = [
SELECT Id, FirstName, LastName, Account.Name, CreatedDate
FROM Contact
WHERE LastName LIKE :namePattern
OR Account.Name LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
System.debug('📋 CONTACTS matching "' + namePattern + '": ' + contactsToDelete.size());
if (!contactsToDelete.isEmpty()) {
System.debug(' Sample records:');
for (Integer i = 0; i < Math.min(10, contactsToDelete.size()); i++) {
Contact con = contactsToDelete[i];
System.debug(' - ' + con.FirstName + ' ' + con.LastName + ' (' + con.Account?.Name + ')');
}
}
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 3: PREVIEW OPPORTUNITIES
// ═══════════════════════════════════════════════════════════════════════════════
List<Opportunity> opportunitiesToDelete = [
SELECT Id, Name, Account.Name, StageName, Amount, CreatedDate
FROM Opportunity
WHERE Name LIKE :namePattern
OR Account.Name LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
System.debug('📋 OPPORTUNITIES matching "' + namePattern + '": ' + opportunitiesToDelete.size());
if (!opportunitiesToDelete.isEmpty()) {
System.debug(' Sample records:');
for (Integer i = 0; i < Math.min(10, opportunitiesToDelete.size()); i++) {
Opportunity opp = opportunitiesToDelete[i];
System.debug(' - ' + opp.Name + ' (' + opp.StageName + ', $' + opp.Amount + ')');
}
}
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 4: PREVIEW CASES
// ═══════════════════════════════════════════════════════════════════════════════
List<Case> casesToDelete = [
SELECT Id, CaseNumber, Subject, Account.Name, Status, CreatedDate
FROM Case
WHERE Subject LIKE :namePattern
OR Account.Name LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
System.debug('📋 CASES matching "' + namePattern + '": ' + casesToDelete.size());
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 5: PREVIEW LEADS
// ═══════════════════════════════════════════════════════════════════════════════
List<Lead> leadsToDelete = [
SELECT Id, FirstName, LastName, Company, Status, CreatedDate
FROM Lead
WHERE LastName LIKE :namePattern
OR Company LIKE :namePattern
ORDER BY CreatedDate DESC
LIMIT :maxRecords
];
System.debug('📋 LEADS matching "' + namePattern + '": ' + leadsToDelete.size());
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// SUMMARY
// ═══════════════════════════════════════════════════════════════════════════════
Integer totalRecords = accountsToDelete.size() + contactsToDelete.size() +
opportunitiesToDelete.size() + casesToDelete.size() +
leadsToDelete.size();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('SUMMARY - Records matching "' + namePattern + '"');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug(' Accounts: ' + accountsToDelete.size());
System.debug(' Contacts: ' + contactsToDelete.size());
System.debug(' Opportunities: ' + opportunitiesToDelete.size());
System.debug(' Cases: ' + casesToDelete.size());
System.debug(' Leads: ' + leadsToDelete.size());
System.debug(' ─────────────────────────────────');
System.debug(' TOTAL: ' + totalRecords);
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 6: DELETE (if not preview mode)
// ═══════════════════════════════════════════════════════════════════════════════
if (!previewOnly && totalRecords > 0) {
System.debug('');
System.debug('⚠️ DELETING RECORDS...');
// Delete in correct order (children before parents)
Integer deletedCount = 0;
// Delete Cases
if (!casesToDelete.isEmpty()) {
delete casesToDelete;
deletedCount += casesToDelete.size();
System.debug(' ✓ Deleted ' + casesToDelete.size() + ' Cases');
}
// Delete Opportunities
if (!opportunitiesToDelete.isEmpty()) {
delete opportunitiesToDelete;
deletedCount += opportunitiesToDelete.size();
System.debug(' ✓ Deleted ' + opportunitiesToDelete.size() + ' Opportunities');
}
// Delete Contacts
if (!contactsToDelete.isEmpty()) {
delete contactsToDelete;
deletedCount += contactsToDelete.size();
System.debug(' ✓ Deleted ' + contactsToDelete.size() + ' Contacts');
}
// Delete Leads
if (!leadsToDelete.isEmpty()) {
delete leadsToDelete;
deletedCount += leadsToDelete.size();
System.debug(' ✓ Deleted ' + leadsToDelete.size() + ' Leads');
}
// Delete Accounts (last - they're parents)
if (!accountsToDelete.isEmpty()) {
delete accountsToDelete;
deletedCount += accountsToDelete.size();
System.debug(' ✓ Deleted ' + accountsToDelete.size() + ' Accounts');
}
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('DELETE COMPLETE: ' + deletedCount + ' records moved to Recycle Bin');
System.debug('═══════════════════════════════════════════════════════════════');
} else if (previewOnly) {
System.debug('');
System.debug('ℹ️ PREVIEW MODE - No records deleted');
System.debug(' To delete, set previewOnly = false and run again');
}
// ═══════════════════════════════════════════════════════════════════════════════
// SF CLI ALTERNATIVE
// ═══════════════════════════════════════════════════════════════════════════════
/*
For large datasets, use sf CLI Bulk API:
# Step 1: Query and export IDs
sf data query \
--query "SELECT Id FROM Account WHERE Name LIKE 'Test%'" \
--target-org myorg \
--result-format csv \
> delete-accounts.csv
# Step 2: Bulk delete
sf data delete bulk \
--file delete-accounts.csv \
--sobject Account \
--target-org myorg \
--wait 30
# For permanent delete (hard delete):
sf data delete bulk \
--file delete-accounts.csv \
--sobject Account \
--target-org myorg \
--hard-delete \
--wait 30
*/
/**
* ═══════════════════════════════════════════════════════════════════════════════
* DELETE TEST DATA - COMPREHENSIVE CLEANUP
* Remove all test data matching common test data patterns
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Clean up test data created by TestDataFactory classes.
* Targets common test data naming patterns across multiple objects.
*
* PATTERNS MATCHED:
* • 'Test%' - Standard test prefix
* • 'BulkTest%' - Bulk testing data
* • 'BatchTest%' - Batch Apex testing data
* • 'BulkData%' - Large volume test data
* • 'BulkImport%' - Import testing data
* • 'Hierarchy%' - Hierarchy testing data
* • '%@testfactory.example.com' - Test email domain
* • '%@bulktest.example.com' - Bulk test email domain
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════
Boolean previewOnly = true; // Set to false to actually delete
Integer maxRecordsPerObject = 10000;
// Patterns to match (add your custom test patterns here)
List<String> namePatterns = new List<String>{
'Test%',
'BulkTest%',
'BatchTest%',
'BulkData%',
'BulkImport%',
'Hierarchy%'
};
List<String> emailPatterns = new List<String>{
'%@testfactory.example.com',
'%@bulktest.example.com',
'%@batchtest.example.com'
};
// ═══════════════════════════════════════════════════════════════════════════════
// TRACKING
// ═══════════════════════════════════════════════════════════════════════════════
Map<String, Integer> recordCounts = new Map<String, Integer>();
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('COMPREHENSIVE TEST DATA CLEANUP');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('Mode: ' + (previewOnly ? 'PREVIEW ONLY' : '⚠️ DELETE MODE'));
System.debug('Patterns: ' + namePatterns);
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('');
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 1: FIND TASKS (delete first - activity records)
// ═══════════════════════════════════════════════════════════════════════════════
List<Task> tasksToDelete = [
SELECT Id, Subject, WhoId, WhatId
FROM Task
WHERE Subject LIKE 'Test%'
OR Subject LIKE 'BulkTest%'
OR Subject LIKE 'Hierarchy%'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Task', tasksToDelete.size());
System.debug('📋 Tasks found: ' + tasksToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 2: FIND EVENTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Event> eventsToDelete = [
SELECT Id, Subject, WhoId, WhatId
FROM Event
WHERE Subject LIKE 'Test%'
OR Subject LIKE 'All Day Event%'
OR Subject LIKE 'Recurring Event%'
OR Subject LIKE 'Calendar Event%'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Event', eventsToDelete.size());
System.debug('📋 Events found: ' + eventsToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 3: FIND CASES
// ═══════════════════════════════════════════════════════════════════════════════
List<Case> casesToDelete = [
SELECT Id, Subject, AccountId
FROM Case
WHERE Subject LIKE 'Test%'
OR Subject LIKE 'Hierarchy Case%'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Case', casesToDelete.size());
System.debug('📋 Cases found: ' + casesToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 4: FIND OPPORTUNITIES
// ═══════════════════════════════════════════════════════════════════════════════
List<Opportunity> opportunitiesToDelete = [
SELECT Id, Name, AccountId
FROM Opportunity
WHERE Name LIKE 'Test%'
OR Name LIKE 'BulkTest%'
OR Name LIKE 'BatchTest%'
OR Name LIKE 'Hierarchy%'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Opportunity', opportunitiesToDelete.size());
System.debug('📋 Opportunities found: ' + opportunitiesToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 5: FIND CONTACTS
// ═══════════════════════════════════════════════════════════════════════════════
List<Contact> contactsToDelete = [
SELECT Id, FirstName, LastName, Email, AccountId
FROM Contact
WHERE LastName LIKE 'Contact_%'
OR LastName LIKE 'Hierarchy%'
OR LastName LIKE 'Test%'
OR Email LIKE '%@testfactory.example.com'
OR Email LIKE '%@bulktest.example.com'
OR Email LIKE '%@batchtest.example.com'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Contact', contactsToDelete.size());
System.debug('📋 Contacts found: ' + contactsToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 6: FIND LEADS
// ═══════════════════════════════════════════════════════════════════════════════
List<Lead> leadsToDelete = [
SELECT Id, FirstName, LastName, Company
FROM Lead
WHERE LastName LIKE 'Lead%'
OR LastName LIKE 'Test%'
OR Company LIKE 'Test Company%'
OR Email LIKE '%@testlead.example.com'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Lead', leadsToDelete.size());
System.debug('📋 Leads found: ' + leadsToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// STEP 7: FIND ACCOUNTS (delete last - parent records)
// ═══════════════════════════════════════════════════════════════════════════════
List<Account> accountsToDelete = [
SELECT Id, Name
FROM Account
WHERE Name LIKE 'Test%'
OR Name LIKE 'BulkTest%'
OR Name LIKE 'BatchTest%'
OR Name LIKE 'BulkData%'
OR Name LIKE 'BulkImport%'
OR Name LIKE 'Hierarchy%'
OR Name LIKE 'Root Account%'
OR Name LIKE 'Level%'
LIMIT :maxRecordsPerObject
];
recordCounts.put('Account', accountsToDelete.size());
System.debug('📋 Accounts found: ' + accountsToDelete.size());
// ═══════════════════════════════════════════════════════════════════════════════
// SUMMARY
// ═══════════════════════════════════════════════════════════════════════════════
Integer totalRecords = 0;
for (String objName : recordCounts.keySet()) {
totalRecords += recordCounts.get(objName);
}
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('CLEANUP SUMMARY');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug(' Tasks: ' + recordCounts.get('Task'));
System.debug(' Events: ' + recordCounts.get('Event'));
System.debug(' Cases: ' + recordCounts.get('Case'));
System.debug(' Opportunities: ' + recordCounts.get('Opportunity'));
System.debug(' Contacts: ' + recordCounts.get('Contact'));
System.debug(' Leads: ' + recordCounts.get('Lead'));
System.debug(' Accounts: ' + recordCounts.get('Account'));
System.debug(' ─────────────────────────────────');
System.debug(' TOTAL: ' + totalRecords);
System.debug('═══════════════════════════════════════════════════════════════');
// ═══════════════════════════════════════════════════════════════════════════════
// EXECUTE DELETE
// ═══════════════════════════════════════════════════════════════════════════════
if (!previewOnly && totalRecords > 0) {
System.debug('');
System.debug('⚠️ DELETING RECORDS...');
System.debug(' Deleting in dependency order (children before parents)');
System.debug('');
Integer deletedCount = 0;
// Delete in order: Activities → Children → Parents
try {
if (!tasksToDelete.isEmpty()) {
delete tasksToDelete;
deletedCount += tasksToDelete.size();
System.debug(' ✓ Deleted ' + tasksToDelete.size() + ' Tasks');
}
if (!eventsToDelete.isEmpty()) {
delete eventsToDelete;
deletedCount += eventsToDelete.size();
System.debug(' ✓ Deleted ' + eventsToDelete.size() + ' Events');
}
if (!casesToDelete.isEmpty()) {
delete casesToDelete;
deletedCount += casesToDelete.size();
System.debug(' ✓ Deleted ' + casesToDelete.size() + ' Cases');
}
if (!opportunitiesToDelete.isEmpty()) {
delete opportunitiesToDelete;
deletedCount += opportunitiesToDelete.size();
System.debug(' ✓ Deleted ' + opportunitiesToDelete.size() + ' Opportunities');
}
if (!contactsToDelete.isEmpty()) {
delete contactsToDelete;
deletedCount += contactsToDelete.size();
System.debug(' ✓ Deleted ' + contactsToDelete.size() + ' Contacts');
}
if (!leadsToDelete.isEmpty()) {
delete leadsToDelete;
deletedCount += leadsToDelete.size();
System.debug(' ✓ Deleted ' + leadsToDelete.size() + ' Leads');
}
if (!accountsToDelete.isEmpty()) {
delete accountsToDelete;
deletedCount += accountsToDelete.size();
System.debug(' ✓ Deleted ' + accountsToDelete.size() + ' Accounts');
}
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('✅ CLEANUP COMPLETE');
System.debug(' Deleted: ' + deletedCount + ' records');
System.debug(' Status: Moved to Recycle Bin');
System.debug('═══════════════════════════════════════════════════════════════');
} catch (DmlException e) {
System.debug('');
System.debug('❌ DELETE ERROR: ' + e.getMessage());
System.debug(' Some records may have dependencies preventing deletion.');
System.debug(' Try running again after resolving dependencies.');
}
} else if (previewOnly) {
System.debug('');
System.debug('ℹ️ PREVIEW MODE - No records deleted');
System.debug(' To delete, set previewOnly = false and run again');
System.debug('');
System.debug(' ⚠️ Please review the counts above before deleting!');
}
// ═══════════════════════════════════════════════════════════════════════════════
// BATCH APEX VERSION (For very large datasets)
// ═══════════════════════════════════════════════════════════════════════════════
/*
For datasets larger than 10,000 records, use batch Apex:
public class TestDataCleanupBatch implements Database.Batchable<SObject> {
private String objectName;
private String query;
public TestDataCleanupBatch(String objName, String soqlQuery) {
this.objectName = objName;
this.query = soqlQuery;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<SObject> records) {
delete records;
}
public void finish(Database.BatchableContext bc) {
System.debug('Cleanup complete for ' + objectName);
}
}
// Execute for each object (in order):
Database.executeBatch(new TestDataCleanupBatch('Task',
'SELECT Id FROM Task WHERE Subject LIKE \'Test%\''), 200);
Database.executeBatch(new TestDataCleanupBatch('Account',
'SELECT Id FROM Account WHERE Name LIKE \'Test%\''), 200);
*/
/**
* ═══════════════════════════════════════════════════════════════════════════════
* TRANSACTION ROLLBACK PATTERN
* Using Database.Savepoint for test isolation
* ═══════════════════════════════════════════════════════════════════════════════
*
* PURPOSE:
* Create test data, run tests, then roll back all changes.
* This ensures complete test isolation without leaving orphan data.
*
* HOW IT WORKS:
* 1. Create a savepoint BEFORE creating any test data
* 2. Create your test data
* 3. Run your test/validation logic
* 4. Roll back to the savepoint (deletes all created records)
*
* USE CASES:
* • Unit testing with data setup
* • Integration testing without pollution
* • Manual testing with automatic cleanup
* • Validation runs that shouldn't persist
*
* LIMITATIONS:
* • Cannot roll back asynchronous operations
* • Savepoints don't persist across transactions
* • Maximum 5 savepoints per transaction
*
* ═══════════════════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════════════════
// BASIC SAVEPOINT/ROLLBACK PATTERN
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('TRANSACTION ROLLBACK DEMONSTRATION');
System.debug('═══════════════════════════════════════════════════════════════');
// Step 1: Create savepoint BEFORE any DML
Savepoint sp = Database.setSavepoint();
System.debug('✓ Savepoint created');
// Track what we create for verification
Set<Id> createdAccountIds = new Set<Id>();
Set<Id> createdContactIds = new Set<Id>();
try {
// ═══════════════════════════════════════════════════════════════════════
// Step 2: Create test data
// ═══════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Creating test data...');
// Create Accounts
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
testAccounts.add(new Account(
Name = 'Rollback Test Account ' + i,
Industry = 'Technology',
Description = 'This record will be rolled back'
));
}
insert testAccounts;
for (Account acc : testAccounts) {
createdAccountIds.add(acc.Id);
}
System.debug(' Created ' + testAccounts.size() + ' Accounts');
// Create Contacts
List<Contact> testContacts = new List<Contact>();
for (Account acc : testAccounts) {
testContacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact',
AccountId = acc.Id,
Email = 'test@rollback.example.com'
));
}
insert testContacts;
for (Contact con : testContacts) {
createdContactIds.add(con.Id);
}
System.debug(' Created ' + testContacts.size() + ' Contacts');
// ═══════════════════════════════════════════════════════════════════════
// Step 3: Verify data exists
// ═══════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Verifying data exists...');
Integer accountCount = [SELECT COUNT() FROM Account WHERE Id IN :createdAccountIds];
Integer contactCount = [SELECT COUNT() FROM Contact WHERE Id IN :createdContactIds];
System.debug(' Accounts in database: ' + accountCount);
System.debug(' Contacts in database: ' + contactCount);
// ═══════════════════════════════════════════════════════════════════════
// Step 4: Run your test logic here
// ═══════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Running test logic...');
// Example: Verify trigger fired correctly
// Example: Check field calculations
// Example: Validate workflow results
System.debug(' Test logic completed');
} finally {
// ═══════════════════════════════════════════════════════════════════════
// Step 5: Roll back ALL changes
// ═══════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Rolling back transaction...');
Database.rollback(sp);
System.debug('✓ Rollback complete');
}
// ═══════════════════════════════════════════════════════════════════════════════
// Step 6: Verify rollback worked
// ═══════════════════════════════════════════════════════════════════════════════
System.debug('');
System.debug('Verifying rollback...');
Integer accountCountAfter = [SELECT COUNT() FROM Account WHERE Id IN :createdAccountIds];
Integer contactCountAfter = [SELECT COUNT() FROM Contact WHERE Id IN :createdContactIds];
System.debug(' Accounts remaining: ' + accountCountAfter + ' (expected: 0)');
System.debug(' Contacts remaining: ' + contactCountAfter + ' (expected: 0)');
if (accountCountAfter == 0 && contactCountAfter == 0) {
System.debug('');
System.debug('═══════════════════════════════════════════════════════════════');
System.debug('✅ ROLLBACK SUCCESSFUL - All test data removed');
System.debug('═══════════════════════════════════════════════════════════════');
} else {
System.debug('');
System.debug('⚠️ WARNING: Some records were not rolled back');
}
// ═══════════════════════════════════════════════════════════════════════════════
// PATTERN: TEST METHOD WITH SAVEPOINT
// ═══════════════════════════════════════════════════════════════════════════════
/*
@isTest
public class MyTestClass {
@isTest
static void testWithRollback() {
// In test methods, DML is automatically rolled back
// This example shows explicit savepoint usage
Savepoint sp = Database.setSavepoint();
try {
// Create test data
Account testAccount = new Account(Name = 'Test');
insert testAccount;
// Run the test
Test.startTest();
MyClass.processAccount(testAccount.Id);
Test.stopTest();
// Assert results
Account result = [SELECT Field__c FROM Account WHERE Id = :testAccount.Id];
System.assertEquals('Expected', result.Field__c);
} finally {
// Optional explicit rollback (tests auto-rollback anyway)
Database.rollback(sp);
}
}
}
*/
// ═══════════════════════════════════════════════════════════════════════════════
// PATTERN: NESTED SAVEPOINTS
// ═══════════════════════════════════════════════════════════════════════════════
/*
// You can have up to 5 savepoints
Savepoint sp1 = Database.setSavepoint();
// Create data set 1
insert accounts;
Savepoint sp2 = Database.setSavepoint();
// Create data set 2
insert contacts;
// Rollback to sp2 (removes contacts, keeps accounts)
Database.rollback(sp2);
// Rollback to sp1 (removes everything)
Database.rollback(sp1);
*/
// ═══════════════════════════════════════════════════════════════════════════════
// PATTERN: CONDITIONAL ROLLBACK
// ═══════════════════════════════════════════════════════════════════════════════
/*
Savepoint sp = Database.setSavepoint();
Boolean shouldCommit = true;
try {
// Create data
insert records;
// Validate
if (!isValid(records)) {
shouldCommit = false;
}
} finally {
if (!shouldCommit) {
Database.rollback(sp);
System.debug('Changes rolled back due to validation failure');
} else {
System.debug('Changes committed');
}
}
*/
// ═══════════════════════════════════════════════════════════════════════════════
// IMPORTANT NOTES
// ═══════════════════════════════════════════════════════════════════════════════
/*
1. SAVEPOINT LIMITATIONS:
• Max 5 savepoints per transaction
• Cannot cross transaction boundaries
• Does not roll back @future, Queueable, or Batch operations
• Does not roll back platform events
• Does not roll back email sends
2. WHAT GETS ROLLED BACK:
✓ All DML operations (insert, update, delete, undelete)
✓ Workflow field updates triggered by DML
✓ Process Builder / Flow changes triggered by DML
3. WHAT DOES NOT GET ROLLED BACK:
✗ Asynchronous operations (@future, Queueable, Batch, Scheduled)
✗ Platform events
✗ Outbound messages
✗ Email sends
✗ Callouts (HTTP requests)
✗ Debug logs
4. BEST PRACTICES:
• Create savepoint as early as possible
• Always use try-finally to ensure rollback
• Verify rollback worked by querying records
• Don't rely on savepoints for async operations
*/
Name,Industry,Type,AnnualRevenue,NumberOfEmployees,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry,Phone,Website,Description
Test Import Account 001,Technology,Prospect,1000000,100,100 Main Street,San Francisco,CA,94102,USA,(555) 100-0001,https://testaccount001.example.com,Test account for import validation
Test Import Account 002,Healthcare,Customer,2500000,250,200 Oak Avenue,New York,NY,10001,USA,(555) 100-0002,https://testaccount002.example.com,Healthcare customer account
Test Import Account 003,Finance,Partner,5000000,500,300 Wall Street,Chicago,IL,60601,USA,(555) 100-0003,https://testaccount003.example.com,Financial services partner
Test Import Account 004,Manufacturing,Prospect,750000,75,400 Industrial Way,Detroit,MI,48201,USA,(555) 100-0004,https://testaccount004.example.com,Manufacturing prospect
Test Import Account 005,Retail,Customer,3000000,350,500 Shopping Lane,Los Angeles,CA,90001,USA,(555) 100-0005,https://testaccount005.example.com,Retail chain customer
Test Import Account 006,Education,Other,500000,50,600 Campus Drive,Boston,MA,02101,USA,(555) 100-0006,https://testaccount006.example.com,Educational institution
Test Import Account 007,Energy,Prospect,10000000,1000,700 Pipeline Road,Houston,TX,77001,USA,(555) 100-0007,https://testaccount007.example.com,Energy sector prospect
Test Import Account 008,Media,Customer,1500000,150,800 Broadcast Blvd,Atlanta,GA,30301,USA,(555) 100-0008,https://testaccount008.example.com,Media company
Test Import Account 009,Technology,Customer,8000000,800,900 Tech Park,Seattle,WA,98101,USA,(555) 100-0009,https://testaccount009.example.com,Enterprise tech customer
Test Import Account 010,Healthcare,Prospect,2000000,200,1000 Medical Center,Denver,CO,80201,USA,(555) 100-0010,https://testaccount010.example.com,Healthcare prospect
FirstName,LastName,Email,Phone,MobilePhone,Title,Department,MailingStreet,MailingCity,MailingState,MailingPostalCode,MailingCountry,Account.Name,Description
John,Smith,john.smith@testimport.example.com,(555) 200-0001,(555) 300-0001,CEO,Executive,100 Main Street,San Francisco,CA,94102,USA,Test Import Account 001,Primary contact for account
Jane,Doe,jane.doe@testimport.example.com,(555) 200-0002,(555) 300-0002,CFO,Finance,100 Main Street,San Francisco,CA,94102,USA,Test Import Account 001,Finance lead
Robert,Johnson,robert.johnson@testimport.example.com,(555) 200-0003,(555) 300-0003,CTO,Technology,200 Oak Avenue,New York,NY,10001,USA,Test Import Account 002,Technical decision maker
Emily,Williams,emily.williams@testimport.example.com,(555) 200-0004,(555) 300-0004,VP Sales,Sales,200 Oak Avenue,New York,NY,10001,USA,Test Import Account 002,Sales executive
Michael,Brown,michael.brown@testimport.example.com,(555) 200-0005,(555) 300-0005,Director,Operations,300 Wall Street,Chicago,IL,60601,USA,Test Import Account 003,Operations director
Sarah,Davis,sarah.davis@testimport.example.com,(555) 200-0006,(555) 300-0006,Manager,Marketing,400 Industrial Way,Detroit,MI,48201,USA,Test Import Account 004,Marketing manager
David,Miller,david.miller@testimport.example.com,(555) 200-0007,(555) 300-0007,Senior Developer,Engineering,500 Shopping Lane,Los Angeles,CA,90001,USA,Test Import Account 005,Technical lead
Jennifer,Wilson,jennifer.wilson@testimport.example.com,(555) 200-0008,(555) 300-0008,Analyst,Analytics,600 Campus Drive,Boston,MA,02101,USA,Test Import Account 006,Data analyst
Christopher,Moore,christopher.moore@testimport.example.com,(555) 200-0009,(555) 300-0009,Consultant,Consulting,700 Pipeline Road,Houston,TX,77001,USA,Test Import Account 007,External consultant
Amanda,Taylor,amanda.taylor@testimport.example.com,(555) 200-0010,(555) 300-0010,Project Manager,PMO,800 Broadcast Blvd,Atlanta,GA,30301,USA,Test Import Account 008,Project lead
Name,Status__c,Type__c,Amount__c,Date__c,Description__c,Account__r.Name
Test Custom Record 001,Draft,Standard,1000.00,2025-03-01,Test custom object record 1,Test Import Account 001
Test Custom Record 002,Active,Premium,2500.00,2025-03-02,Test custom object record 2,Test Import Account 001
Test Custom Record 003,Pending,Standard,1500.00,2025-03-03,Test custom object record 3,Test Import Account 002
Test Custom Record 004,Active,Enterprise,5000.00,2025-03-04,Test custom object record 4,Test Import Account 002
Test Custom Record 005,Complete,Standard,750.00,2025-03-05,Test custom object record 5,Test Import Account 003
Test Custom Record 006,Draft,Premium,3000.00,2025-03-06,Test custom object record 6,Test Import Account 003
Test Custom Record 007,Active,Standard,1250.00,2025-03-07,Test custom object record 7,Test Import Account 004
Test Custom Record 008,Pending,Enterprise,7500.00,2025-03-08,Test custom object record 8,Test Import Account 005
Test Custom Record 009,Complete,Premium,4000.00,2025-03-09,Test custom object record 9,Test Import Account 006
Test Custom Record 010,Active,Standard,2000.00,2025-03-10,Test custom object record 10,Test Import Account 007
Name,StageName,CloseDate,Amount,Probability,Type,LeadSource,NextStep,Description,Account.Name
Test Opportunity 001,Prospecting,2025-03-15,50000,10,New Customer,Web,Initial discovery call,New opportunity from website lead,Test Import Account 001
Test Opportunity 002,Qualification,2025-03-20,75000,20,New Customer,Phone Inquiry,Schedule demo,Qualified lead from phone inquiry,Test Import Account 001
Test Opportunity 003,Needs Analysis,2025-03-25,100000,40,Existing Customer - Upgrade,Partner Referral,Present solution options,Upgrade opportunity from partner,Test Import Account 002
Test Opportunity 004,Value Proposition,2025-04-01,150000,50,New Customer,Trade Show,Send proposal,Trade show lead in negotiation,Test Import Account 002
Test Opportunity 005,Proposal/Price Quote,2025-04-10,200000,65,New Customer,Web,Follow up on proposal,Proposal sent awaiting response,Test Import Account 003
Test Opportunity 006,Negotiation/Review,2025-04-15,250000,80,Existing Customer - Replacement,Employee Referral,Finalize contract terms,Replacement deal in final stages,Test Import Account 003
Test Opportunity 007,Prospecting,2025-04-20,30000,10,New Customer,Other,Initial outreach,Cold outreach opportunity,Test Import Account 004
Test Opportunity 008,Qualification,2025-04-25,45000,20,New Customer,Web,Qualify budget,Inbound lead qualification,Test Import Account 005
Test Opportunity 009,Needs Analysis,2025-05-01,125000,40,New Customer,Partner Referral,Technical assessment,Partner referral in analysis,Test Import Account 006
Test Opportunity 010,Closed Won,2025-02-28,175000,100,New Customer,Trade Show,Contract signed,Successfully closed deal,Test Import Account 007
{
"records": [
{
"attributes": {
"type": "Account",
"referenceId": "AccountRef1"
},
"Name": "Tree Import Account 001",
"Industry": "Technology",
"Type": "Customer",
"AnnualRevenue": 5000000,
"BillingCity": "San Francisco",
"BillingState": "CA",
"BillingCountry": "USA",
"Description": "Parent account with multiple contacts",
"Contacts": {
"records": [
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef1"
},
"FirstName": "Alice",
"LastName": "Johnson",
"Email": "alice.johnson@treeimport.example.com",
"Phone": "(555) 400-0001",
"Title": "CEO",
"Department": "Executive"
},
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef2"
},
"FirstName": "Bob",
"LastName": "Smith",
"Email": "bob.smith@treeimport.example.com",
"Phone": "(555) 400-0002",
"Title": "CTO",
"Department": "Technology"
},
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef3"
},
"FirstName": "Carol",
"LastName": "Williams",
"Email": "carol.williams@treeimport.example.com",
"Phone": "(555) 400-0003",
"Title": "CFO",
"Department": "Finance"
}
]
}
},
{
"attributes": {
"type": "Account",
"referenceId": "AccountRef2"
},
"Name": "Tree Import Account 002",
"Industry": "Healthcare",
"Type": "Prospect",
"AnnualRevenue": 2500000,
"BillingCity": "New York",
"BillingState": "NY",
"BillingCountry": "USA",
"Description": "Healthcare prospect with contacts",
"Contacts": {
"records": [
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef4"
},
"FirstName": "David",
"LastName": "Brown",
"Email": "david.brown@treeimport.example.com",
"Phone": "(555) 400-0004",
"Title": "VP Operations",
"Department": "Operations"
},
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef5"
},
"FirstName": "Emma",
"LastName": "Davis",
"Email": "emma.davis@treeimport.example.com",
"Phone": "(555) 400-0005",
"Title": "Director",
"Department": "Marketing"
}
]
}
},
{
"attributes": {
"type": "Account",
"referenceId": "AccountRef3"
},
"Name": "Tree Import Account 003",
"Industry": "Finance",
"Type": "Partner",
"AnnualRevenue": 10000000,
"BillingCity": "Chicago",
"BillingState": "IL",
"BillingCountry": "USA",
"Description": "Financial services partner",
"Contacts": {
"records": [
{
"attributes": {
"type": "Contact",
"referenceId": "ContactRef6"
},
"FirstName": "Frank",
"LastName": "Miller",
"Email": "frank.miller@treeimport.example.com",
"Phone": "(555) 400-0006",
"Title": "Partner Manager",
"Department": "Partnerships"
}
]
}
}
]
}