# API Reference: IEntry / Entry / MockEntry

`IEntry` is ApexEloquent's record wrapper (the interface). The return value of `Eloquent.get(scribe)` and similar is `List<IEntry>`. Production uses `Entry`; tests use `MockEntry`.

For usage and typical scenarios, see [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access) and [Parent Fields, Child Subqueries, and Many-to-Many](/apex-stem/docs/apex-eloquent-relations).

## The Three Relationships

```
IEntry  (interface)        ← the type handled by Usecases / business logic
   ↑
   ├── Entry               ← production. Wraps SObject or AggregateResult
   └── MockEntry           ← tests. Not bound to an SObject type; any field can be set
```

`IEntry` is an `SObject` wrapper, and via `MockEntry.set` it can express test data unreachable from raw `SObject` (writes to formula fields, rollups, parent relationships, auto-number). `AggregateResult` also works through the same `IEntry` interface, so the consumer-side code stays uniform between SObject queries and aggregate queries.

## IEntry (Interface)

### Field Access

| Signature | Return | Purpose |
|---|---|---|
| `get(String fieldName)` | `Object` | Read any field's value (cast required) |
| `put(String fieldName, Object value)` | `void` | Write a value |
| `getId()` | `Id` | Dedicated Id getter (no cast) |
| `getName()` | `String` | Dedicated Name getter (no cast) |
| `getRecord()` | `SObject` | Extract the wrapped `SObject` (last resort) |
| `setRecord(SObject record)` | `IEntry` | Internal use. Swap the SObject |
| `setFieldStructure(FieldStructure fs)` | `IEntry` | Internal use. Set the SELECT-omission detection schema |
| `setDescribeResult(Schema.DescribeSObjectResult)` | `void` | Internal use |

```apex
IEntry oppEntry = eloquent.first(oppScribe);
Id oppId = oppEntry.getId();
String name = oppEntry.getName();
String industry = (String) oppEntry.get('Industry__c');
oppEntry.put('Status__c', 'Active');
```

### Relations (reading)

| Signature | Return | Purpose |
|---|---|---|
| `getParent(String parentIdFieldName)` | `IEntry` | Get the parent record (e.g. `'AccountId'`) |
| `getChildren(String name)` | `List<IEntry>` | Child record list |
| `getThrough(String junctionName, String relatedKey)` | `List<IEntry>` | Many-to-many. Get the related target via a junction object |

The first argument of `getChildren` / `getThrough` **resolves as either an object name or a relationship name**. It looks for an object name first and falls back to a relationship name.

```apex
// Both give the same result
List<IEntry> opps = accountEntry.getChildren('Opportunity');    // object name
List<IEntry> opps = accountEntry.getChildren('Opportunities');  // relationship name
```

If you declared `relationName(...)` on the `Scribe` side, that name works too.

#### Deprecated: `*ByRelationName`

| Signature | Use instead |
|---|---|
| `getChildrenByRelationName(String childRelationName)` | `getChildren(String)` |
| `getThroughByRelationName(String junctionRelationName, String relatedKey)` | `getThrough(String, String)` |

Now that name resolution is unified, a relationship-name-only entry point is unnecessary. They remain for backward compatibility, but **do not use them in new code** — they may be removed in a future version.

## Entry (Production)

The production class implementing `IEntry`. Supports both SObject-derived and AggregateResult-derived modes, and internally enforces SELECT-omission detection based on the `Scribe`'s selected fields. **No additional public methods**.

Each element in the return value of `Eloquent.get(scribe)` is an `Entry` instance.

### Safe to carry on batch state (v3.4.1+)

You can hold `IEntry` as an instance variable on a `Database.Stateful` batch.

> ⚠️ **Through v3.4.0 this could throw `SerializationException` when crossing chunks**, because the `Schema.DescribeSObjectResult` held internally is not serializable. Worse, whether it fired depended on **cache warm-up** — only the first cache-miss instance for a type carried the value — so identical code passed or failed seemingly at random. v3.4.1 made the field `transient` and re-derives it on demand.

