Skip to content

fix: preserve stored widget property metadata on ALTER PAGE REPLACE (CE0463) - #797

Open
ront85 wants to merge 1 commit into
mendixlabs:mainfrom
ront85:fix/112-replace-widget-ce0463
Open

fix: preserve stored widget property metadata on ALTER PAGE REPLACE (CE0463)#797
ront85 wants to merge 1 commit into
mendixlabs:mainfrom
ront85:fix/112-replace-widget-ce0463

Conversation

@ront85

@ront85 ront85 commented Jul 28, 2026

Copy link
Copy Markdown

Problem

Any ALTER PAGE … REPLACE of a pluggable widget produces CE0463 ("The definition of this widget has changed…") on the rebuilt widget — even an identity rebuild that changes nothing:

alter page DOC."SalesDocumentType_Select" {
  replace "comboBox1" with {
    combobox "comboBox1" (
      Attribute: "DOC.DocumentTypeSelector_Partner",
      DataSource: database from MasterData."Partner",
      CaptionAttribute: "Name"
    )
  }
}

mxcli reports success and mxcli check --references passes; only a real MxBuild check catches it. This blocks the whole class of "adjust an existing pluggable widget" edits (e.g. adding an XPath constraint to a combobox datasource). Reproduced identically on v0.13.0 and v0.16.0 against a Mendix 11.12.2 MPRv2 project with Combobox v2.8.1.

Root cause

The rebuild emits CustomWidgets$WidgetPropertyType metadata generated from the embedded template + installed .mpk, while untouched sibling widgets in the same page unit carry metadata from whichever (older) widget version authored them in Studio Pro. A byte-level diff of the page unit before/after an identity rebuild showed exactly two divergences, both per-property display metadata:

Property Stored by Studio Pro Emitted by mxcli
filterInputDebounceIntervalCategory Advanced::Filter Events
onChangeEventCaption On selection On change

A rebuilt widget and its neighbours end up described by two different widget versions in one file, and MxBuild rejects the mix.

Note on #112 (closed): its stated root cause — the template having more PropertyTypes than WidgetProperties — is not the defect. That asymmetry is intentional (system properties Label/Visibility/Editability deliberately have no value entry, per the comment in modelsdk/widgets/generate.go), and 13 other shipped templates share it while working fine. The actual defect is the metadata vintage mismatch above.

Fix

Mutator.ReplaceWidget now grafts the stored (old) widget's per-property display metadata — Caption, Category, Description — onto the replacement's freshly generated Type block, so the rebuilt widget stays consistent with the stored model rather than with the toolchain's template:

  • Matched by slash-joined PropertyKey path (/markers/latitude), not flat key, so distinct nested object types that reuse a key keep independent metadata.
  • Applied only when the replacement's WidgetId matches the replaced widget's — including pluggable widgets nested inside container replacements.
  • Leaf display fields only. $IDs and ValueTypes are never copied, so the replacement's ObjectType cross-references (freshly generated IDs) stay intact.
  • Properties the stored widget doesn't define (added in newer widget versions) keep their generated metadata.
  • No-ops safely when the replaced widget isn't a pluggable widget or the replacement is a different widget package.

This is deliberately scoped to REPLACE, where "match the stored model" is unambiguous; the wider template/schema question stays with #529.

Verification

  • 6 new unit tests in mdl/backend/pagemutator (same-widget graft, different-WidgetId untouched, non-pluggable old widget, nested object types, duplicate keys scoped by path, combobox nested in container replacement).
  • Full go test ./... passes; make lint-go clean.
  • End-to-end against a real Mendix 11.12.2 MPRv2 project (Combobox v2.8.1, Studio Pro–authored pages):
    • Before: identity rebuild → docker check → 1 error (CE0463).
    • After: identity rebuild → 0 errors; the stored Caption/Category values are preserved byte-for-byte in the unit.
    • After: REPLACE adding a where [Name != ''] XPath datasource constraint → 0 errors, constraint present in the unit.

…loses mendixlabs#112)

Replacing a pluggable widget emitted Type metadata (per-property
Caption/Category) generated from the embedded template and installed
.mpk, while untouched sibling widgets in the same page unit carried
metadata from whichever widget version authored them in Studio Pro.
MxBuild flags the mixed vintages as CE0463 on the rebuilt widget, even
for an identity rebuild that changes nothing.

ReplaceWidget now grafts the stored widget's per-property display
metadata (Caption, Category, Description) onto the replacement's
freshly generated Type block:

