Repository navigation
fix(model): clear the whole per-request query cache on any ORM write and on rollback - #4437
Conversation
…and on rollback The per-request finder cache (cacheQueriesDuringRequest, on by default) stored each finder's result in the base model's slot keyed by its SQL, but an ORM write cleared only the writing model's slot. A query that joined another model through include= lives in the BASE model's slot, so a write to the included model left it returning the joined model's pre-write columns for the rest of the request (#4429). Reads inside a transaction that then rolled back had the same shape: the rolled-back rows stayed cached as phantoms. - $clearRequestCache() now clears the entire request.wheels["$queryCache"], not just the calling model's slot. Every ORM write path already funnels through it (create / update / delete, the *All and *ByKey forms, the #4397 soft-delete UPDATE via $runDeleteStatement), so none can forget the cross-model case and there is no new merge race. - $runInTransaction now clears the request cache when it resolves a rollback — both the thrown-body path and the returned-false / mode=rollback path. A commit does not need it; its reads committed. - forgetCachedQueries (#4364) is unchanged; its docs now point at all=true after a raw write to a table that other models join through include=. The core runner forces cacheQueriesDuringRequest OFF, so neither staleness was suite-visible. A new tests/specs/requestcache group turns the cache on per spec (restored in afterEach, with the mutated rows restored too) and pins the same-model clear, the cross-model include= clear (RED on develop), the rollback phantom case (RED on develop), and that a read with no write between is still served from cache. The guides' caching page documents the per-request cache. Closes #4429 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
Review: head ee04e34. One MEDIUM to fix; the rest is APPROVE. What's right
MEDIUM: a savepoint rollback doesn't clear the cache
LOW
The PR needs a full matrix (shared plumbing), which orch1 is holding until Actions recovers. PR CI at review time: 28 pending, 2 skipping. The final follows the MEDIUM fix, the matrix, and PR CI. |
Addresses rev1-r1's review of #4437: a transaction="savepoint" rollback goes through $runSavepoint, not $runInTransaction, so a savepoint unit's reads stayed cached as phantoms after the savepoint rolled back while the outer transaction carried on and committed. - $runSavepoint now clears the request cache after both rollbackToSavepoint calls (the exception path and the returned-false path). - New specs: a savepoint rollback clears its cached reads (RED on the prior head), and a rollback through the exception/throw path of $runInTransaction clears too. - The guides note that a raw transaction{} block rollback needs forgetCachedQueries(all=true), since the automatic clear only fires for ORM transactions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
Re-review: head dac3232. The MEDIUM and both LOWs are resolved; code APPROVE.
This is shared plumbing, so the final needs a full compat matrix on this head (orch1 is holding dispatches until Actions recovers) and green PR CI. |
Wheels Test Results35 785 tests 35 060 ✅ 11m 12s ⏱️ Results for commit 4f08511. ♻️ This comment has been updated with latest results. |
|
Matrix 37379702292 (head dac3232): 30/32 legs green. One MUST-FIX in the new spec.
MUST-FIX: the spec's raw-write helper binds a CockroachDB id as a 32-bit integer (adobe2023-cockroachdb and adobe2025-cockroachdb).
PR CI has one failure, "Visual regression". It's the After the bigint fix, a narrowed re-run (Adobe 2023/2025 × CockroachDB, plus a SQLite compile check) on the new head should be enough, read together with this full run. |
…ot integer Addresses the #4437 matrix review: $rawPostTitle bound the post id as cf_sql_integer, so Adobe 2023/2025 x CockroachDB errored on 64-bit ids ("Invalid data ... for CF_SQL_INTEGER"). Bind cf_sql_bigint and take the id as an untyped argument so a 64-bit value is not truncated through a numeric cast. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…-cache-invalidation
|
FINAL: APPROVE. Head d8861ac.
|
…-cache-invalidation Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # web/sites/guides/src/content/docs/v4-2-0/digging-deeper/caching.mdx
|
FINAL (re-confirmed after the develop conflict): APPROVE. Head 4f08511.
|
The bug
With
cacheQueriesDuringRequeston (the default), a finder's result is cached in the base model's slot keyed by its SQL. An ORM write cleared only the writing model's slot, so a query that joins another model throughinclude=— cached under the base model — kept returning the joined model's pre-write columns for the rest of the request (#4429). Reads made inside a transaction that then rolled back had the same shape: the rolled-back rows stayed cached as phantoms.Fix (Approach A, approved by rev1-r1 + orch1)
$clearRequestCache()now clears the entirerequest.wheels["$queryCache"], not just the calling model's slot. Every ORM write path already funnels through it —create/update/delete/ the*Alland*ByKeyforms / the fix(model): keep soft deletes reversible and make softDelete=false a real delete #4397 soft-delete UPDATE via$runDeleteStatement— so none can forget the cross-model case, and there's no new merge race. Whole-cache-on-write is the correct semantics for a request-scoped cache: a write ends the read-consistency window, and re-running a read afterward is both correct and cheap. (B — per-model/table tagging — was rejected: its dependency set can't be made complete for nested includes or rawwhere=cross-table refs without fragile SQL parsing, and a gap silently reintroduces the bug.)$runInTransactionclears the request cache when it resolves a rollback — both the thrown-body path and the returned-false /transaction="rollback"path. A commit doesn't need it; its reads committed.forgetCachedQueries(feat(model): add public forgetCachedQueries() to clear the per-request finder cache #4364) is unchanged. Its docs now point atall=trueafter a raw write to a table other models join throughinclude=; the named form stays for single-table raw writes.Tests
The core runner forces
cacheQueriesDuringRequestoff (tests/runner.cfm), so neither staleness was ever suite-visible. A newtests/specs/requestcache/group turns the cache on per spec (restored inafterEach, which also restores the one post + author the specs mutate, so a failing spec can't leak state) and pins:include=write clears the base model's cached query — RED on develop;Two existing specs that asserted the old per-model-clear behaviour (
requestQueryCacheSpec,requestQueryCacheTenantCollisionSpec— one literally "clears only its own model's cache") are updated to the new clear-all behaviour.Verification
tests/specs/requestcache/: RED on develop for the two cross-cutting cases, GREEN with the fix.pnpm build): 852 pages, Complete — the caching guide now documents the per-request cache.Closes #4429
🤖 Generated with Claude Code