# API Reference: IEloquent / Eloquent / MockEloquent

`IEloquent` is ApexEloquent's data-access contract (the interface). Production uses `Eloquent`; tests use `MockEloquent`, both DI'd via the Layered Constructor Pattern.

For usage and typical scenarios, see [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access).

> This page covers both the v3 and v2 lines. The only difference between them is the execution mode, so that part is called out separately under "Execution Mode (v3 line)".

## The Three Relationships

```
IEloquent  (interface)       ← the contract Usecase depends on
   ↑                       
   ├── Eloquent             ← production. Hits standard SOQL / DML directly
   └── MockEloquent         ← tests. No DB; provides Spy + failOn*
```

The Usecase holds an `IEloquent`-typed field, into which it receives `new Eloquent()` in production and `new MockEloquent(...)` in tests via the Layered Constructor.

## IEloquent (Interface)

Every contract method is implemented by both `Eloquent` and `MockEloquent`.

### Labeling

| Signature | Return | Purpose |
|---|---|---|
| `label(String labelName)` | `IEloquent` | Tags the next operation with a label so the chain stays composable. A single `IEloquent` can be **multiplexed by label** — fold "last-month / this-year / Account update" purposes onto one instance instead of DI-ing a separate `IEloquent` per purpose |

```apex
// Distinguish two SOQLs and one DML on the same IEloquent
IEntry job = this.eloquent.label('jobLoad').firstOrFail(jobScribe);
List<IEntry> details = this.eloquent.label('detailLoad').get(detailScribe);
this.eloquent.label('finalDml').doUpdate(toUpdate);
```

Passing `null` or blank to `label(...)` throws.

`'default'` is the name **assigned internally to label-less operations**. `label('default')` itself goes through, but it points at the same slot as an unlabelled call, so the intent doesn't read. Use a different name. (`whenLabel('default')`, on the other hand, is explicitly rejected as redundant.)