- matched by slash-joined PropertyKey path, not flat key, so distinct
  nested object types that reuse a key (e.g. markers/latitude vs
  dynamicMarkers/latitude) stay independent
- only onto replacements of the same WidgetId, including pluggable
  widgets nested inside container replacements
- leaf display fields only; $IDs and ValueTypes are never copied, so
  the new widget's Object/Type cross-references stay intact

Verified against a Mendix 11.12.2 MPRv2 project with Combobox v2.8.1:
identity rebuild and datasource-XPath replace both now pass docker
check with zero errors (previously CE0463 on both).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ront85

ront85 commented Jul 28, 2026

Copy link
Copy Markdown
Author

Update: I re-tested both repros in this PR against unpatched upstream/main (i.e. without this PR's change) and they now pass with 0 errors — including with --no-update-widgets, which rules out the docker pre-flight remediation as the explanation.

Root cause: #8a14fc2d ("reconcile ComboBox definition drift generically — close #112 CE0463", merged 2026-07-20) made the freshly-emitted CustomWidgetType byte-identical (ID-masked) to mx update-widgets output for the ComboBox drift case. That fix landed after v0.16.0 and after the environment (v0.13.0) I originally reproduced this on, and it appears to close the same underlying drift this PR targets, just via a different mechanism (fixing the .mpk parser's property ordering/assignableTo handling rather than grafting stored metadata during REPLACE).

I no longer have a reproducible failure on current main for the scenarios in this PR's description. I'm going to hold off on merging this until I can find a case 8a14fc2 doesn't cover (if one exists) — will update or close depending on what I find. Apologies for the noise; posting this rather than leaving a possibly-redundant PR silently open.

@ako

ako commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks for digging into this, and for the byte-level diff — that's what made it possible to confirm quickly.

Your follow-up hunch was right, but the credit goes to a different commit than you identified. The metadata drift is already fixed on main:

  • 4934fa2"fix(widgets): reconcile PropertyType metadata from .mpk (within-key CE0463 drift)". This is the relevant one. reconcilePropertyMetadata in modelsdk/widgets/augment.go walks the widget Type and overwrites Category, Caption, Description and the ValueType DefaultValue from the installed .mpk for every matched property key — exactly the filterInputDebounceIntervalCategory and onChangeEventCaption divergences in your analysis.
  • 8a14fc2 — the commit you cited. It fixed ComboBox-specific drift (interleaved <systemProperty> ordering and <returnType assignableTo=...>), which is adjacent to your case but not the display-metadata fix.

It's applied in the template loader (augmentFromMPK, called from GetTemplateBSON / GetTemplateFullBSON), so it covers every pluggable-widget emission path rather than ALTER PAGE REPLACE specifically.

The reason we went with the .mpk as the source of truth rather than grafting from the stored sibling: it's what mx update-widgets itself derives from, and it also covers the case grafting can't — a page with no pre-existing widget of that type to copy metadata from.

That said, your PR identifies one thing that is still a real gap, and it'd be very welcome as a standalone change: the slash-joined PropertyKey path matching (/markers/latitude). The current code flattens the .mpk into a map keyed on the bare key, last-write-wins, and matches on the bare PropertyKey — so a nested child shadows a same-named top-level property. Probing it directly:

key 'attribute' resolved to Caption="Column attribute" Category="Column"
   → nested child shadowed the top-level property

For a widget carrying, say, both a top-level attribute and a column-level attribute with different captions, that stamps the wrong metadata — CE0463 again, from the opposite direction. It's latent rather than a confirmed live failure (I reproduced the shadowing with a synthetic definition; I haven't found a shipped widget with a colliding pair whose metadata actually differs), but the fix is clearly correct either way.

Would you be up for re-scoping this PR to just the path-scoped key matching in mpkPropDefsByKey / reconcilePropertyMetadata, dropping the Mutator.ReplaceWidget grafting? Your nested-object and path-scoped-key tests would carry over more or less as-is.

Before you do — worth re-running your identity-rebuild repro against current main first. It should now come back with 0 errors without your patch, and if it doesn't, that's a more interesting finding than any of the above and we should chase that instead.


Generated by Claude Code

@ako

ako commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Review

Recommendation: hold — please re-validate against current main before merging. I think this has been overtaken by work that landed on 2026-08-04, six days after this branch was authored, and if it hasn't, the mechanism is pointed in the opposite direction to it.

To be clear up front: this is careful work. Real byte-level diagnosis, an explicit disproof of #112's stated root cause, six focused unit tests, and end-to-end docker check verification. The problem below is timing, not rigour.