## MockEntry (Mock Extension)

The test-side implementation of `IEntry`. On top of the `IEntry` contract, it adds many extension APIs for building test data.

### Factories

| Signature | Purpose |
|---|---|
| `MockEntry.of(System.Type recordType)` | Create an entry for a normal SObject type (`MockEntry.of(Account.class)`) |
| `MockEntry.asAggregateResult()` | Create an entry for aggregate-query results (not bound to an SObject type) |
| `MockEntry.asAggregateResult(Map<String, Object> fieldToValue)` | Same as above, with initial values |

### Field Operations

| Signature | Return | Purpose |
|---|---|---|
| `set(String fieldName, Object value)` | `MockEntry` | Set a field value (non-writable fields are allowed) |
| `template(Map<String, Object> fieldToValue)` | `MockEntry` | Set multiple fields at once via a Map |

```apex
MockEntry accEntry = MockEntry.of(Account.class)
  .template(new Map<String, Object>{
    'Name' => 'Acme Co.',
    'Industry' => 'Technology'
  })
  .set('Active__c', true);
```

**Immediate detection of SObject field-name typos**: passing a **non-existent SObject field name** to `set` / `add` / `setParent` / `addParent` throws `ApexEloquentException` immediately.

```apex
MockEntry.of(Account.class).set('Naame', 'foo');
// → ApexEloquentException ("The field 'Naame' does not exist on the SObject.")
```

Previously, the typo would slip silently into `fieldToValue`; when the SUT later asked for the correct name (`get('Name')`), it received `null` and followed the null branch — passing the test while failing in production. False positives like this had a structural home. Now they are **caught the moment you write the test setup**, closing off setup-origin false positives by structure.

Note that `put` already runs field-name validation through standard `SObject.put`, so typos are caught there too — no behavior change for `put`.

> `set` has a synonym, `add(String, Object)`. They behave identically (both return a new `MockEntry`), so keep new code on `set`. `setParent` / `setChildren` likewise have `addParent` / `addChildren`.

### Baking in the SELECT contract: fetchedBy

| Signature | Return | Purpose |
|---|---|---|
| `fetchedBy(Scribe scribe)` | `MockEntry` | Bake that `Scribe`'s SELECT clause onto this entry as its contract |

Entries returned through `MockEloquent` **pick up the contract automatically** from the `Scribe` you passed to `get(scribe)`. That is the mechanism that throws when you touch a field you never selected.

**Entries handed straight to the SUT have no contract**, though. They never went through a `Scribe`, so there is no way for them to know which fields count as selected. `fetchedBy` attaches the contract after the fact.

```apex
MockEntry card = MockEntry.of(BusinessCard__c.class)
  .autoId(1)
  .set('CompanyName__c', 'Acme')
  .fetchedBy(RematchCompanyCardsHandler.scope());
```

Where this earns its keep is mainly **batches**. Records arriving at `execute(bc, scope)` never pass through `IEloquent`, so production is covered by the platform (they are real query results) while tests are **not checked at all** — you built those entries yourself. Pull the query construction into a `@TestVisible` method and the test can hand over exactly the same `Scribe` production uses.

See "Extract the Scribe and SELECT-omission detection works in tests too" in [API Reference: Scribe](/apex-stem/docs/apex-eloquent-api-scribe) for the full pattern.

> Production `Entry` needs no `fetchedBy`. A real SOQL row already throws from the platform the moment you touch an unqueried field. This is a strictness tool the mock side alone requires.

### Relations (building)

| Signature | Return | Purpose |
|---|---|---|
| `setParent(String parentIdFieldName, MockEntry parent)` | `MockEntry` | Hang a parent record under the entry |
| `setChildren(String name, List<MockEntry> children)` | `MockEntry` | Hang a child-record list under the entry |

For the first argument of `setChildren`, use the name you specified in `Scribe` via `relationName` if you did; otherwise use the **object name** (e.g. `'Opportunity'` — singular, no `__r`).

