feat: multi-class FK cascade reach through union / polymorphic-base targets (#262) - #290
Conversation
- Add CascadeUnionMemberA/B + CascadeUnionOwner fixtures (union-typed FK) - Assert edge lists both candidates and single-target edge keeps candidates=None - RED: union crashes on UnionType.__name__; CascadeEdge has no candidates field Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _unwrap_relational_target returns an accumulating, deduped candidate list - Thread models through _static_walk_fk_edges/_resolve_target_cls - Add CascadeEdge.candidates (None default keeps single-target JSON byte-identical) - Set target=candidates[0]; candidates=list only when >1 - validate_cascade_ttl_targets loops every candidate (Rule 3: fix broken _static_walk_fk_edges caller in test_cascade_sf_held_ref_plan.py) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add CascadePolyBase + CascadePolySub1/2 + CascadePolyOwner/DedupOwner fixtures - Assert base+subclasses enumerated (len 3), declaration order, dedup, degradation - RED: Task-1 leaf enumerates verbatim only, so subclasses are not yet collected Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…edges
- _expand_candidates returns {T} union registered subclasses over threaded models
- Base included iff registered (Decision #3); safe_issubclass(T,T) drives it
- Order-preserving, deduped across union members; degrades to single target
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on + A2 guard - Golden plan JSON/hash regression (0bc1f0e973ecfcf4, no candidates key) - Validator fails fast on a non-first candidate lacking Meta.ttl (model_name) - Guard: no pre-existing single-target model is silently expanded - Add candidates= param to _edge/_entry/_plan hand-build helpers Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…smatched_class counter
- library.lua: multi-class push_child branch gated on edge.candidates; first-colon
{class}:{pk} prefix split via string.match; exact candidate membership; nil prefix
is an uncounted dead-end; non-candidate/absent-from-plan increments a new
cascade_apply-scope mismatched_class local; 3-element return tuple (D-01/D-03)
- result.py: CascadeResult.mismatched_class: int = 0 (defaulted, forward-compat)
- base.py: real-Redis results[-1] unpacks 3 elements; fakeredis fast-path sets
mismatched_class=0 explicitly
- tests: new tracer test_cascade_multi_class_apply.py (scalar-union re-arm via public API)
- cascade_types.py: register CascadeUnion* fixtures in ALL_CASCADE_MODELS so the
integration plan bakes their edges (Rule 3 blocking fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the 3-element return - unit: test_aset_ttl_cascade_flag mock pipe results carry a third element (0) and pin mismatched_class in every CascadeResult assertion - unit: test_cascade_sf_held_ref_fakeredis_fallback + test_cascade_action_boundary pin mismatched_class=0 so the fakeredis no-op divergence asserts zero drift - integration (Rule 3 lockstep): test_cascade_sf_held_ref_apply + test_cascade_depth_and_gate compared the raw fcall result to 2-element list literals; extended to 3 elements (mismatched_class=0) for the now-3-tuple return - grep audit: no remaining 2-element unpack/compare of an fcall result; golden single-target plan hash 0bc1f0e973ecfcf4 unchanged; full unit cascade suite green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…icipants
- Add CascadeKeyInitialsError(RapyerError) carrying model_name, mirroring CascadeTargetTtlMissingError
- Export it from rapyer.errors (import block + __all__)
- Add validate_cascade_key_initials(models) beside validate_cascade_ttl_targets: builds the participant set (roots + edge targets/candidates), asserts class_key_initials() == __name__, no Redis I/O
- Wire the guard into init_rapyer() right after validate_cascade_ttl_targets, pre-freeze
- D-04 docstrings on _unwrap_relational_target and CascadeEdge.candidates: class identity resolved from {class}:{pk} prefix; class_key_initials() must equal __name__
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cipant scope - Conforming participant plan raises nothing - Override on a NON-root reached candidate raises CascadeKeyInitialsError (D-02 participant-scope) - Override on a cascade root also raises - Assert model_name equals offending __name__ and message names the overridden initials - All local fixtures use init_with_rapyer=False; guard called on LOCAL model lists, never REDIS_MODELS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-pk fixtures
- CascadeUnionListOwner/DictOwner/SetOwner/PQOwner extend the union target
(CascadeUnionMemberA | CascadeUnionMemberB) across list/dict/RedisSet/PQ shapes
- CascadeMultiClassDiamond{Leaf,MemberA,MemberB,Root}: two candidate classes FK a
single shared leaf so a cascade reaches it via two candidate-class paths (CMCT-08)
- CascadeColonPkMember (Key[str] pk) + CascadeColonPkOwner lock the first-colon
{class}:{pk} split against a colon-bearing pk (Pitfall 2)
- every new fixture is ttl-bearing and appended to ALL_CASCADE_MODELS
- list/dict/RedisSet/PriorityQueue union owners each enumerate BOTH members as candidates (set-equality, order-independent — CMCT-07 ordering) - mixed-class-diamond root lists both member classes; both members FK the same shared leaf class (CMCT-08 support) - colon-pk union owner enumerates both the colon-pk and plain candidate
- both union candidates resolve/re-arm from the same scalar edge (CMCT-04 adjacency) - list & dict union owners re-arm every referenced member class (CMCT-05/07) - RedisSet & RedisPriorityQueue union owners re-arm both member classes (CMCT-07) - empty collection/set/pq union owners reach no child and never crash (CMCT-07 empty) - colon-bearing Key[str] pk resolves via first-colon split and re-arms (Pitfall 2)
… + dead-ends - mixed-class diamond: shared leaf re-armed via both candidate-class paths, no crash (CMCT-08) - add CascadeUnionDepthRoot fixture (union entry over CascadeChainNode | CascadeUnionMemberB, depth=1) - depth budget truncates a chain entered THROUGH a resolved-class union edge (CMCT-08 depth clause, direct) - non-candidate reach: skipped, no TTL applied, mismatched_class tallied (D-03, CMCT-10) - corrupt no-colon reach: silent dead-end, NOT tallied (CMCT-10)
- Add tests/unit/cascade/test_cascade_multi_class_fakeredis_fallback.py - Scalar union owner: own main key re-arms, reached member NOT re-armed - SF-held union set owner: own container key re-arms, reached member NOT re-armed - Both assert zero-drift CascadeResult(0, 0, mismatched_class=0) fast path - Local fixture wires union models onto fakeredis without polluting the byte-identity-guarded CASCADE_PLANNER_MODELS list
- Add 'Multi-Class FK Targets (Union / Polymorphic Base)' subsection under
Cascade-Eligible Shapes
- ONE runnable example covering BOTH a union Reference[A|B] FK and a
polymorphic-base FK with registered subclasses, each re-arming its reached
child to that child's own Meta.ttl
- Admonition records the {class}:{pk} prefix identity rule: class_key_initials()
must equal __name__ (enforced by CascadeKeyInitialsError) and class-name
uniqueness (enforced by DuplicateModelNameError) -- folds in the #262 concern
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughCascade TTL planning and execution now support union and polymorphic foreign-key targets across scalar, collection, and special-field shapes. Redis key prefixes select concrete classes, per-class TTLs are applied, and class mismatches are reported. ChangesMulti-class cascade support
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Client
participant aset_ttl
participant CascadeFunction
participant Redis
Client->>aset_ttl: Call aset_ttl(cascade=True)
aset_ttl->>CascadeFunction: Execute serialized cascade plan
CascadeFunction->>Redis: Resolve key prefix and refresh concrete target TTL
CascadeFunction-->>aset_ttl: Return dangling and mismatch counters
aset_ttl-->>Client: Return CascadeResult
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
tests/unit/cascade/test_aset_ttl_cascade_flag.py (1)
91-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the stale "two-element result" comment.
The mock now returns three elements and the assertion checks
mismatched_class. The comment still describes a two-element result.📝 Proposed comment fix
# Standalone call (no outer pipeline): enqueues run_sha, awaits - # pipe.execute() itself, and decodes the two-element result. + # pipe.execute() itself, and decodes the three-element result.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/cascade/test_aset_ttl_cascade_flag.py` around lines 91 - 94, Update the comment above the standalone pipeline mock in fcall_pipeline_spy to describe decoding a three-element result, including the mismatched_class value asserted by the test; leave the mock behavior and assertions unchanged.tests/integration/foreign_keys/test_cascade_multi_class_apply.py (1)
328-381: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding a real-Redis polymorphic-target case.
The suite covers union targets end-to-end. Polymorphic base targets are covered only at plan level. A subclass-key reach through a
Reference[CascadePolyBase]edge exercises a different candidate-enumeration path. Adding one case would close that gap.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/foreign_keys/test_cascade_multi_class_apply.py` around lines 328 - 381, The integration suite needs a real-Redis cascade test for polymorphic base references, not just union targets and plan-level coverage. Add an async test alongside the existing cascade reach tests that creates a concrete subclass target referenced through CascadePolyBase, persists the relevant keys, runs apply_cascade, and verifies the subclass candidate is handled with the expected TTL behavior and result counters. Reuse the existing polymorphic model and helper symbols from the test module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/documentation/special-fields/ttl-cascade.md`:
- Line 154: Update the code block beginning at the ``` marker in ttl-cascade.md
to use the repository-required indented Markdown style, unless the project
configuration explicitly intends fenced blocks; otherwise adjust the relevant
Markdown lint configuration consistently.
- Around line 144-149: Update the TTL cascade documentation’s return description
and fakeredis example to include all CascadeResult fields, including
mismatched_class, and use the complete constructor shape. Document that
mismatched_class counts both valid model classes outside the edge candidate set
and colon-bearing class prefixes absent from CASCADE_PLAN, while colon-free
corrupt keys remain uncounted.
---
Nitpick comments:
In `@tests/integration/foreign_keys/test_cascade_multi_class_apply.py`:
- Around line 328-381: The integration suite needs a real-Redis cascade test for
polymorphic base references, not just union targets and plan-level coverage. Add
an async test alongside the existing cascade reach tests that creates a concrete
subclass target referenced through CascadePolyBase, persists the relevant keys,
runs apply_cascade, and verifies the subclass candidate is handled with the
expected TTL behavior and result counters. Reuse the existing polymorphic model
and helper symbols from the test module.
In `@tests/unit/cascade/test_aset_ttl_cascade_flag.py`:
- Around line 91-94: Update the comment above the standalone pipeline mock in
fcall_pipeline_spy to describe decoding a three-element result, including the
mismatched_class value asserted by the test; leave the mock behavior and
assertions unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4ca81bc2-e438-46dd-ba9d-6c5d11b9bef3
📒 Files selected for processing (21)
docs/documentation/special-fields/ttl-cascade.mdrapyer/base.pyrapyer/cascade/planner.pyrapyer/errors/__init__.pyrapyer/errors/cascade.pyrapyer/init.pyrapyer/result.pyrapyer/scripts/lua/cascade/library.luatests/integration/foreign_keys/test_cascade_depth_and_gate.pytests/integration/foreign_keys/test_cascade_multi_class_apply.pytests/integration/foreign_keys/test_cascade_sf_held_ref_apply.pytests/models/cascade_types.pytests/unit/cascade/test_aset_ttl_cascade_flag.pytests/unit/cascade/test_cascade_action_boundary.pytests/unit/cascade/test_cascade_key_initials_guard.pytests/unit/cascade/test_cascade_multi_candidate_plan.pytests/unit/cascade/test_cascade_multi_class_fakeredis_fallback.pytests/unit/cascade/test_cascade_plan_injection.pytests/unit/cascade/test_cascade_sf_held_ref_fakeredis_fallback.pytests/unit/cascade/test_cascade_sf_held_ref_plan.pytests/unit/cascade/test_cascade_ttl_required_validation.py
| In both cases the edge carries **all** candidate classes, and the cascade resolves | ||
| each reached child's *actual* class at traversal time from its stored key's | ||
| `{class}:{pk}` prefix, then re-arms that child to **its own** resolved class's | ||
| `Meta.ttl` — exactly as with a single-target FK. A child whose class is a valid model | ||
| but is *not* among the edge's candidates is skipped (never re-armed) and counted as | ||
| class drift in `CascadeResult.mismatched_class`. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Document the complete mismatched_class contract.
This section introduces mismatched_class, but the later return section still lists only dangling_children and dangling_special. The fakeredis example also uses CascadeResult(0, 0). The runtime counts a colon-bearing prefix that is absent from CASCADE_PLAN, not only a valid model outside the candidate set. Update the return description and examples to list all fields and document both mismatch cases. Keep colon-free corrupt keys uncounted.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/documentation/special-fields/ttl-cascade.md` around lines 144 - 149,
Update the TTL cascade documentation’s return description and fakeredis example
to include all CascadeResult fields, including mismatched_class, and use the
complete constructor shape. Document that mismatched_class counts both valid
model classes outside the edge candidate set and colon-bearing class prefixes
absent from CASCADE_PLAN, while colon-free corrupt keys remain uncounted.
| The single runnable example below shows **both** shapes — a union FK and a | ||
| polymorphic-base FK — each re-arming its reached child to that child's own `Meta.ttl`: | ||
|
|
||
| ```python |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the Markdown code-block style.
markdownlint-cli2 reports MD046 at Line 154. Convert this example to the repository-required indented style, or update the Markdown lint configuration if fenced blocks are intentional here.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 154-154: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/documentation/special-fields/ttl-cascade.md` at line 154, Update the
code block beginning at the ``` marker in ttl-cascade.md to use the
repository-required indented Markdown style, unless the project configuration
explicitly intends fenced blocks; otherwise adjust the relevant Markdown lint
configuration consistently.
Source: Linters/SAST tools
…ationale as one-line # comments Docstrings state only what a function does; implementation and decision rationale moved to single-line # comments over the block they explain. - _unwrap_relational_target / _expand_candidates: drop the D-04 and Decision-#3 essays, keep a one-line summary each - CascadeEdge.candidates, CascadeResult.mismatched_class: one-line intent - validate_cascade_key_initials, CascadeKeyInitialsError: black-box docstring - library.lua push_child: 40 lines of prose down to three # lines Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…; fix import order Test comments now sit over the block they explain and stay one line; descriptive test names carry the rest. Also drops plan-task and requirement-ID tags from section headers. - test_cascade_multi_class_apply.py: 12 comment paragraphs condensed - multi-candidate plan / key-initials / fakeredis-fallback modules: one-line module docstrings, per-block notes - cascade_types.py fixtures: one-line docstrings, matching the file's style - ruff I001 on test_cascade_multi_candidate_plan.py (pre-existing, never caught because CI lint never ran) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage reportTotal coverage: 99% Full report |
| model_cls: Any, | ||
| parent_path: str, | ||
| fks: list[CascadeEdge], | ||
| models: list, |
There was a problem hiding this comment.
list of what? improve annotation
…TL-cascade page section Multi-class FK reach closes a gap in the original cascade rather than adding a user-facing capability, so it does not warrant its own documentation section. Reverts the page to its develop state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eased entry since v1.3.4 Only #288 had an entry; five merged items were missing. - [1.3.6] new: SF-held reference cascade (#289, Added) and multi-class union / polymorphic-base FK reach (#290, Fixed) - [1.3.5] backfilled: the configurable TTL cascade feature itself (#283), the init_rapyer client-rebind fix (#276), runtime CPU optimizations (#263) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… on planner model-list annotations PR #290 review: bare `list` did not say list of what. Use the `list[type["AtomicRedisModel"]]` form already used by build_cascade_plan for every model-list parameter, return, and accumulator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Extends the TTL cascade so a single FK edge referencing a
Reference[A | B]union or a polymorphic base class resolves to multiple candidate target classes._unwrap_relational_targetnow enumerates every candidate (union members + registered subclasses);CascadeEdgecarries acandidateslist alongsidetarget; single-target plan JSON stays byte-identical (golden hash unchanged);validate_cascade_ttl_targetsfails fast for every candidate.push_childresolves each reached key's actual class from its{class}:{pk}first-colon prefix, exact-matches it against the edge's candidates, and re-arms each reached child to its own class'sMeta.ttl/ special-suffix keys / outgoing edges. Non-candidate reaches are tallied via a new third FCALL return elementmismatched_class; corrupt/no-colon reaches are silent, uncounted dead-ends. Adds an init-timeCascadeKeyInitialsErrorguard (a cascade participant'sclass_key_initials()must equal__name__).## [1.3.6]→ Fixed. The page section that originally accompanied this PR was dropped — this closes a gap in the existing cascade rather than adding a user-facing capability. The same commit backfills every other unreleased changelog entry since v1.3.4 (Milestone: Configurable TTL Cascade #283, Fix init_rapyer connection rebinding order #276, Runtime CPU optimizations: lazy _pk + cheaper __setattr__ type checks #263, v1.3.6: Cascade reach through special-field references (RedisSet/RedisPriorityQueue of ForeignKey) #289), which had been missing.Single-target cascade behavior is preserved byte-for-byte; the per-child cascading-TTL-refresh apply layer is reused unchanged.
Testing
0bc1f0e973ecfcf4unchanged.ruff checkclean (also fixes a pre-existingI001that CI never caught, because the earlier lint job died on a GitHub Actions outage before running).Closes #262
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation