Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Idempotent Writes

Re-running an MDL script against a project that is already in sync leaves the .mpr and mprcontents/ files byte-identical. git status stays clean and Studio Pro shows no version-control changes, because the write does not happen at all.

This matters beyond tidiness: without it, git diff cannot answer “did this script change anything”, two people running the same script commit different bytes, and a .mxunit merge conflict is not resolvable by hand.

Why a write used to happen anyway

create or replace does not reconcile. It rebuilds the document from the MDL and overwrites, and every sub-element in that rebuild is a new object with a freshly random $ID. The stored bytes were a function of the script and a random source, so no amount of care in the builders could have made them stable.

What mxcli compares

Before writing a unit, mxcli compares the new document against the stored one in a canonical form: every element $ID is replaced by its index in a deterministic walk, so a difference in which UUIDs were minted is not a difference. If the two are canonically equal, the write is skipped and the stored document — with its existing IDs — stays exactly as it was.

Skipping is safer than writing, not merely cheaper. The stored IDs are the ones every pointer inside that unit already agrees with, and nothing outside the unit can observe them: cross-document references are by qualified name.

The comparison is biased toward writing. If anything cannot be decided, mxcli writes. A redundant write costs you a diff; a wrongly skipped write would lose your edit.

Identity is preserved, not re-minted

A microflow carries a StableId, which Mendix declares as an identifier and which the build turns into the operation id the browser uses to call that microflow. mxcli carries the stored value onto the rebuilt document instead of minting a new one, so re-running a script does not renumber operations in your deployed model.

Both engines, every write path

The policy lives in one place (modelsdk/canon) and is applied at the single write choke point of both the default modelsdk engine and the legacy engine. Which engine ran is an --engine flag, and it must not be visible in your diff.

Turning it off

MXCLI_ALWAYS_WRITE=1 mxcli exec script.mdl -p app.mpr

Every write lands, whether or not anything changed. This exists for bisecting a suspected elision bug (“does it still reproduce if nothing is skipped?”) and is not a supported option. It disables skipping only — identity is still preserved, because a forced write that re-minted StableId would change the deployed app rather than help you debug it.

Verifying it yourself

Run your script twice against a settled project and compare the stored units:

find mprcontents -name '*.mxunit' | sort | xargs sha256sum > before.txt
mxcli exec script.mdl -p app.mpr
find mprcontents -name '*.mxunit' | sort | xargs sha256sum > after.txt
diff before.txt after.txt        # expect no output

Two cautions, both of which produce a meaningless zero:

  • Make sure the script is actually re-runnable. A script containing create module or create enumeration fails on the second run and writes nothing, so the diff is empty for the wrong reason. Check the run’s output.
  • Run the control. Repeat with MXCLI_ALWAYS_WRITE=1 and confirm the diff is non-empty. If it is empty too, your measurement cannot detect churn and the clean result proves nothing.

For a per-unit view of what would be skipped, scripts/mprsnapshot -canon emits canonical digests keyed by unit id.

See ADR-0008 for the decision and the measurements behind it.