> ⚠️ **Keep the `Scribe` key and the `setChildren` key aligned.**
> `MockEntry.getChildren` checks against the child relation name the `Scribe` registered (the object name unless `relationName` was set). If they diverge, **an `ApexEloquentException` is thrown**. Production `Entry` resolves relationship names too, but **in mocks you must call with the key you declared on the `Scribe`**. It never quietly returns an empty list, so a mismatched key fails the test on the spot.

### Auto-Generated Id

| Signature | Return | Purpose |
|---|---|---|
| `autoId(Integer suffix)` | `MockEntry` | Auto-generate an 18-character Id (numeric suffix) |
| `autoId(String suffix)` | `MockEntry` | Auto-generate an 18-character Id (string suffix; supports placeholder expansion like `{#}`) |

### Bulk Generation (times)

| Signature | Return | Purpose |
|---|---|---|
| `times(Integer count)` | `List<MockEntry>` | Expand `count` records from the template. Replaces placeholders `{#}` `{A}` `{a}` with sequential values |
| `times(Integer count, Integer startAt)` | `List<MockEntry>` | Specify the start of the sequence |
| `times(Integer count, Integer startAt, Integer interval)` | `List<MockEntry>` | Specify the start and increment |

```apex
List<MockEntry> contacts = MockEntry.of(Contact.class)
  .autoId('{#}')
  .set('LastName', 'Contact-{#}')
  .alias('con_{#}')
  .times(3);  // con_1, con_2, con_3
```

**Nested multiplication is not supported**: you cannot expand "2 parents × 4 children each" hierarchically by having `MockEntry.of(Account.class).times(2)` contain `times(4)` on the child side. Build the child side by enumerating individual entries inside `setChildren`.

### alias (Retrieving Generated Ids)

| Signature | Return | Purpose |
|---|---|---|
| `alias(String name)` | `MockEntry` | Attach a name to this entry |
| `getByAlias(String name)` | `MockEntry` | Retrieve a MockEntry by alias via recursive search |
| `getAliasId(String name)` | `Id` | Retrieve the auto-generated Id of an alias (handy for assertions) |

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .alias('opp').autoId(1);
Id oppId = oppEntry.getAliasId('opp');

// In the test:
Opportunity updated = (Opportunity) mock.upsertedRecordsAt(MyUsecase.LBL_UPDATE)[0];
Assert.areEqual(oppId, updated.Id);
```

### Disabling Detection (Two Layers)

`MockEntry` has detection in **two independent layers**, each with its own escape hatch. Neither should be used as a rule — both safety nets exist to close off false positives.

| Signature | Return | What It Disables |
|---|---|---|
| `withoutFieldValidation()` | `MockEntry` | **Scribe FieldStructure** check (permits `get(...)` access to fields not SELECTed in `Scribe`) |
| `withoutSObjectFieldValidation()` | `MockEntry` | **SObject field-name** check (permits non-existent field names in `set` / `add` / `setParent` / `addParent`) |

```apex
// Example: a rare case where you want to store data under a name that isn't an SObject field
MockEntry.of(Account.class)
  .withoutSObjectFieldValidation()
  .set('Synthetic__c', 'value');  // not an Account field, but permitted
```

**These two are separate flags**. Relaxing the Scribe check via `withoutFieldValidation()` keeps the SObject typo detector intact (a clear `set('Naame', ...)` typo still throws). The reverse holds as well. Each escape hatch disables only its own responsibility — the other safety net stays on.

## Read Next

- [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access): the usage guide
- [Parent Fields, Child Subqueries, and Many-to-Many](/apex-stem/docs/apex-eloquent-relations): typical relation-operation examples
- [API Reference: IEloquent / Eloquent / MockEloquent](/apex-stem/docs/apex-eloquent-api-eloquent): the side that returns `IEntry`
- [API Reference: Scribe](/apex-stem/docs/apex-eloquent-api-scribe): the side that builds the source data for SELECT-omission detection
- [ApexEloquent Guide](/apex-stem/docs/apex-eloquent-guide): back to the guide index