**Opt-in strict mode**: the first `.label(...)` call on an `IEloquent` instance switches it into **strict mode** (sticky for the rest of the instance's lifetime):

- Every subsequent operation must be preceded by `.label(...)` (label-less operations throw)
- Each label can be consumed at most once per instance (reusing throws)
- Instances that never call `.label()` stay in lenient mode (full backward compatibility)

This closes the "I added a new DML and forgot to label it" footgun at runtime: the offending call throws instead of silently landing in the default bucket.

### Execution Mode (v3 line only)

| Signature | Returns | Purpose |
|---|---|---|
| `userMode()` | `IEloquent` | Run subsequent SOQL / DML as `AccessLevel.USER_MODE` (the v3 default) |
| `systemMode()` | `IEloquent` | Run subsequent SOQL / DML as `AccessLevel.SYSTEM_MODE`, ignoring FLS and object permissions |

In the v3 line `Eloquent` became `inherited sharing` and SOQL / DML default to **user mode** (honouring the running user's field and object permissions). Only system processes — aggregation, stamping, data migration, anything that must complete no matter who triggered it — opt out explicitly with `systemMode()`.

```apex
// A system process: mark the calling class without sharing, then be explicit here
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
```

- **Sticky**: calling it once applies to every subsequent operation on that instance (unlike `label()`, which resets per operation)
- **A no-op on `MockEloquent`**: it simply returns itself, so unit tests can ignore the mode entirely
- ⚠️ `systemMode()` only lifts **FLS and object permissions**. **Sharing (record visibility) is a separate axis** — to lift that, the calling class must be declared `without sharing`

### Query Methods

| Signature | Return | Purpose |
|---|---|---|
| `get(Scribe scribe)` | `List<IEntry>` | Execute the query. Empty list on 0 hits |
| `first(Scribe scribe)` | `IEntry` | The first record. `null` on 0 hits |
| `firstOrFail(Scribe scribe)` | `IEntry` | The first record. Throws `ApexEloquentException` on 0 hits |
| `firstOrFail(Scribe scribe, Exception orFail)` | `IEntry` | The first record. On 0 hits, throws **the exception you passed**, as-is |
| `getAsSObject(Scribe scribe)` | `List<SObject>` | `SObject` list (last resort) |
| `firstAsSObject(Scribe scribe)` | `SObject` | SObject version of the first record |
| `firstOrFailAsSObject(Scribe scribe)` | `SObject` | SObject version of the first record; throws on 0 hits |
| `rawSoql(String soql)` | `List<IEntry>` | Bypass `Scribe` and run raw SOQL (last resort; SELECT-omission detection is disabled) |

**When `firstOrFail(scribe, orFail)` earns its keep**: when zero rows is a business error you want to surface to the screen. The exception you hand it is thrown verbatim, so you skip `first` + null check + your own throw.

What matters is **where it gets caught**. Hand it a business exception and it lands in your `catch (UsecaseException)`.

```apex
IEntry job;
try {
  job = this.eloquent.label(LBL_JOB).firstOrFail(
    jobScribe,
    new UsecaseException('The requested job was not found.')
  );
} catch (UsecaseException ex) {
  this.t.skip('Business error: ' + ex.getMessage());
}
```

The no-argument `firstOrFail(scribe)` throws `ApexEloquentException`, which never reaches that `catch`.

### DML Methods

| Signature | Return |
|---|---|
| `doInsert(SObject record)` | `SObject` |
| `doInsert(List<SObject> records)` | `List<SObject>` |
| `doUpdate(SObject record)` | `SObject` |
| `doUpdate(IEntry entry)` | `IEntry` |
| `doUpdate(List<SObject> records)` | `List<SObject>` |
| `doUpdate(List<IEntry> entries)` | `List<IEntry>` |
| `doUpdate(SObject record, Boolean allOrNone)` | `Database.SaveResult` |
| `doUpdate(IEntry entry, Boolean allOrNone)` | `Database.SaveResult` |
| `doUpdate(List<SObject> records, Boolean allOrNone)` | `List<Database.SaveResult>` |
| `doUpdate(List<IEntry> entries, Boolean allOrNone)` | `List<Database.SaveResult>` |
| `doUpsert(SObject record)` | `SObject` |
| `doUpsert(IEntry entry)` | `IEntry` |
| `doUpsert(List<SObject> records)` | `List<SObject>` |
| `doUpsert(List<IEntry> entries)` | `List<IEntry>` |
| `doUpsertByExternalId(SObject record, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` |
| `doUpsertByExternalId(IEntry entry, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` |
| `doUpsertByExternalId(List<SObject> records, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List<Database.UpsertResult>` |
| `doUpsertByExternalId(List<IEntry> entries, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List<Database.UpsertResult>` |
| `doDelete(SObject record)` | `void` |
| `doDelete(IEntry entry)` | `void` |
| `doDelete(List<SObject> records)` | `void` |
| `doDelete(List<IEntry> entries)` | `void` |

Default to bulk; use the single-record versions only when exactly one record is guaranteed.

**`doUpdate` with `allOrNone`**: the return type is `Database.SaveResult` / `List<Database.SaveResult>`, so partial-success per-record outcomes can be inspected through the `IEloquent` abstraction. Useful in ETL / incremental sync / batch migration paths that log failed records and continue.

**`doUpsertByExternalId`**: brings the standard `Database.upsert(records, externalIdField, allOrNone)` API under the `IEloquent` abstraction. Essential for "upsert by external Id key" in ETL / incremental sync / batch migration, handled through a shared production / Mock contract.

## Eloquent (Production)

The production class implementing `IEloquent`. It hits standard SOQL / DML directly. **No additional public methods** (interface-only).

```apex
IEloquent eloquent = new Eloquent();
List<IEntry> opps = eloquent.get(scribe);
eloquent.doUpdate(opps);
```

## MockEloquent (Mock Extension)

On top of the `IEloquent` contract, it adds test-side **Spy properties** and the **failOn series**.

### Constructors

| Signature | Behavior |
|---|---|
| `new MockEloquent()` | Empty. If you use labels, feed data with `attach(...)` |
| `new MockEloquent(IEntry entry)` | Preloads one record as the label-less (`'default'`) result |
| `new MockEloquent(List<IEntry> entries)` | Same, list version |

Data passed to the constructor only feeds **label-less (`'default'`) operations**. If production code uses `label(...)`, supply it with `attach(label, ...)` instead.

`MockEloquent` **does not evaluate `Scribe`'s WHERE conditions** — it hands back the list you gave it, as-is. To tell condition-varied queries apart, **label each query and feed them separately with `attach(label, ...)`** (below).

### An unattached label throws

Call `get` / `first` / `firstOrFail` under `label('X')` without a matching `attach('X', ...)` and **the test throws**. The error lists the labels that were attached, so a typo shows up immediately.

This guards a specific false positive: mistype a label, get zero rows, fall into the "nothing to do, skip" branch, and **the test goes green while verifying nothing**.

When you genuinely want to test the zero-row path, **attach an empty list** to say so.

```apex
MockEloquent mock = (new MockEloquent())
  .attach(MyUsecase.LBL_FETCH, new List<IEntry>());
```

> Upgrading from an older version can turn some tests red here. Those are the tests that were green while verifying nothing. Rather than mechanically adding empty attaches, check what data should have been injected in the first place.

### Spy Properties and Per-Label Accessors

| Property / Method | Type | Content |
|---|---|---|
| `upsertedRecords` | `List<SObject>` | Records passed to `doInsert` / `doUpdate` / `doUpsert` / `doUpsertByExternalId` for the **`'default'` bucket** (※ `@deprecated`: new code should prefer `upsertedRecordsAt('default')`) |
| `deletedCount` | `Integer` | Total `doDelete` invocations for the **`'default'` bucket** (※ `@deprecated`: new code should prefer `deletedCountAt('default')`) |
| `upsertedRecordsAt(String label)` | `List<SObject>` | Accumulated DML records for the given label |
| `deletedCountAt(String label)` | `Integer` | Delete invocation count for the given label |
| `attach(String label, IEntry entry)` | `MockEloquent` | Preload a single record as the given label's query data (chainable; re-attaching the same label overwrites) |
| `attach(String label, List<IEntry> entries)` | `MockEloquent` | Same, list version |
| `failSave(Id recordId, String errorMessage)` | `MockEloquent` | Make **only the named record fail to save** (chainable) |

```apex
// Label-less, traditional usage (backward compatible)
MockEloquent updateEloquent = new MockEloquent();
(new MyUsecase(input, fetchEloquent, updateEloquent)).invoke();
Assert.areEqual(1, updateEloquent.upsertedRecords.size());
Opportunity updated = (Opportunity) updateEloquent.upsertedRecords[0];
Assert.areEqual('Technology', updated.Industry__c);
```

```apex
// Label-multiplexed — one MockEloquent serves query preloading and DML verification, sorted by label
MockEloquent mock = (new MockEloquent())
  .attach('jobLoad', jobEntry);

(new FinalizeJobUsecase(jobId, mock)).invoke();

Assert.areEqual(1, mock.upsertedRecordsAt('finalDml').size());
```

**Backward compatibility note**: the existing `upsertedRecords` / `deletedCount` public fields continue to work and internally reflect the `'default'` bucket. They are JSDoc-marked `@deprecated`; migrate to `*At('default')` over time. The legacy constructor `new MockEloquent(List<IEntry>)` also still works — the data it accepts feeds the `'default'` source.

### failOn Series (Exception Simulation)

Each method has two overloads: no-argument and `Exception`-accepting. Without an `Exception`, a default exception is thrown.

| Method | Corresponding Contract |
|---|---|
| `failOnGet()` / `failOnGet(Exception e)` | `get(scribe)` |
| `failOnFirst()` / `failOnFirst(Exception e)` | `first(scribe)` |
| `failOnFirstOrFail()` / `failOnFirstOrFail(Exception e)` | `firstOrFail(scribe)` |
| `failOnGetAsSObject()` / `failOnGetAsSObject(Exception e)` | `getAsSObject(scribe)` |
| `failOnFirstAsSObject()` / `failOnFirstAsSObject(Exception e)` | `firstAsSObject(scribe)` |
| `failOnFirstOrFailAsSObject()` / `failOnFirstOrFailAsSObject(Exception e)` | `firstOrFailAsSObject(scribe)` |
| `failOnRawSoql()` / `failOnRawSoql(Exception e)` | `rawSoql(soql)` |
| `failOnDoInsert()` / `failOnDoInsert(Exception e)` | `doInsert(*)` |
| `failOnDoUpdate()` / `failOnDoUpdate(Exception e)` | `doUpdate(*)` |
| `failOnDoUpsert()` / `failOnDoUpsert(Exception e)` | `doUpsert(*)` |
| `failOnDoUpsertByExternalId()` / `failOnDoUpsertByExternalId(Exception e)` | `doUpsertByExternalId(*)` |
| `failOnDoDelete()` / `failOnDoDelete(Exception e)` | `doDelete(*)` |

### Scoping Failures to a Label with whenLabel()

| Signature | Purpose |
|---|---|
| `whenLabel(String label)` | Scopes the most recent `failOn*()` to **fire only for a specific label** (chainable) |

```apex
// Only the DML labeled 'finalDml' fails; the others run normally
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated finalDml failure'))
  .whenLabel('finalDml');
```

Reads as "fail on get when label is X" — useful in label-multiplexed Usecases where you want exactly one side effect to fail.

```apex
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated DML failure'))
  .whenLabel(MyUsecase.LBL_UPDATE);

try {
  (new MyUsecase(input, mock)).invoke();
  Assert.fail('Expected exception');
} catch (DmlException e) {
  Assert.isTrue(TraceFlow.isLastAbort());
}
```

### Expressing "Always Fails" with repeat()

| Signature | Purpose |
|---|---|
| `repeat()` | Call after a `failOn*` to **repeat the failure forever afterward** (count cannot be specified) |

`failOn*()` alone fails just the next call. `failOn*` entries **queue up per method**, so putting two in a row fails the first and second call and lets the third succeed (the retry-then-succeed path).

Add `.repeat()` and the entry you just planted **fails on every subsequent invocation**. That is how you check the *abort* side of "retry up to 3 times, then give up" — without counting calls.

Queues are **kept per label**. A configuration without `whenLabel` targets label-less calls (`'default'`).

⚠️ Every test-setup builder (`attach` / `failOn*` / `whenLabel` / `failSave` / `repeat`) **returns a new instance rather than mutating `this`**. Drop the return value and the configuration is lost, so keep the chain or reassign.

💡 The once-per-label rule counts **successful** operations. A failed operation releases its label, so production code that catches and retries under the same label tests just fine.

⚠️ In a test that uses labels, forgetting `whenLabel` leaves the configuration aimed at `'default'`, where it can never fire. That is detected and reported as an exception, naming the label you should have used.

### The failure patterns you can express

Combining `failOn*` / `whenLabel` / `repeat` gives five shapes.

| Written as | What happens |
|---|---|
| `failOnGet(exA).failOnGet(exB)` | Fails with `exA`, then `exB`, and **succeeds on the third call** |
| `failOnGet(ex).repeat()` | **Fails every time** |
| `failOnGet(ex).whenLabel('opp')` | `'opp'` fails once; **a retry succeeds** |
| `failOnGet(exA).whenLabel('opp')`<br>`.failOnGet(exB).whenLabel('opp')` | `'opp'` fails twice in a row, **succeeds on the third** |
| `failOnGet(ex).whenLabel('opp').repeat()` | `'opp'` **fails every time** |

```apex
// "fail once, succeed on retry"
MockEloquent mock = (new MockEloquent())
  .attach('opp', new List<IEntry>{ oppEntry })
  .failOnGet(new QueryException('boom'))
  .whenLabel('opp');

// 1st call: throws
// 2nd call: the failure released the label, so the attached data comes back
```

Because queues are per label, failures aimed at different labels on the same method never interfere.

```apex
MockEloquent mock = (new MockEloquent())
  .failOnGet(new QueryException('fetch failed')).whenLabel('fetch')
  .failOnDoUpdate(new DmlException('save failed')).whenLabel('update');
```

Add `failSave` — where the call succeeds but a specific record is not saved — and that is the whole of what `MockEloquent` can express.

### failSave(): partial save failure

`failSave` differs in kind from `failOn*`. Rather than throwing, **the call succeeds while the named record is left unsaved** — the partial failure of an `allOrNone` DML.

```apex
Id badId = MockEntry.of(Account.class).autoId(2).getId();

MockEloquent mock = (new MockEloquent())
  .failSave(badId, 'Rejected by a validation rule');
```

- `allOrNone = false` — that record's `SaveResult` carries `success = false` and your message, and it is **not recorded by the spy** (it was never saved)
- `allOrNone = true` — as with real all-or-nothing DML, **the whole operation throws** before anything is recorded

The target is named by **record Id**, not index, so pair it with `MockEntry.autoId()`. It survives records being added later.

## Read Next

- [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access): the usage guide
- [API Reference: Scribe](/apex-stem/docs/apex-eloquent-api-scribe): the query-assembly side
- [API Reference: IEntry / Entry / MockEntry](/apex-stem/docs/apex-eloquent-api-entry): the `IEntry` side that's returned
- [Layered Constructor Pattern](/apex-stem/docs/layered-constructor-pattern): the design for DI'ing `IEloquent` into a Usecase
- [ApexEloquent Guide](/apex-stem/docs/apex-eloquent-guide): back to the guide index
