fix: reject non-Ref foreign key cycles at metamodel generation time - #440
Merged
Conversation
Two entities referencing each other with non-Ref @fk fields, or an entity referencing itself, generated metamodels whose constructors build each other's record-typed children eagerly: the first metamodel use died at class initialization with an ExceptionInInitializerError wrapping a StackOverflowError. Both processors now run a memoized depth-first check over non-Ref record fields before generation and report an error naming the cycle and the fix, mirroring the rule the engine states at template level for self-references: a foreign key cycle must cross a Ref boundary to be loadable. A cycle of inline records gets its own diagnostic, since an inline record embeds its columns and a cycle cannot be modeled at all. Each cycle is reported once, not doubled by the nullable-chain variants. Covered by mirrored tests in both processor suites: the mutual cycle and the self-reference are rejected with the cycle named, and a cycle that crosses a Ref boundary still generates as before.
A three-entity chain whose cycle closes through a Ref generates as usual in both processors, including the reference metamodel at the boundary and the navigation metamodels beyond it: the cycle check skips Ref edges, so only cycles that cannot be loaded are rejected.
… cycle The record graph validation named the cycle but not the remedy, while the self-reference check in TemplatePreparation and the processor diagnostics both state that a foreign key cycle must cross a Ref boundary. The cycle message now matches the processors: it renders the cycle members from the type the cycle re-enters and names the fix, with the inline-record variant for cycles of non-Data records.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #412.
Problem
Two entities referencing each other with non-Ref
@FKfields — or an entity referencing itself — compiled with no diagnostic, but the generated metamodels could never be used. Their constructors build record-typed children eagerly, so the metamodels of a cycle construct each other until the stack overflows: the first metamodel use after a clean build died with anExceptionInInitializerErrorwrapping aStackOverflowError. The existing guards in the processors only prevented generating a file twice; the generated code itself still recursed. Verified with a reproduction test against the unmodified processor before applying the fix: the mutualOwner/Petpair compiled cleanly and constructing the generated metamodel overflowed the stack.Fix
Both processors (APT and KSP) run a memoized depth-first check over non-Ref record fields before generation and reject a cycle with an error naming its members and the fix:
This mirrors the rule the engine already states at template level for self-references (
TemplatePreparation): a foreign key cycle must cross aRefboundary to be loadable. A cycle of inline records gets its own diagnostic, since an inline record embeds its columns and a cycle cannot be modeled at all. The error is attached to the field that closes the cycle and each cycle is reported once, not doubled by the nullable-chain variants.The runtime record validation (
RecordValidation.validateRecordGraph), which catches such a cycle at first model use when the processors are not attached, reports the same message: it renders the cycle members from the type the cycle re-enters and names theReffix, instead of the bare "Cyclic dependency detected". Its traversal already skippedRefedges, so enforcement is unchanged; only the message is aligned.Tests
Mirrored across both processor suites:
@FKcycle is rejected, with the cycle named and (Java side) the diagnostic asserted to print exactly once,@FKis rejected (Employee -> Employee),Refboundary still generates as before,Refat depth generates as before, including the reference metamodel at the boundary and the navigation metamodels beyond it.Full 25-module reactor build is green with the check active and produces zero cycle diagnostics, so no existing model is affected. The storm-core validation and metamodel test classes (155 tests) are green after the runtime message alignment.