Raw read-serving¶
The identity map intercepts Eloquent reads. A raw query-builder read — DB::table('users')->find(1) — bypasses Eloquent entirely, so by default it always issues SQL even when that exact row is already cached.
With raw read-serving enabled, the package answers raw single-key and key-set reads of the full row from the identity map, issuing zero SQL and returning stdClass rows byte-identical to a bypassed query. There is no fixed cap on a key set's size — a whereIn of any length is served as long as every key in it is covered by a current snapshot; the moment one key is uncovered the whole read falls through to SQL.
The feature is off by default and risk-managed: enabling it installs a per-driver connection resolver so DB::table() reads can be intercepted.
How it stays correct¶
Serving a raw row that differs from what SQL returns — even by a value's type or a missing column — would be a correctness bug. Two design choices prevent it:
-
The served row is a real database row, never a reconstruction. On a genuine full-column
SELECTthrough Eloquent, the store snapshots the row exactly as the driver returned it — native types, every column, original order. A raw read replays that snapshot verbatim. Rows created in-memory (create(), mass-writes) are never snapshotted: their attributes are cast PHP values and may omit database defaults, so they could not reproduce a raw row faithfully. -
A snapshot is served only while it is provably current. Any change to the cached row — an Eloquent save, a mass update, a raw write that flushes the class — invalidates the snapshot immediately. A read that isn't fully covered by a fresh snapshot falls through to SQL. The rule is absolute: a covered read returns exactly what SQL would, or it runs SQL.
What is served¶
Only the shapes that map cleanly to a cached row:
DB::table('users')->find(1)and->where('id', 1)->first()/->get()DB::table('users')->whereIn('id', [1, 2, 3])->get()(fully covered)
A key-set read is returned in primary-key-ascending order — a whereIn without an explicit ORDER BY has no defined SQL order, and ascending by key is a deterministic, portable ordering of the same set.
What falls through to SQL¶
Anything that can't be answered from a snapshot, exactly:
- Any non-key predicate (
->where('id', 1)->where('active', 1)) — the snapshot isn't consulted for filters. - A column projection (
->select('name')) — the snapshot is the whole row. - Joins, aggregates,
GROUP BY,DISTINCT,LIMIT/OFFSETthat could change the row set, unions, locks. - A key-set read where any key is uncovered (the whole read runs so no row is silently dropped).
- A key-set read with an explicit
ORDER BY— the snapshot only guarantees the primary-key-ascending order described above, so an ordered read runs SQL. - The write-invalidated window: the first read after a change re-snapshots.
Warming¶
A snapshot is captured when a genuine SELECT runs. Immediately after create() the row is already in the map, so the next Eloquent read is served from memory without a SELECT — and therefore without a native-row snapshot. In practice the map is flushed at request / job boundaries, so a later request's first Eloquent read of a row issues the SELECT that warms the snapshot; raw reads in that request are then served.
Observability¶
A served raw read records a ReturnRawRowFromMemory plan, visible through IdentityMap::explain() and the streaming decision log:
$explanations = IdentityMap::explain(function () {
DB::table('users')->find(1);
});
// => Explanation { type: ReturnRawRowFromMemory, sqlExecuted: false, ... }
Enabling at runtime¶
Enable the feature before the connection is first resolved — via env in a real app. Toggling it after a connection is live requires a reconnect (DB::purge()), because the swapped connection class is chosen at connection build time.