1. The fix contradicts the reconciliation now on main

modelsdk/widgets/augment.go:331 has reconcilePropertyMetadata, which overwrites each property's Category/Caption from the installed .mpk. Its test (modelsdk/widgets/augment_metadata_test.go) states the goal outright:

after this reconciliation every Gallery@10.24 PropertyType matches what mx update-widgets produces

That is the platform's own reference behaviour. This PR grafts Caption/Category/Description in the opposite direction — from the stored old widget onto the replacement — and preserveStoredWidgetPropertyMetadata runs after serialization, so on a REPLACE the stored vintage wins and the .mpk reconciliation is undone.

That reaches further than it might look, because pagemutator is shared by both engines — mdl/backend/mpr/page_mutator.go:32 and mdl/backend/modelsdk/page_mutator_write.go:34 both construct this Mutator. So the graft lands on the default engine (--engine help now reads "modelsdk (default), legacy (fallback)"), which is precisely the engine that has the .mpk reconciliation to undo.

2. The symptom is very likely already fixed at the source

9b133b6e "fix(widgets): run value reconciliation even when the property set matches" (2026-08-04) describes this symptom precisely: AugmentTemplate's opening guard returned early for any widget whose property set already matched its package, skipping all six value-level passes — including reconcilePropertyMetadata. Widgets emitted that way carried stale Caption/Category from the embedded 11.6-era template. The commit names Data Widgets 3.10's drop-down filter, which "declares exactly the 25 keys the embedded template already has."

A Combobox v2.8.1 fits that profile. The two divergences documented in the PR body — both per-property display metadata, one of them a Category — are the signature of that guard. It is also cause family #5 in the .claude/skills/diagnose-ce0463.md skill added the same day:

Augmentation never ran. Before theorising about a widget's stored BSON, confirm the reconciliation reached it.

What would settle it: re-run the identity rebuild on current main at 11.12.2 with Combobox v2.8.1.

  • 0 errors without this patch → close as superseded.
  • Still CE0463 → that is a genuinely valuable result, and the gap is most likely in sdk/widgets/augment.go, which has none of the six reconciliation passes (reconcilePropertyMetadata, reconcileEnumValues, reconcileValueTypesFromMPK, completeValueTypeEnvelope, reorderPropertyTypes are all modelsdk/widgets-only). Porting them there is the fix that matches the established direction — and cause family DATAGRID widget is completely unusable via MDL in mxcli v0.2.0-dirty #6 flags exactly this parallel-copy trap.

Moderate

  • Merge state is CONFLICTING/DIRTY. mutator.go still applies cleanly to main, but mutator_test.go conflicts (tests were appended upstream) and CHANGELOG.md conflicts with fix: bootstrap PostgreSQL without a working service manager (#823) #824's ### Fixed block. Needs a rebase regardless of the outcome above.
  • A stored-model graft is the "value fix that is right for NEW properties but wrong applied to all" shape the skill warns about explicitly — nulling every TextTemplate took CE0463 from 33 → 127. Grafting all three display fields for every matching property is the same broad stroke. If some version of this survives, scope it to the properties that actually diverge.
  • Not tested against the bundled package. Skill rule of thumb: "Test any candidate fix against the bundled package too" — pruning to the update-widgets reference fixed 2 widgets on Data Widgets 3.10 and took bundled 3.4 from 0 → 139. Verification here is one project on one Combobox version.

Things that check out

  • I specifically checked whether the bson.D-only type switch would silently no-op on the codec path (the primitive.D vs map[string]any trap). It's fine: RawType is bson.D in sdk/pages, and the modelsdk path deep-converts via v2ToV1BSON (marshal → unmarshal into bsonv1.D), so nested nodes are bson.D on both engines. The hand-built bson.D fixtures do reflect the real shape.
  • Path-scoped matching (/markers/latitude) rather than flat keys is the right call, and the duplicate-key test is a good one.
  • Never copying $ID/ValueType correctly protects the ObjectType cross-references.

Minor

  • No mdl-examples/bug-tests/ script. This one is MDL-expressible — the alter page … replace repro in the PR body is already exactly what's needed.

Disproving #112's stated root cause is a real contribution and worth keeping in the record whatever happens to the patch. The open question is narrow: which vintage should win on a REPLACE — the stored page, or the installed .mpk? main has committed to the .mpk, so if CE0463 survives on current main, the fix should reach the same answer by a different route.

🤖 Generated with Claude Code

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