Skip to content

Sync ako/mxcli: error handlers in branches, REST body expressions, rename fixes, IMPORT LINK, pluggable widgets patched in place - #1647

Merged
ako merged 26 commits into
mendixlabs:mainfrom
ako:main
Oct 24, 2026
Merged

ako merged 26 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 10 commits from fork PRs ako#1662–ako#1670.

Fixes #1545, #1546, #1557, #1565, #1596 and #1643.

Microflows

Renames

Data import

  • IMPORT … LINK links again (IMPORT ... LINK silently links nothing on Mendix 11.14 (storage_format is integer 1, code expects "Column") #1557). It reported success and linked nothing.
    • The import compared mendixsystem$association.storage_format with "Column" / "Table", but the runtime stores a smallint there: 1 for an FK column, 2 for a junction table.
    • This was measured with psql on apps booted by 10.24 to 11.14.
    • The object-sequence UPDATE now spells its aliases with AS. That changes nothing against PostgreSQL, and it lets the import run end to end on SQLite in unit tests. It is a refactor, in its own commit.

Pages

  • A filter's Placeholder: and Adjustable: in create page are written (textfilter Placeholder: / Adjustable: in create page pass check and exec but are not written; alter page set does write them #1643). They passed check and exec but weren't stored, while alter page … set wrote both.
  • A stored pluggable widget is patched per property instead of rebuilt. An edited widget used to be rebuilt whole from mxcli's template, so every property the statement didn't name lost what Studio Pro had stored. For example, a workflow task grid's Assignee column was re-pointed to System.User.FullName, which gave CE1613.
  • A stored DisabledDuringExecution = false is kept on rebuilt pluggable widgets and layouts.

Fork tooling

  • The fork's issue-triage skill classifies the fork's own issues alongside the upstream mirrors

claude and others added 26 commits October 24, 2026 10:19
The issue-triage skill only classified mirrored upstream issues and left
the fork's own issues unlabelled, so a cluster showed only half of what
the maintainer tracks. Fork issues (open, not `mirror`, not the board)
now get the same cluster and class labels and triage comment, ahead of
mirrors within MAX_TRIAGE. They are never closed, parked or retitled in
any mode; a mirror that duplicates a fork issue is the one closed. The
board shows mirrors and fork issues per cluster in separate columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDRrgJhKKmSUnBFaDUw62
Triage the fork's own issues alongside the mirrors
…uggable widgets and layouts (#549)

Where Studio Pro stored DisabledDuringExecution false on an unused action
slot (a Forms$NoAction), a rewrite still wrote true over it in two places
the #721 L2 carry did not reach:

- a pluggable widget the statement changes, rebuilt from its template:
  its slots are WidgetValues inside the Object, not child actions of a
  named widget. On ako/TestApp, re-running describe of
  WorkflowCommons.Snip_TaskDashboard_Header with only dropDown2's
  emptyOptionText changed passed check, reported "Replaced snippet" and
  turned dropDown2's onEnterEvent and onLeaveEvent true.
- a layout rewrite, which never called the carry.

The rebuilt pluggable widget's NoAction slots now take the stored
widget's flag, paired by property key through each side's own Type; a
slot the statement fills with an action keeps its own settings. Layout
rewrites carry like page and snippet rewrites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUbjJ5DeS2vysn9hfKir4Y
fix(pages): keep a stored DisabledDuringExecution false on rebuilt pluggable widgets and layouts (#549)
PostgreSQL treats AS as optional in UPDATE ... FROM; SQLite requires it.
Spelling it out changes nothing against a Mendix database and lets the
import pipeline run end to end against an in-process SQLite database in
unit tests, which the fix for #1454 relies on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ebuilding it (#832)

A pluggable widget the passthrough declined was rebuilt whole from the
template, so every property the statement did not mention lost what
Studio Pro had stored. The passthrough declines an edited widget, and
also an unchanged one whenever describe prints it imperfectly: on
ako/TestApp, re-running `describe page WorkflowCommons.TaskInbox`
unchanged rebuilt both data grids, turning the Assignee(s) column's
Administration.Account.FullName into System.User.FullName (CE1613) and
the row click that opens the task into a NoAction. Rebuilding every
pluggable widget of TestApp changed 23 distinct untouched properties.
Layout rewrites never loaded the passthrough at all.

A declined widget is now patched: it is built a second time from
describe's own text for the stored widget, the two builds are compared
by property key, and only the properties they disagree on are written,
onto the stored Type. A property bound to the widget's own data source
is kept while that data source is; any other property naming model
elements is kept only while it names what is stored, so a data view
moved to another entity still rebinds. Widgets properties always come
from the statement's build. Layouts load the passthrough, and the docs
say a rewrite keeps a widget's stored schema (use `mxcli widget sync`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUbjJ5DeS2vysn9hfKir4Y
…#1652)

`textfilter tf (Attributes: [Name], Placeholder: 'Search name',
Adjustable: false)` inside `create page` passed check and exec and stored
neither value; describe printed a bare `textfilter tf (Attributes: [Name])`.
`alter page … set (placeholder: …, adjustable: …)` on the same widget wrote
both. Mirror of mendixlabs#1643.

Cause: the filters' hand-written definitions map neither key, so both go
through the widget engine's explicit-property pass, which looked the
authored name up in the template's property keys byte-for-byte.
`Adjustable` is not `adjustable`, and `Placeholder:` is a grammar keyword
the visitor always stores as "Placeholder", so no spelling reached the
template's `placeholder` -- not even the lower-case one describe prints.
check's allow-list and its "persisted by the explicit pass" verdict, the
mapping pass (lookupProperty) and ALTER's setter (#1069) all fold case; the
create writer was the one reader that did not.

Fix: explicitPropertyKey resolves the authored name to the template's own
key -- exact match first, then a case-only match that is unique and of a
kind the pass writes correctly (the set check already calls persisted) --
and the pass writes and scopes by that key. Authored names are iterated
sorted, since two spellings can now land on one key. The number and date
filters' identical placeholder/adjustable pair and the text/number filters'
`delay` are fixed by the same change, as is any unmapped key on any
pluggable widget written in a different case.

Verified on Mendix 11.13.0: the repro stores the placeholder (identical to
the value ALTER writes, ids stripped) and adjustable "false", describe
prints both, describe -> exec reports "Unchanged page", raw mx check
0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their line breaks (#1506)

`alter entity Mod.Ent rename attribute Old to New` rewrote the stored XPath
constraint of every document whose constraint carries a line break between
predicate groups -- in every module, whether or not it named the attribute --
turning `[Status = 'Open']\n[Code = 'X']` into `[Status = 'Open'][Code = 'X']`
and reporting each as "Updated N XPath constraint(s) in N document(s)". On a
larger project one rename wrote 147 units in 21 modules (mendixlabs#1596).

Cause: rewriteConstraint split the constraint with
SplitXPathPredicateGroups, which returns the groups without the whitespace
between and around them, and re-joined them with "" even when no group
changed. The scanner decides whether a unit changed (and counts a rewrite) by
comparing that result with the stored string, so the lost separators alone
made a write. canon.Reconcile could not elide it: the content really differed.

Fix: the splitter gains XPathPredicateGroupSpans, which returns each group's
offsets in the string as given. rewriteConstraint returns the stored string
itself when no group changed, and otherwise splices each rewritten group back
at its offset, copying every byte outside the groups -- so a constraint that
does name the attribute keeps its layout and changes only the name token.

Tests: byte-identical results for unrelated multi-line constraints (another
entity's same-named attribute, a string literal, a longer name sharing the
prefix), layout kept on a real rename, a scan test whose control unit is
written, and an executor test pinning the reported count. Reverting the fix
fails them with the reported symptom ("Updated 3 XPath constraint(s) in 3
document(s)", unrelated units written without line breaks).

Verified on Mendix 10.24.24 and 11.13.0 with stored line breaks: before, 4
constraints in 4 units written; after, 2 in 2 (the referencing retrieve and
the access rule), line breaks kept, mx check 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream: mendixlabs#1557.

Cause: the import read mendixsystem$association.storage_format into a
string and compared it with "Column" / "Table". The runtime stores a
smallint there: 1 for an FK column on the owner's table, 2 for a junction
table. Measured with psql on apps booted by Mendix 10.24.24, 11.12.2,
11.13.0 and 11.14.0, all four store the same values. Neither comparison
ever matched, so a column-stored LINK left the FK out of the INSERT, a
table-stored LINK skipped the junction insert, and the import printed
"<Assoc>: 1 storage" and finished with exit 0. This was not an 11.14
change. LINK had never linked anything against a real database, for
either storage kind.

Fix:
- LookupAssociationInfo decodes storage_format: 1/2, their decimal text,
  and the model names (any case). Anything else, NULL included, is an
  error that names the association.
- ExecuteImport refuses a LINK whose StorageFormat is neither Column nor
  Table before it writes the first row, so an unplaceable link can no
  longer pass silently.
- The executor prints a link count for every LINK, zero included.

Tests: sql/import_assoc_test.go runs the import pipeline end to end
against SQLite fixtures that mirror the measured system rows. With the
fix reverted, both storage kinds report "0 linked / 0 missed, want 4 / 1",
storage_format 1 decodes to "1", and an unrecognised format imports
without error. End to end on 10.24.24, 11.12.2, 11.13.0 and 11.14.0, psql
showed 0 FKs and 0 junction rows with the old binary and 4 + 4 with this
one. On 11.14 an OQL read through the booted runtime showed the same
links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(pages): patch a stored pluggable widget per property instead of rebuilding it (#832)
…1443)

`rename page Shop.Order_Overview to Order_List` (MDL or `mxcli rename`) on a
page in a folder named 'Order_Overview' renamed the folder, left the page as
it was, printed "Renamed page: ..." and had already pointed every caller at
the new name, so mx check reported CE1613 "The selected page
'Shop.Order_List' no longer exists". The same held for every kind RENAME
handles as a document: microflow, nanoflow, constant, enumeration, Java
action and workflow. Mirror of mendixlabs#1545.

Cause: the handlers resolved the document among the requested kind only to
set a `found` flag, then called RenameDocumentByName(module, old, new), which
looked the bare name up again over every unit in the module's containers,
whatever its $Type, and rewrote the first match. A Projects$Folder is such a
unit, and a FOLDER clause inserts the folder before the document it holds, so
the folder won. The reference sweep ran first, so the damage was done either
way.

Fix: remove the second lookup. The handlers keep the ID of the document they
found and call RenameDocument(id, oldName, newName), which replaces
RenameDocumentByName on the backend interface and refuses a folder or module
unit and a stored Name other than oldName. The document is now renamed before
the reference sweep, so a refused rename leaves every reference untouched.

Verified on a blank 11.12.2 app with the issue's repro plus nanoflow,
constant, enumeration, Java action and workflow: folders keep their names,
references follow the documents; the bug-test script goes from 4x CE1613
(main) to 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…text (#1444)

`call rest service post '…' (Body: $Payload)` was stored as the custom
request template '$Payload' with no parameters, so the request body was
the eight characters "$Payload". check, exec and mx check all passed;
mx check only warned that $Payload was never used (CW0094). The
deprecated `body $Payload` clause built the same statement, and a URL
written as an expression (`@M.BaseUrl + '/rates'`) was stored as that
text, so the request never reached the server. Upstream:
mendixlabs#1546.

Cause: addRestCallAction turned any URL or custom body that was not a
string literal into fb.exprToString(...) and stored that source text as
the Microflows$StringTemplate's Text.

Fix: an expression is now the template '{1}' with the expression as its
one parameter. This is what Studio Pro stores (ako/TestApp
Mappings.PostCustomRequestTemplate_2, 11.13.0: Text '{1}' plus one
TemplateParameter), and it is the rule log, show message, validation
feedback and send email already follow. A string literal is still the
template text, including one that spans lines (a SourceExpr around the
literal). An expression followed by `with (…)` has no {n} to fill, so it
is refused as MDL-REST05. checkRestCallTemplatesStmt makes that decision
for both the check pass and exec. declaredMatches normalizes both
spellings the same way, so `create or modify` re-run against the stored
flow reports the activity unchanged. Describe is unchanged: it prints
`Body: template '{1}' with ({1} = $Payload)`, which re-executes to the
same document.

Verified on an 11.13.0 app. The CW0094 for $Payload is gone and mx check
reports 0 errors. Describe -> exec reports "Unchanged microflow", and so
does re-running the original `Body: $Payload` spelling. At runtime an
echo server received `{"number": "N-1444"}` where the baseline sent
"$Payload", and the expression-URL request now arrives (the baseline's
never did).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low instead of continuing after the failing activity (#1459)

Upstream: mendixlabs#1565. A custom error handler that does not end
in RETURN or RAISE ERROR on an activity inside an IF branch was wired to
the wrong place. With a result variable (`$N = call microflow …`,
`$Obj = create …`, `$R = call rest service … returns response`) the
handler's last activity flowed into a merge spliced in front of the
enclosing split, so a call that kept failing re-entered the decision for
ever. Without one it flowed into a new end event and nothing after the
failure ran; with `returns … as $Result` that end returned a variable that
does not exist (CE0109). In a THEN ending in RETURN the handler skipped to
the code after the IF. The same held in CASE and type-split arms, nanoflow
loop/while bodies and `while true`. check, exec and mx check passed.

Cause: pending fall-through rejoins live in one builder-wide slot that only
buildFlowGraph's top-level loop settled, on the flow leaving the failing
activity. The branch body loops settled only an empty handler, so a
non-empty one leaked out of the branch and the enclosing loop settled it on
the next flow it drew: the skip-var path (any activity with an output
variable) ignores the origin and spliced it into `previous -> split`;
without a variable nothing matched and terminatePendingErrorHandlersAtEnd
gave it an end event; a loop sub-builder dropped it.

Fix: scope the slot to each branch body (enterBranch/leaveBranch). Every
flow a branch draws settles the branch's own handlers (settleBranchFlow),
including its exit into the IF/CASE merge; only what a branch cannot settle
is handed back to the enclosing path — the last activity of an arm that
continues without a merge. The enclosing path's handlers are no longer
visible to a branch's flows either. In an arm that ends in RETURN a
skip-var handler that would have to skip a statement reading the failed
output still continues where the IF does (a30881b's shared
continuation), but the IF's merge is now created only when an arm actually
leaves such a handler, instead of whenever the arm has a continuing
handler. A rejoin merge spliced into a branch's exit stays on the
branch's row.

Verified: describe -> exec round trip unchanged on every shape, mx check
0 errors on 11.12.2, and at runtime a failing call logs 'failed', 'after
call', 'end' once each (before: 3063 'failed' lines in five seconds).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pped

fix: a filter's Placeholder/Adjustable in create page are not written (#1652)
…-xpath

fix: rename attribute rewrites unrelated XPath constraints and drops their line breaks (#1506)
fix: IMPORT ... LINK reports success and links nothing (#1454)
fix: rename of a document in a same-named folder renames the folder (#1443)
fix: call rest service sends an expression body or URL as its source text (#1444)
fix: a fall-through error handler inside a branch loops or ends the flow (#1459)
The storage_format section added for #1454 grew "Step 4" by seven
lines without regenerating the skill's line index, so every range after it
pointed seven lines early. `make build` (skill-index) rewrites it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1455)

Inside `on error … end error`, an `if` without `else` whose THEN ends in
RETURN passed check and exec, but its decision left on `true` and NoCase:
mx check reported CE0079 at the decision and CE0773 at the flow, and
describe read the NoCase edge back as an `else`. The same `if` in the main
flow was stored with true/false. Mirror of mendixlabs#1558.

Cause: such an `if` has no merge, so it leaves its FALSE edge pending — the
case (and @anchor) the NEXT edge must carry. addErrorHandlerFlow's
handler-body loop kept the case in lastErrCase but drew the edge to the
next statement with a plain newHorizontalFlow; every other body loop takes
the pending case and anchor there.

Fix: draw that edge with newHorizontalFlowWithCase when a case is pending,
and apply the deferred anchor through pendingFlowAnchors, as the IF arms do.

Tests assert the decision's edge cases directly (describe text hides the
defect), with a main-flow decision as the control and a describe round
trip. Reverted fix: every handler decision leaves on ["" "true"]; reverting
only the anchor half fails 1→3 vs bottom→top. On Mendix 11.13.0 the bug-test
script goes from 10 errors (5× CE0079, 5× CE0773) to 0, describe → exec is
Unchanged, and re-running the script leaves all documents in sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld one (#1671)

`rename constant M.C_Root to C_Base` printed "Renamed constant" with no
reference count, and mx check then reported CE1613 "The selected constant
'M.C_Root' no longer exists" wherever an expression used it. A cross-module
`move constant` the same. `rename enumeration` renamed a value that was the
whole expression but not one inside a longer one (CE0117).

Cause: the reference sweep's two primitives (replaceQNInValueCounted for
RENAME, replaceQualifiedNameRef for cross-module MOVE) rewrite a stored
string only when it equals the old qualified name or starts with it plus
'.'. An expression always writes a constant as @Module.Name, so not even a
constant that is the whole expression matched; an enumeration value matched
only as the whole expression.

Fix: types.RewriteExpressionReferences rewrites, as whole tokens, @oldQN (a
constant) and oldQN.Member (an enumeration value) not preceded by a name
byte, '.', '/', '$' or '@' — so neither a longer name, another module's
element, nor an association path step — and skips single-quoted literals
('' escapes included). Both primitives apply it to any string the
whole-string match leaves alone. A module rename's prefix pair is left to
its own path. The `mxcli syntax rename` topic says what is now covered.

Tests: 20 rewriter cases (XPath form included, which MDL cannot author yet)
and an executor test on PedApp covering rename constant, rename enumeration
and a cross-module move, with a string literal that must stay verbatim.
Reverted fix: every expression use stays stale; reverting only the MOVE
primitive fails only the move assertions. Mendix 11.13.0: the issue's rename
goes from 3x CE1613 to 0 ("Updated 3 reference(s)"), the enumeration rename
from CE0117 to 0, the bug-test script from 4 errors to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the old module (#1674)

`move enumeration Shop.Status to Other;` printed "Moved enumeration", but
every reference outside the domain models kept naming the old module:
mx check reported CE1613 at a parameter of the enumeration type, a
create-variable activity (type and value), an end event returning a value,
and the return type. Entity attributes of the type were updated.

Cause: execMove's dispatch did `return moveEnumeration(…)`, returning before
the shared cross-module reference sweep (updateQualifiedNameRefs) that every
other document's move falls through to. moveEnumeration itself only updated
domain-model attribute types.

Fix: the enumeration case falls through like the others, so a cross-module
enumeration move runs the sweep and reports "Updated references in N
document(s)".

Test: an executor test on PedApp moving an enumeration used as a parameter
type, return type, variable type and value, with the entity attribute as
the control; against the unfixed code every microflow reference stays on
the old module. Mendix 11.13.0: the issue's repro and the bug-test script
go from 5x CE1613 to 0 errors. An enumeration value inside a LONGER
expression additionally needs the expression-aware sweep of
#1671 — measured: this fix alone leaves CE0117 on
`$In = X.Open or …`, both together 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: an if without else inside an error handler loses its false case (#1455)
fix: rename constant leaves @Module.Const in expressions naming the old one (#1671)
fix: move enumeration to another module leaves its references naming the old module (#1674)
@ako ako closed this Oct 24, 2026
@ako ako reopened this Oct 24, 2026
@ako
ako merged commit ff2473f into mendixlabs:main Oct 24, 2026
28 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.

rename page renames a folder with the same name instead of the page, reports success and repoints every reference to a page that does not exist

2 participants