Skip to content

fix(odata): stop create-external-entities duplicating suffixed associations (formula1 §50) - #124

Merged
ako merged 1 commit into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h
Aug 9, 2026
Merged

fix(odata): stop create-external-entities duplicating suffixed associations (formula1 §50)#124
ako merged 1 commit into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h

Conversation

@ako

@ako ako commented Aug 9, 2026

Copy link
Copy Markdown
Owner

mxcli-formula1 §50, still-open item #1. One commit, rebased onto main after #123 merged.

The report

Re-running create external entities from added two associations every time and never stopped:

one generation      season  season_2  season_3        (correct: 6 in F1Cached)
+1 re-run           …       season_4  season_5
+2 re-runs          …       season_6  season_7

That project reached season_15 before anyone noticed — twelve spurious associations, committed. mx check clean, every test passing. The only symptom is duplicate links in Studio Pro's domain model, which nothing in the toolchain looks at.

Root cause, one layer below the report

The reported cause — "the dedup compares association names and the generator computes a fresh suffix before it looks" — is the visible half. It does not explain why the dedup has a nav-property index intended for exactly this case.

Association names are unique per module, so three entities each exposing a season navigation property give season, season_2, season_3. A suffixed association can never match its own nav property by name. The dedup knows that, and keys a second index on RemoteParentNavigationProperty — the field recording which OData nav property an association was generated from.

That index was always empty. The modelsdk reader never read the OData association source back. The write path sets the field (domainmodel_write.go:649), the gen type carries it, and the legacy parser reads it — only assocFromGen on the default engine dropped it. So the field survived one save and vanished on the next load, silently degrading the dedup to the name match that cannot work.

A field the write path sets and the read path drops is invisible to a grep for the identifier: it appears in three places and is missing from a fourth.

The fix

assocFromGen now reads Rest$ODataRemoteAssociationSource back — both nav properties, Navigability2, and the four Creatable/Updatable flags. The dedup index moves into indexExistingAssociations so it is directly testable.

Explicitly not fixed by stripping a trailing _<n> from association names. That heuristic also matches a user's genuine season_2, and the model already records the true origin — the correct key exists, it just was not being read.

Testing

mdl/backend/modelsdk/association_odata_source_test.go

  • Round-trip of an association deliberately named season_2 with RemoteParentNavigationProperty: "season" — the exact shape the re-import failed to recognise. Control (reverting the read) fails on all four fields.
  • A plain association must not acquire an OData source: the read has to branch on the stored type, not stamp every association.

mdl/executor/cmd_contract_reimport_test.go

  • The three-season domain model, asserting all three parents are recognised as already imported.
  • The assertion that matters: the two suffixed ones must match via the nav index and not the name index. Without it the first assertion could pass for the wrong reason.
  • Nav-absent fallback (legacy and Studio Pro-authored associations) and an unresolvable-parent guard.

Control: dropping the nav index fails with "DriverStandings.season is not recognised as already imported — a re-import will create a duplicate with a fresh suffix".

Not addressed here

Existing damage. A project that ran the import N times still carries 2N spurious associations, and they cannot simply be deleted — §50 found that external-entity access rules reference them, so removing one leaves CE1613 "The selected association no longer exists". That needs a repair path, which is its own change.

§50's other half — sub-element id churn (still-open #2). Reproduced exactly, on a 9.24 project:

run1 32759e5c9ff14f36  243 bytes
run2 2eaf9e097dbfb4b1  243 bytes
run3 b2231045a8b7f4e0  243 bytes
→ 16 bytes differ, one contiguous run at offset 150, right after `Type` — one UUID

Mechanism: assignID already no-ops on a non-empty id; the churn is that the write path builds fresh gen elements from the semantic model, so they arrive with empty ids and get minted. 162 call sites.

One correction to how §50 frames it, which matters for prioritisation. §50 files this as the cosmetic half, separate from "the one that is not cosmetic". That is not reliably true. 06a9face fixed this same shape for entity attributes, where a fresh id made Mendix's DB synchronizer read "attribute departed + new attribute added" and drop and re-add the column — 11 feeds and 98 articles blanked, mx check clean throughout. So sub-element id churn is cosmetic for some element types and data-destroying for others; the remaining types need judging individually rather than as one bucket.

The precedent for the fix is per-type identity reuse (name→id map from the existing document, reuse for retained elements), in the executor rather than as a global mechanism. Across ~630 ids in a domain model and ~858 in a database connection that is a multi-commit change where a mis-matched id is model corruption, so it is deliberately not started here.

go test ./mdl/... ./sdk/... green; go vet and gofmt clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4


Generated by Claude Code

…ations

mxcli-formula1 §50. Re-running `create external entities from` added two
associations every time, without bound: `season_2`, `season_3`, then
`season_4`, `season_5`, and so on. One project had reached `season_15`
before anyone noticed — `mx check` clean, every test passing, the only
symptom duplicate links in Studio Pro's domain model.

Association names are unique per module, so the second entity with a
`season` nav property is stored as `season_2`. The dedup looks for an
association matching the nav property it is about to import, and
`season_2` can never match `season` by name. The index that WOULD have
matched it is keyed on RemoteParentNavigationProperty — and the modelsdk
reader never read the OData association source back, so that index was
always empty on the default engine. The write path set the field and the
legacy reader read it; only the default read dropped it, so it survived
one save and vanished on the next load.

assocFromGen now reads Rest$ODataRemoteAssociationSource back
(nav properties, navigability, and the four capability flags). The dedup
index moves into indexExistingAssociations so it can be tested directly.

Not fixed by stripping a trailing _<n> from names: that would also match
a user's genuine `season_2`, and the model already records the true
origin. Existing damage is not cleaned up here — those associations are
referenced by external-entity access rules, so deleting one raises CE1613.
@ako
ako merged commit 38a1137 into main Aug 9, 2026
3 checks passed
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.

2 participants