fix(write): re-running a script no longer rewrites unchanged documents (formula1 §50) - #125
fix(write): re-running a script no longer rewrites unchanged documents (formula1 §50)#125ako wants to merge 3 commits into
Conversation
mxcli-formula1 §50, still-open item #2. A `create or modify` rebuilds the whole gen tree from the semantic model, so every sub-element arrived with no ID and was assigned a fresh UUID. The document keeps its own ID, so the model was identical and the bytes were not: one constant re-declared identically rewrote exactly 16 of its 243 bytes, and a full script re-run rewrote 143 documents across two apps with a different tree hash every time. That makes `git diff` unable to answer "did this script change anything", makes two developers running the same scripts commit different bytes, and makes a `.mxunit` merge unresolvable by hand. element.PreserveIDs walks the stored tree and the rebuilt one in parallel and carries IDs where they correspond. Matching is deliberately conservative, because a wrong match moves an existing element's identity onto a different element: - types must match, or neither the node nor its subtree inherits; - child lists match by Name when every child on both sides has a unique one, so an insertion in the middle keeps IDs with their elements rather than their positions; - otherwise position, and only for equal-length lists. Unequal means something was added or removed, so those children keep fresh IDs. Wired through encodePreservingIDs into persistDM (all 17 domain-model writes) and the 18 other Update* paths. Creates are untouched: a new document has nothing to inherit from. §50 files this as the cosmetic half. It is not uniformly so — 06a9fac fixed the same shape for entity attributes, where a fresh ID made Mendix's database synchronizer drop and re-add the column. This removes the class rather than another instance. Verified on both formats: a v1 .mpr and a v2 mprcontents tree (376 files) are byte-identical across three consecutive re-runs, and `mx check` reports 0 errors on the result. Controls reproduce three distinct hashes on both.
|
Tested this on a real MPR v2 app (Mendix 11.13.0, 413 I built Same result on the merge ref Minimal reproRe-declaring a single microflow identically is enough: mxcli exec one.mdl -p Sudoku.mpr # one unchanged `create or replace microflow`
mx check Sudoku.mpr # KeyNotFoundException: '408c1692-…'Control on the same input: By document type: microflows and nanoflows corrupt; pages and navigation stay valid. Cause
Searching the whole tree for the GUID the loader rejects finds it once, as an Where it doesn't corrupt, it also doesn't help muchPages + navigation only, one extra identical run: Inside one page, 980 of 988 IDs still regenerate — only the top-level chain ( The ~59% churn reduction across the whole script set comes entirely from microflows — the Separately, SuggestionRewriting IDs after the tree is built means every reference to those IDs has to be The commit message reports both formats byte-identical across three re-runs with Generated by Claude Code |
…documents" This reverts commit e89d71d.
The PreserveIDs attempt (reverted in f1371f7) rewrote element $IDs without rewriting the pointers that reference them, which makes a project unopenable with KeyNotFoundException at ResolvePostponedProperties. Records the symptom, why only some document types break, and the verification gap that let it through: a fixture with no microflow cannot exercise a bug in microflow sequence flows, and unit tests do not run mx check at all.
|
You're right, and the diagnosis is exact. Reverted in CI caught the same thing independently, which I should have seen before asking for review: Confirming the cause in the codeYour reading is right, and the reason the walk misses pointers is structural. if val, err := raw.LookupErr("OriginPointer"); err == nil {
if s, ok := val.StringValueOK(); ok {
o.origin.SetFromDecode(element.ID(s))
} else if _, bdata, bok := val.BinaryOK(); bok {
o.origin.SetFromDecode(element.ID(codec.BinaryToUUID(bdata)))
}
}So a child-walk cannot see them by construction. Where my verification failedThe gap is the one you guessed. My test project had two entities, an association, an enumeration and constants — no microflow at all. So That is the more useful lesson than the bug: a green I've recorded both the symptom and the verification gap in On the two other measurementsBoth land, and together they change the cost/benefit enough that I don't think a fixup pass is the right next step:
Next attemptYour second suggestion is the right one: seed the rebuild with the stored IDs so pointers are computed correctly from the start, rather than rewriting IDs afterwards and chasing references. A fixup pass would need to identify which primitive properties are ID-valued — possible via the generated I'd want any retry to come with: an integration test over a microflow with sequence flows, a whole-project Happy to take that on, but it's a larger change than this PR was, so say if you'd rather it wait or go to someone else. §50 item #2 goes back to open either way; item #1 (#124) is unaffected. Generated by Claude Code Generated by Claude Code |
mxcli-formula1§50, still-open item #2 — the other half of §50, after #124 closed item #1.The report
Re-running a script against an already-built project rewrites documents with different bytes every time. Zero added, zero deleted, semantic fingerprint identical throughout:
The same 74 files, a different hash every time.
git difftherefore cannot answer "did this script change anything", two people running the same scripts commit different bytes, and a.mxunitmerge is not resolvable by hand.Cause
A
create or modifyrebuilds the whole gen tree from the semantic model, so every sub-element arrives with no ID andassignIDmints a fresh UUID. The document keeps its own ID, which is exactly why the model is unchanged and the bytes are not.Reproduced here to the byte, on a constant:
One correction to §50, which changes how this should be prioritised
§50 files this as the cosmetic half, separate from "the one that is not cosmetic". That is not uniformly true.
06a9facefixed this same shape for entity attributes, where a fresh ID reads to Mendix's database synchronizer as "attribute departed, new attribute added" — so it drops and re-adds the column. Rows survived, values gone; 11 feeds and 98 articles blanked,mx checkclean throughout, and the loss only surfaced when something read the data.So sub-element ID churn is cosmetic for some element types and destructive for others. Rather than continue auditing types one at a time, this removes the class.
The fix
element.PreserveIDswalks the stored tree and the rebuilt one in parallel and carries IDs where they correspond. Matching is deliberately conservative, because a wrong match moves an existing element's identity onto a different element — worse than the churn it fixes:TypeNamediffers is a different element; neither it nor its subtree inherits anything.Namewhen every child on both sides has a unique one. This is what makes an insertion in the middle safe: names travel with their element rather than with their position. The name index is scoped by type, since two children can share a name across types.Anything unmatched keeps the ID it arrived with, so a genuinely new element is always genuinely new.
Wired through
encodePreservingIDsintopersistDM(the single choke point for all 17 domain-model writes) and the 18 otherUpdate*paths. Creates are untouched — a new document has nothing to inherit from — and the helper falls back to a plain encode if the stored version cannot be read, since a write must not fail because an optimisation could not run.Verification
Both MPR formats, three consecutive re-runs of a script creating two entities, an association, an enumeration and constants:
.mpr(whole-tree hash)mprcontents(376 files)mx checkon the resultControls reproduce three distinct hashes on both formats.
Tests:
modelsdk/element/preserve_ids_test.go— nine cases covering each matching rule: identical tree, type change, named insertion (the new attribute must not inherit an ID and the existing ones must keep theirs), removal, positional match, unequal-length refusal, name-scoped-by-type, duplicate-name fallback, nils.mdl/backend/modelsdk/preserve_ids_test.go— an unchanged rewrite is byte-identical, and a real change still writes. The second is not optional: a "fix" that achieved stability by not writing would pass the first.Control on the backend test fails with "an unchanged rewrite changed 78 of 1771 bytes — sub-element IDs are being regenerated".
Not addressed
Cleaning up associations a project already accumulated from item #1 still needs a repair path — external-entity access rules reference them, so deleting one raises CE1613.
go test ./modelsdk/... ./mdl/... ./sdk/... ./cmd/...green;go vetandgofmtclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Generated by Claude Code