Skip to content

fix: preserve cross-module associations on CREATE object actions#502

Merged
ako merged 1 commit intomendixlabs:mainfrom
hjotha:submit/create-object-cross-module-association
May 4, 2026
Merged

fix: preserve cross-module associations on CREATE object actions#502
ako merged 1 commit intomendixlabs:mainfrom
hjotha:submit/create-object-cross-module-association

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented May 4, 2026

Part of #352.

Summary

  • A create TargetMod.Entity (OwnerMod.Assoc = \$Ref) statement round-tripped as an attribute assignment when the association lived in a different module than the create target.
  • Describer side: formatAction on CreateObjectAction stripped every association member down to its bare name, losing the cross-module qualifier.
  • Builder side: resolveMemberChange queried the create target's module for the bare name, didn't find the association, and fell through to AttributeQualifiedName — triggering Studio Pro CE1613 ("selected attribute no longer exists") on re-open.

Fix

  1. Describer keeps the Module. prefix on any association whose owning module differs from the create target's module.
  2. Builder uses the authored module (when the member name is qualified) to look up the association, matching how the describer expresses it.

Test plan

  • New unit test TestResolveMemberChange_CrossModuleAssociationKeepsAssociationSlot verifies the association lands on AssociationQualifiedName (not the attribute slot) with synthetic module names.
  • go build -tags integration ./... builds clean.
  • go test ./mdl/executor/ -count=1 passes.
  • Validated in Mendix Studio Pro with `mx check` on a local project where a `CREATE` assigns a cross-module association — CE1613 no longer reported.

🤖 Generated with Claude Code

A `create TargetMod.Entity (OwnerMod.Assoc = $Ref)` statement
round-tripped as a plain attribute assignment when the association
lived in a module other than the create target. Two sides of the
same bug:

1. **Describer** (`formatAction` on `CreateObjectAction`) stripped
   every association member down to its bare name
   (`Module.Assoc` → `Assoc`), so the authored cross-module
   qualifier was lost.

2. **Builder** (`resolveMemberChange`) then queried the create
   target's module for the bare name. The association was not
   defined there, so the lookup fell through to the attribute
   slot and Studio Pro reopened the MPR with CE1613 "selected
   attribute no longer exists".

Fixes:

- Describer now keeps the `Module.` prefix on any association
  whose owning module differs from the create target's module.
- Builder now uses the authored module (when the member name is
  qualified) to look up the association, matching how the describer
  expresses it.

Added builder unit test
\`TestResolveMemberChange_CrossModuleAssociationKeepsAssociationSlot\`
for the synthetic cross-module pattern.

Part of mendixlabs#352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

AI Code Review

What Looks Good

  • The PR correctly identifies and fixes the root cause of cross-module association handling in CREATE object actions on both the describer (formatAction) and builder (resolveMemberChange) sides.
  • The new unit test TestResolveMemberChange_CrossModuleAssociationKeepsAssociationSlot effectively verifies the fix using synthetic module names and mock backend, ensuring the association stays in the AssociationQualifiedName field without leaking to the attribute slot.
  • Changes are minimal and focused: only the two relevant executor functions are modified, with clear explanatory comments.
  • The fix maintains backward compatibility by preserving existing behavior for same-module associations while correctly handling cross-module cases.
  • Test plan shows integration build passes and executor tests pass, with manual validation pending (as noted).

Minor Issues

  • The test comment mentions testing both CREATE and CHANGE actions, but the test only verifies a CREATE scenario. While the resolver logic is shared, adding a CHANGE counterpart would strengthen coverage (though not critical since the fix is in the shared resolver).
  • In cmd_microflows_format_action.go, the variable entityModule is declared but only used in the association branch; minor style improvement would be to declare it inside the association conditional block to limit scope.

Recommendance

Approve. The PR resolves the reported issue with appropriate test coverage and minimal, well-documented changes. The fix aligns with the project's architecture and maintains full-stack consistency for the existing MDL feature. The minor issues do not block merging.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@ako
Copy link
Copy Markdown
Collaborator

ako commented May 4, 2026

Review: fix: preserve cross-module associations on CREATE object actions

Verdict: Approve with minor note

What the PR does

Fixes a describe → exec → describe roundtrip breakage for cross-module associations on CREATE/CHANGE actions (part of #352):

Describer side (formatAction): Previously all association names were unconditionally stripped to bare names (OwnerMod.Assoc → Assoc). The fix compares the association's module against the create target's module and keeps the prefix only when they differ.

Builder side (resolveMemberChange): Previously the domain model lookup always used the create target's module (moduleName), so a qualified name like OwnerMod.Assoc looked in the wrong module and fell through to AttributeQualifiedName, triggering Studio Pro CE1613. The fix extracts lookupModule from the qualifier when the member name is dotted.

No blockers

Minor: formatter change is untested

cmd_microflows_create_cross_module_assoc_test.go covers the builder path correctly. The formatter change in formatAction — which decides whether to emit Module.Assoc or bare Assoc — has no new test. The existing TestFormatAction_CreateObject_WithAssociationMember only covers the same-module case ("MyModule.Order_Customer" on "MyModule.Order"), which still strips correctly after the fix.

A test for the cross-module path would be:

action := &microflows.CreateObjectAction{
    EntityQualifiedName: "TargetMod.Entity",
    OutputVariable:      "NewObj",
    InitialMembers: []*microflows.MemberChange{
        {AssociationQualifiedName: "OwnerMod.Entity_Other", Value: "$Ref"},
    },
}
want := "$NewObj = create TargetMod.Entity (OwnerMod.Entity_Other = $Ref);"

Not a blocker — the builder test catches the end-to-end regression, and the formatter logic is straightforward — but worth adding for roundtrip completeness (Recurring Finding #4).

Positive notes

  • strings.SplitN(..., 2) is the right choice over strings.Split — Mendix qualified names are always Module.Name, and N=2 avoids surprises with any hypothetical extra dots
  • The lookupModule variable makes the intent clear: prefer the authored module qualifier over the entity's module for association lookups
  • Same-module associations are unaffected — the existing test TestFormatAction_CreateObject_WithAssociationMember confirms the prefix is still stripped when assocModule == entityModule
  • Clean PR, no bundled unrelated changes

@ako ako merged commit dc7ef0d into mendixlabs:main May 4, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants