Sync ako/mxcli: silent data-loss fixes, IN QUEUE, and capability docs corrected against measurement - #921
Merged
Conversation
…EFAULT mendixlabs#910. `ALTER ENTITY … MODIFY ATTRIBUTE UserID SET DEFAULT NULL` silently rewrote an Integer attribute to `Enumeration(set)`. MODIFY ATTRIBUTE always parses a type, and the grammar's last dataType alternative is a bare qualifiedName (an entity reference), so `SET` was read as the type and `DEFAULT NULL` as a constraint. The executor then wrote an EnumerationAttributeType with no enumeration behind it. Two things make this worse than reported. It is not specific to SET DEFAULT NULL — an ordinary typo (`MODIFY ATTRIBUTE A Integr`) corrupts identically. And the result is not merely wrong, it is unloadable: mx check dies before running any validation with System.ArgumentNullException: Value cannot be null. (Parameter 'value') at EnumerationAttributeType.set_EnumerationId CREATE ENTITY has rejected exactly this since mendixlabs#552, but the guard was inline in the create handler and never reached MODIFY. It is extracted to validateAttributeTypeRef and applied on the modify path before the attribute is touched. Refusing alone would leave the reporter's actual goal unreachable, since restating a type is the only way to keep a default and there was no way to clear one. So DROP DEFAULT ON ATTRIBUTE is added, and the refusal names it. A calculated attribute is refused rather than silently converted to a plain stored one. mxcli's own `syntax` topic documented `MODIFY ATTRIBUTE AttrName SET DEFAULT val` — the corrupting form — which is the likeliest way the reporter arrived at it. Verified against a pre-fix binary: that documented statement produced `Enumeration(set) default set.5`. The topic now shows the type as mandatory, names the trap, and lists DROP DEFAULT. Verified end to end on Mendix 11.13.0: before, mx check could not load the project; after the same flow it reports 0 errors. Each test was checked to fail against a stubbed guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Verifies the 8-slice feedback report from the mxcli-banking app against this checkout. 13 of 14 mxcli-side claims reproduce; roots pinned for 12. Notable: - DROP ATTRIBUTE's validation-rule cleanup is dead code: the executor matches on element ID while parseValidationRule stores a qualified name in AttributeID. Same confusion affects MemberAccess. - Microflow ApplyEntityAccess is hardcoded false in both engines with no MDL syntax to set it, which is the reported datasource leak. - CONV010's ALLOWED_ACTIONS uses BSON storage names the linter never sees; QUAL004 counts only call/schedule edges. - Two new defects not in the report: dropping the last attribute of an entity is a silent no-op, and a synced skill documents a NON_PERSISTENT spelling that does not parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…abs#910) `alter entity Mod.Ent rename attribute Old to New;` renamed the attribute inside the domain model and stopped there. Everywhere else Mendix stores an attribute reference it stores the fully qualified name as a string, so microflow create/change members, page attribute widgets, and the entity's own validation and access rules all kept pointing at the old name. mxbuild reports each one as CE1613 "The selected attribute 'Mod.Ent.Old' no longer exists."; Studio Pro still opens the project, so nothing looks wrong until a build. `mxcli rename` already had the project-wide reference scanner for entities, enumerations, associations, modules and documents — attributes were the one kind never wired to it. The handler now calls RenameReferences and reports how many documents it touched. Order is load-bearing: the scan runs AFTER UpdateEntity, not before. It is a raw-BSON pass over every unit including the domain model, while UpdateEntity re-serializes the whole domain model from the parsed entity — scanning first would hand the model write the last word and undo the scan's edits inside the domain model, which is where a validation rule's Attribute string and a MemberAccess.Attribute live. execRenameEntity gets away with the opposite order only because entity refs within the domain model are binary pointers. Also adds the missing collision guard: renaming onto a name the entity already uses produced two attributes no reference could tell apart. Scope, stated rather than hidden: uses inside expressions ($obj/Attr) and XPath constraints ([Attr = ...]) are free text where a bare name is only resolvable from the type of what precedes it, so they are not rewritten. mxbuild reports those as CE0117 / CE0161 and the command now says so instead of reading as complete. Controls on Mendix 11.13.0 with the new bug-test script: a build from origin/main leaves 3 errors after the rename (2 microflow members + the validation rule), this build leaves 0. Each new unit test was shown to fail with the reported symptom against the stubbed-out fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…shipped default (mendixlabs#891) Authoring an Accordion raised CE0463 "the definition of this widget has changed" -- on a widget created seconds earlier against the very package it was built from. An object-list item's REQUIRED TextTemplate that the author leaves unset was serialized as null. The empty-ClientTemplate convention lived in emptyClientTemplateRules, a hardcoded per-widget table covering only DataGrid columns, so every other object-list widget fell through it. In a stock blank app the Accordion's group (required headerText) failed, and thirteen shipped widgets declare object lists with texttemplate items. Required-ness is genuinely absent from the widget XML here -- and the widget schema defaults `required` to TRUE -- so headerText is mandatory though nothing says so. It now comes from the widget's own PropertyTypes, needing no per-widget table and unable to go stale against a package upgrade. Both weaker forms were measured and rejected. null is CE0463; an EMPTY Forms$ClientTemplate is CE4899 "Property 'Groups/1/Text' is required" -- so the intuitive "emit an empty template" fix only moves the error. Only the widget's shipped <translations> satisfy both, which is what mx update-widgets writes: mxcli and the reference now emit the same 'Header'/'Koptekst'. Scoped to REQUIRED properties. An optional unset TextTemplate keeps its null, matching Studio Pro; filling every one is the documented way to take CE0463 from 33 to 127. Measured against a baseline fixture (accordion + pop-up menu + a plain DataGrid2): 3 errors -> 2, no new errors of any code, DataGrid2 untouched. Control run with the call site removed fails the new test. PropertyTypeIDEntry exists in three packages, so the field is carried through convertPropTypeIDs; without that it silently never arrives. The pop-up menu's remaining CE0463 is a DIFFERENT defect -- its `caption` is required="false" in the XML, so this rule correctly does not fire on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…typing The half of mendixlabs#910 the reference scanner cannot reach — attribute names inside microflow expressions ($Order/Status) and XPath constraints ([Status = 'Open']) — needs exactly the resolution machinery PROPOSAL_expression_type_checking already builds for its check and refs consumers. Adds it as a fourth consumer alongside those, and links the dependency from the rename proposal's scope exclusions (which did not mention the gap, and listed attribute rename as plainly "Works"). Three things worth having written down rather than rediscovered: - The XPath half needs no type system and is shippable before build-order step 1. A constraint's target entity is known structurally, the parse and re-render round trip already ships (ParseXPathConstraint / expressionToXPath / SplitXPathPredicateGroups), and enrichXPathConstraintForDescribe is a working precedent for resolving bare attribute names against a known entity. Only multi-hop association paths need the resolver. - The expression half is genuinely step 1, and is the one place the dormant exprcheck parser is in the hot path: stored expressions come back from BSON as raw strings, not as the mdl/ast nodes the visitor produces. - The proposal's failure mode inverts for a mutating consumer. Unresolved → KindUnknown → "catch less, never false-positive" is right for an advisory check; a rename must instead refuse to rewrite and report, because silently rewriting an occurrence whose type could not be established corrupts a model that was valid. That is a constraint on the resolver interface, not on its callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
ALTER SETTINGS MODEL refused every spelling of Mendix's optimistic
locking setting with a bare 'unknown model setting', which put a
security-relevant runtime setting on the needs-Studio-Pro list. Reported
by the mxcli-banking app as the unclosable half of a read-then-write
balance race in a transfer microflow.
alter settings model EnableDataStorageOptimisticLocking = true;
Every layer but one was already in place: model.ModelSettings carried the
field and both engines read and wrote it, so the property round-tripped
faithfully on every write while being unreachable from MDL. Only the
executor's assignment case was missing.
The stored key is verified against Studio-Pro-created projects on both
9.24 and 11.13.0. Unlike JavaVersion -> JavaMajorVersion (renamed between
11.6 and 11.12) this one did not move, so a single spelling is correct
for every supported version.
The unknown-key error now lists the accepted model keys, guarded by a
drift test. The report tried three wrong names in a row and was never
pointed at the right one.
Verified on an 11.13.0 project: correct BSON key written with
JavaMajorVersion preserved beside it, mx check 0 errors, re-running is
byte-identical (MXCLI_ALWAYS_WRITE=1 as the control that does churn), and
a non-boolean value is refused by mxcli check (MDL-SET02) and the write.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…endixlabs#910) XPath names an attribute as a bare step — [Status = 'Open'] — so the qualified-name scan that fixes "Mod.Entity.Attr" references cannot see it, and a scan for the bare name would corrupt string literals, function names and other entities' identically-named attributes. Renaming therefore left every constraint dangling, which mxbuild reports as CE0161. No type inference is needed for this half. A constraint's target entity is known structurally — a retrieve names its Entity, a widget data source its EntityRef.QualifiedName, an access rule the entity it sits inside — and every further hop is an association or entity named in the path, resolved against a flat index built from the domain models. Three design points, each of which cost a wrong first attempt: - The edit is textual, gated by the parse, not a re-render of the parsed tree. Re-rendering would churn spacing in constraints the rename has no business touching, and any parser/renderer disagreement would corrupt a working one. Only a single identifier token changes. - The load-bearing safety check is a count invariant. ParseXPathConstraint runs with ANTLR's error listeners removed, so it recovers and can hand back a tree that silently omits part of its input — [LastName = 'L' FirstName] parses and the trailing step is simply absent. Requiring the lexical occurrence count to equal the walked occurrence count is what makes leaning on that parse safe. - A mutating consumer needs three answers, not two: this entity's attribute (rewrite), a different entity's (leave alone, say nothing), and cannot tell (leave alone, report). Collapsing the last two made the pass silently skip constraints it should have questioned. Also fixes a second defect the same rename exposed. UpdateEntity re-derives an access rule's members from the attributes it can match, so renaming in the model orphaned the old member and the reference scan then renamed the orphan into a duplicate — three members for a two-attribute entity, reported as CE0066 "Entity access is out of date". The entity's own by-name references are now fixed in the model before the write. Trap worth recording: a domain model entity node is stored under DomainModels$EntityImpl, not DomainModels$Entity (that is the metamodel's name for a reference target). Matching the latter matched nothing and made every access rule look like a cautious refusal rather than a bug. Controls on Mendix 11.13.0 with the new bug-test script: the same script without the rename is 0 errors; with the rename it was 3x CE0161 and then 1x CE0066; it is now 0. On the earlier repro the progression is 5 errors (pre-fix) to 3 (qualified-name half) to 1 — and that one is the microflow expression, which is reported as out of scope and needs the resolver in PROPOSAL_expression_type_checking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
… them across versions Adds FirstDayOfWeek, DecimalScale, DefaultTimeZoneCode, UseDatabaseForeignKeyConstraints, UseOQLVersion2 and SslCertificateAlgorithm to ALTER SETTINGS MODEL. The two enum-typed ones are canonicalised against generated/metamodel and a non-member is refused (MDL-SET03) rather than written through: an enum value the metamodel cannot resolve is what makes Studio Pro throw 'Sequence contains no matching element'. Fixes a defect found while checking whether that was safe. A blank Mendix 9.24 project stores 12 model settings; a blank 11.13 stores 17. Both engines' overlays wrote every known property unconditionally, so on a 9.24 project 'alter settings model BcryptCost = 11' also introduced DecimalScale = 0 and UseDatabaseForeignKeyConstraints = false -- properties that version does not store, at values that are wrong anyway (11.13 defaults them to 8 and true). One unrelated statement silently changed two other settings, and a property the type may not define is what Studio Pro refuses to open while mxbuild loads it happily. The overlay is now presence-gated and shared by both engines rather than duplicated (as Configurations already was). The executor refuses an ALTER naming a property the stored document does not carry, so the gate never becomes a silent no-op, and DESCRIBE emits only stored properties so its output replays on both versions. UseSystemContextForBackgroundTasks is read and preserved but deliberately not writable: mx check on 11.13 rejects a project holding true with CE9436 'not supported anymore', so its only legal value is its default. Verified on real 9.24 and 11.13 projects: the 9.24 key set is unchanged through a write, 11.13 takes all six with mx check at 0 errors, both describe outputs replay, re-running is byte-identical with MXCLI_ALWAYS_WRITE=1 as the control, and the regression test reproduces the leak when the guard is stubbed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
mxbuild 11.13.0 is cached now, so finding #14 was tested rather than reasoned about. It does not reproduce: SHOW ENTITIES lists the view as Type: View, DESCRIBE round-trips it and its OQL, GRANT succeeds, a microflow RETURNS LIST OF it, '--' comments in the OQL body parse, and CREATE OR MODIFY prunes members the OQL no longer produces. The whole chain passes mx check with 0 errors. No fix landed in the window that would explain it. The reporter's own notes describe a view that had been dropped and never recreated, which would make 'entity not found' from GRANT and RETURNS, plus the absence from SHOW ENTITIES, correct answers rather than three bugs. This matters because the finding is what killed their dashboard design. Two things in the area are real, and recorded: a view entity needs UseOQLVersion2 = true or the build fails CE6779 (a setting that was not reachable from MDL until the previous commit), and cast() parses without a length but not with one, so a pass-through String(unlimited) column still cannot be narrowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…faces
write-microflows.md documented `case … when … end case;` as supported
(with CE0079/MDL056/MDL008 detail), then declared "CASE/SWITCH not
supported — use nested IF" 1000 lines later. Five other surfaces sided
with the wrong half, and the generated project CLAUDE.md gave a third
shape that parses in neither.
Settled against the binary rather than by argument, one variant per
claim:
case $Status when Open, Pending then … when (empty) then … end case;
-> passes
case $Order/Status when Open then … else … -> MDL008
case $Order/Status as s … -> mismatched input 'as'
when Module.Enum.Value … -> mismatched input '.'
when 'Active' … -> extraneous input ''Active''
So CASE is supported, in microflows and nanoflows alike, for enum splits
only: bare enum values, a branch per value including `(empty)`, no
`else`, no alias. Notably the old "WRONG" example failed for an
unrelated reason (string-literal values), which is what made the false
claim look confirmed.
Corrected: write-microflows.md (the unsupported section now pins the
three spellings that really fail), check-syntax.md, MDL_QUICK_REFERENCE
(which also contradicted itself), docs-site control-flow (given a proper
CASE section — it had no CASE docs at all, only two denials),
appendixes/{quick-reference,common-mistakes}, and init_claudemd.go.
Pinned both directions in mdl-examples/bug-tests/ so `make check-mdl`
catches drift back: a positive fixture for the documented forms and a
.fail.mdl for the quoted/qualified/alias spellings. Symptom row appended
to fix-issue.md, including why the existing skill-MDL gate could not
have caught this (it skips microflow bodies).
The skills said "NEVER use a string literal for an enumeration"; docs-site used `Status = 'Draft'` in a dozen CREATE/CHANGE examples and called it "plain strings when the context is unambiguous". Both are half right, and `mxcli check` passes the good and the bad form alike, so the build is where a reader finds out which half applied to them. Measured on a real project rather than argued — mxcli new 11.13.0, one microflow per construct, `mxcli docker check` read per construct: if $O/Status = 'Draft' then … CE0117 "Error(s) in expression." log warning 'x: ' + $O/Status; CE0117 "Error(s) in expression." change $O (Status = 'Submitted'); clean create M.E (Status = 'Draft'); clean DEFAULT 'Draft' on the attribute clean retrieve … where [Status='Draft'] clean getCaption(…) / toString(…) concat clean The string form is accepted wherever the slot is already enum-typed, and rejected in an expression, where it is a String compared to an Enumeration. So only comparisons and concatenations were fixed: control-flow.md (its nested-IF example had both defects — three string comparisons and an enum concatenated into a LOG message), create-microflow.md, and expressions.md, whose "unambiguous context" sentence now names the four accepted slots and the two that are CE0117. A sweep-and-replace over every `Status = '…'` would have "corrected" ten sites that build fine, including the XPath one where the string is the only correct form. The skill gains the same table, plus getCaption()/toString() for putting an enum into a string, and a note that check cannot catch either failing form (the type checker is still a proposal). No .fail.mdl is possible here, so the verdicts live in mdl-examples/bug-tests/enum-string-literal-contexts.mdl, pinning the accepted forms so a later fix does not widen into them. Control: adding it to the probe project left the error count unchanged at the two deliberate failures.
Serialize a required object-list TextTemplate with its shipped default, fixing CE0463 on authored Accordions (mendixlabs#891)
docs: triage mxcli-banking FINDINGS.md against current main
docs: settle contradictory enum documentation (CASE splits, string literals)
mxcli exec ran no semantic checks. Parse errors stopped it, but the ~24 Validate* checks lived inline in cmd_check.go with exactly one caller, so a script mxcli check rejected was applied by exec anyway. The mxcli-banking report hit this with a page whose invalid widget property was silently dropped -- a picker that renders and does nothing. 'Run check before exec' was a convention nothing enforced, on a tool built for unattended agent use. exec now runs the same checks first and refuses when any reports an error: nothing is written, because exec applies statements one at a time and cannot roll back, so a known-bad script would leave the model partly updated. Warnings print and do not block. --no-check opts out. The checks moved to executor.ValidateProgram rather than being copied, because a second copy would drift -- the same failure that produced the CONV010 and QUAL004 false positives and the duplicated settings overlays. mxcli check now calls it too, and its output is byte-identical across all 20 doctype-test scripts. A check wired into nothing is invisible: it reports no violations and reads as a clean project. TestValidateProgram_WiresEveryWholeProgramValidator therefore asserts structurally that every exported Validate*(prog *ast.Program, ...) is called from ValidateProgram, and fails when one is dropped. Verified on a real 11.13 project: a combobox with an unknown property is refused with nothing written, the same script under --no-check writes the page with the property silently dropped, and a warning-only script still applies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
An `OnChange:` authored on a checkbox, radiobuttons, combobox, dropdown, textarea or datepicker parsed cleanly, passed `mxcli check`, and executed with a success report — while the property was never written. In the browser the control changed state and issued zero /xas/ requests. Three independent drops, any one of which was sufficient: 1. The page builder read `w.GetOnChange()` in `buildTextBoxV3` only, so the property died between the AST and the model for the other five. All six now route through one `applyOnChangeV3` helper, so a widget added later cannot forget it. 2. The legacy writer hardcoded `serializeClientAction(nil)` for TextArea, DatePicker, CheckBox, RadioButtons and DropDown — a populated action would not have reached BSON under `--engine legacy` either. 3. For pluggable widgets `actionSourceForKey` matched only the bare keys `onClick`/`onChange`, but Mendix's Combobox names its slot `onChangeEvent`, so no action mapping was emitted at all. One Event/Action suffix is now stripped before matching — narrowly, so the Combobox's `onChangeFilterInputEvent` and `onChangeDatabaseEvent` (distinct properties with no MDL surface) stay unmapped rather than having one `OnChange:` write three actions. The shipped combobox.def.json overrides any .mpk-derived def, so the generator fix alone would never have reached a real combobox; the mapping is added there too, in both modes (modes are exclusive). Generator version bumped 14 → 15 so existing projects regenerate. DESCRIBE is wired at the same time for textarea / datepicker / checkbox / radiobuttons: DESCRIBE is how this class of drop gets found, and it could only see OnChange on textbox. (`dropdown` has no DESCRIBE case at all — a separate gap, left alone.) Control: reverting the combobox def alone fails TestCombobox_OnChangeMappedInBothModes with the reported symptom. Reported as FINDINGS #14 in ako/mxcli-owid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…endix version The presence gate added in #163 runs before the switch that rejects an unrecognised key, so a typo was answered with this project does not store the model setting NotARealSetting ... hint: set it in Studio Pro once, or upgrade the project which sends the reader after a version problem that does not exist. That is a regression against the message the key list was added for: a bare 'unknown model setting' had already sent the mxcli-banking app through three wrong guesses at the optimistic-locking property. An unknown key and a real key this version does not store are different problems with different fixes, and the presence check cannot tell them apart -- so check membership first. The version-specific message stays reachable for a real key, which the second test pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Mendix can run both a Call microflow and a Call Java action activity on a
task queue; it is a property of the call activity, not a separate
construct. MDL could express it on neither, so a queue could be created
and never bound to anything. The gen setters existed and nothing called
them.
CALL MICROFLOW Ops.ACT_Process(Order = $Order) IN QUEUE Ops.Work;
CALL JAVA ACTION Ops.RefreshData(Url = $Url) IN QUEUE Ops.Work;
The clause takes the same position on both — after the argument list,
before any ON ERROR — and DESCRIBE renders it back, so the binding
round-trips.
What is written is a Queues$QueueSettings child, NOT the call's sibling
`Queue` string. gen carries a `Queue` ByNameRef on both call types;
generated/metamodel (the arbiter) has neither, and a call carrying only
`Queue` is inert. Writing it would have produced a call that looks queued
and is not.
The rewrite guard changes shape rather than going away. A CREATE OR
REPLACE/MODIFY still rebuilds the microflow from the statement, so:
- a script that restates every stored queue is allowed — that is now
the normal way to edit a microflow with a queued call, and before
this clause existed there was nothing to restate it with;
- a script that drops one is refused, naming the clause that fixes it;
- a stored retry policy (Queues$QueueFixedRetry / ExponentialRetry) is
still refused outright — MDL has no syntax for it, so a rewrite
would reset it (guard-don't-drop, ADR-0005).
The restatement walk is reflective so a newly added statement nesting
cannot silently stop being searched.
`check --references` resolves the queue name, because the build-time
failure (CE1613) is reported against the call activity rather than the
script and is expensive to trace from a build log.
Verified end to end on mxbuild 11.13.0, from a project created by
`mxcli new`:
- queued CALL MICROFLOW and CALL JAVA ACTION: 0 errors;
- dropping the queue produces CE1613 naming both the queue and each
activity — which is the proof mxbuild resolves what we wrote, not
merely tolerates it;
- a queued Java action returning anything but Nothing is CE7038. mxcli
defaults CREATE JAVA ACTION to Boolean, so `returns void` is
required, not optional. Documented in the skill, the syntax topic and
the example.
Reported as FINDINGS #24 in ako/mxcli-owid, which proposed one setter
call apiece; that would have written the inert property.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
FINDINGS #48: no OQL — well-formed or not, true or not, against a real entity or an invented one — could make a @verify fail. It was parsed into the TestCase and read by nothing but --list. Same silent-absence defect as the dropped @expect, in the annotation covering the harder half of testing a Mendix app: most microflows are side effects, so asserting on the rows one wrote is the only way to test it. @verify now runs its OQL against the app after the microflow returns and keeps three outcomes apart — holds, does not hold (FAIL, with the value that came back), cannot be evaluated (ERROR, counted with the failures). Three things are enforced rather than left to trip you up, each learned by running it against a real app: * @cleanup rollback is refused. It is the default, and it undoes the test's writes before the query could see them, so the assertion would run against the pre-test state and report a confident wrong answer. * The result must be one row and one column. Picking a cell out of a table would be a guess, and a guess here is the same silent wrong answer being fixed. * The legacy after-startup runner refuses a suite that uses @verify: its tests run during boot, so there is no point at which to query the app. The expected value is split off at the last comparison operator outside quotes and parentheses, then required to be a literal — without that check the split silently eats a `where x = 1` and sends a truncated query. **The OQL path itself was broken, and the first real run is what found it.** Unit tests against a stubbed endpoint proved the logic and would have shipped it unusable. Two bugs, both in cmd/mxcli/docker/oql.go: * `mxcli oql` failed on every Mendix older than 11.11. The 11.11+ REST route /dev/preview_execute_oql does not exist there, and the admin API does not 404 it — it dispatches the POST as an ordinary admin request, finds no "action" field, and answers HTTP 200 with {"result":1, "message":"Action not found"}. The fallback to the legacy M2EE action keyed on the 404 alone, so the legacy action — which works fine on those runtimes — was never tried, and the user was told to upgrade mxcli. Measured on 11.6.6: before, no query ran at all; after, `select count(*) as n from Mod.E` returns a row. * A rejected query was reported as 0 rows. The legacy action puts the error inside the feedback with a **success** result code, so M2EEError() says nothing and the error body parsed as an empty result. When the dev route is absent the legacy action is the real attempt, so its error is the one reported — otherwise an unknown entity came back as "upgrade mxcli", which is neither true nor actionable. Verified end to end against a booted Mendix 11.6.6 app, seven canaries from the finding: a true @verify PASSes, two false ones FAIL with the observed count, and four unevaluatable ones ERROR — including the unknown entity now carrying the runtime's own message. Stubbing runVerifies to return early puts every canary back to a wrongly green PASS. Note for suite authors: Mendix OQL requires every selected column to have a name, so write `select count(*) as n`, not `select count(*)`. A bare one is an ERROR, not a pass. Repro: mdl-examples/bug-tests/expect-vacuous-assertions.test.mdl Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
Extract ValidateProgram to unify check and exec validation
Distinguish unknown model settings from version gaps
One conflict: cmd/mxcli/syntax/features_domain_model.go, where both sides rewrote the domain-model.entity.alter Syntax string. Resolved by taking main's text — it carries the corrected MODIFY ATTRIBUTE form (a type is always required) and the new DROP DEFAULT clause, and this branch's copy still showed the wrong `MODIFY ATTRIBUTE X SET DEFAULT v` spelling — and re-attaching this branch's RENAME ATTRIBUTE paragraph after it. Main's Example block is a superset of this branch's, so it is taken as-is. The generated ANTLR parser had to be regenerated for main's DROP DEFAULT rule (mdl/visitor references ctx.DEFAULT); `make build` does that. Also closes an unbalanced code fence in docs-site/src/language/alter-entity.md that arrived with main: the MODIFY ATTRIBUTE example was never closed, so the whole section down to the end of "Clearing a default" rendered as one code block. Not from this branch's work, but visible in a file it edits. Verified after the merge: make test and make lint pass, and both bug-test scripts still land 0 errors on mx check 11.13.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Fix attribute rename to update XPath constraints and entity rules
mxcli test: implement @verify, and fix the OQL path it runs on
…ge key Mapping the Combobox's `onChangeEvent` so that `OnChange:` reaches it had a side effect: `allowedWidgetProperties` adds both a mapping's PropertyKey and its Source, so `onChangeEvent:` became an accepted MDL property name. The engine never reads it — `resolveMapping` for an action mapping reads the fixed AST slot (`w.GetOnChange()`) — so the value would have been accepted by check and dropped on write. That is the same silent-drop class this branch set out to fix, one layer up. An `action` mapping is now authorable by its Source only (`Action`/`OnClick`/`OnChange`). Writing the storage key is an error that names the spelling that works, rather than a generic "unknown property". Caught by TestValidateProgram_ReportsWidgetPropertyError, which lands on main from #165 and uses `onChangeEvent` as its example of a property a widget does not have. It was right. Also fixes the OnChange repro script, which did not build: its data view sourced a microflow that takes a parameter and returns nothing (CE1571 + CE0552), and bound radiobuttons/combobox to a String (CE2421 + CE7247). Now 0 errors on mxbuild 11.13.0, with all six controls' actions stored — the page unit carries six Forms$MicroflowAction nodes. DESCRIBE shows five of them; the pluggable path does not read action slots, which is a separate pre-existing gap and is stated as such in the script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
FINDINGS #47: running the test suite dirties the project file. `git status` reports the `.mpr` modified after a run that changed nothing, so a "run the tests, then assert the tree is clean" CI step fails, and a pull request carries a diff nobody can read — a `.mpr` is a SQLite database, and there is no cheap way to tell a bookkeeping GUID from a real model edit. The runner injects an MxTest module, builds, runs, and takes it back out. That restore is very nearly perfect: `mprcontents/` comes back byte-identical. The `.mpr` does not, for two reasons rather than the one the report identified. Every unit write stamps a fresh UUID into `_Transaction.LastTransactionID`, and the insert/delete cycle also relays SQLite's pages. Restoring the row alone is measurably insufficient — three consecutive runs then hold the id stable and still produce a different file hash every time, which is how that first attempt was rejected. So the whole file is snapshotted before injection and written back after a successful cleanup: byte-exact by construction rather than by enumerating what might have changed. Two refusals are load-bearing. The restore is declined when cleanup failed, and when the `mprcontents/` tree changed during the run. In both cases the project is not in the state the snapshot describes, and putting the old file back would turn a visible, harmless discrepancy into an invisible, misleading one — a tree that reads as clean while the model is not. The write goes through a temp file and a rename, so an interrupted restore cannot truncate the `.mpr`; that would be far worse than the cosmetic diff being fixed. Verified with the reporter's own harness on Mendix 11.6.6 — three consecutive runs against an untouched copy: state = mpr-hash / mprcontents-hash initial 5c787c16e544 0f4a84f6b489 run 1 5c787c16e544 0f4a84f6b489 run 2 5c787c16e544 0f4a84f6b489 run 3 5c787c16e544 0f4a84f6b489 Before, the `.mpr` hash differed on every run. Stubbing restore() to return early puts the snapshot tests back to failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
FINDINGS #45: the watch loop can deploy a half-applied model, and there is no way to recover from it. `watchAndApply` rebuilt on the first mtime bump. An `mxcli exec` of a real script rewrites the `.mpr` and many `mprcontents/*.mxunit` files over several seconds, so the build snapshots the tree mid-write. There was a settle for the web bundler and nothing for the model. On its own that was survivable — you noticed, re-ran the script, and the second build was complete. Byte-idempotent `exec` closed that escape hatch: re-running an already-applied script writes nothing, so nothing re-triggers the watcher and the stale build stands, with no way out the tool offers. Two behaviours each correct alone, composing into a state you cannot leave. The watcher now waits for the source mtime to stop advancing — two poll intervals of quiet — before it builds. A long exec produces one build of the finished model instead of a build of the first file it touched, and a single editor save costs one extra poll. The wait is unbounded on purpose: a long exec is the case it exists for, and building late beats building mid-write. It still selects on the interrupt channel, so Ctrl-C during a write is not swallowed. `touch` on the `.mpr` remains the force-rebuild hatch, now documented rather than folklore. The generalisable half, recorded in the symptom table: a change *signal* is not a change *event* — anything polling mtime samples a writer mid-flight unless it debounces — and an optimisation that skips work should prompt the question of what informal recovery procedure depended on that work happening. Note on the test: the first version asserted the written-file count after wg.Wait(), which holds against a settleSource that returns immediately. It reads the writer's state at the moment settleSource returned instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…ociation storage key Two follow-ups on top of the action-slot fix, both measured against a real Mendix 11.13 project. 1. DESCRIBE could not see a ComboBox's OnChange, so describe -> exec DELETED it. The write was already correct -- the action reaches BSON as Forms$MicroflowAction -- but DESCRIBE read only the built-in OnChangeAction slot, which a pluggable widget does not use. It emitted `combobox cbo (Label: 'X')`, and re-executing that output wrote a widget with no action: 1 Forms$MicroflowAction before the round trip, 0 after, while the datepicker beside it survived. Read via customWidgetPropertyActionMap + renderClientActionMDL, the shape DataGrid2's onClick already used. All six controls now survive the round trip and the page's own Verify recipe can be completed. 2. `attributeAssociation:` is the same silent drop as `onChangeEvent:` -- the association operation also resolves from a fixed AST accessor -- so the storage-key exclusion covers it too. The tempting generalisation is wrong and worth recording: excluding every storage key whose mapping has a Source rejects working syntax, because `optionsSourceAssociationCaptionAttribute:` DOES persist. The list is two operations because that is what the written documents show; a test pins it so the next addition has to be measured rather than reasoned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Add IN QUEUE clause for queued calls and fix OnChange on input widgets
Test runs leave the project byte-identical; --watch waits for writes to finish
FINDINGS #20: a reader grepped MISSING_CAPABILITIES.md and MDL_FEATURE_MATRIX.md, found the External Database Connector recorded as unsupported, and designed a whole workaround around a blocker that CREATE DATABASE CONNECTION had already removed. The docs were wrong about far more than that one row. Verified each claim against the grammar's statement rules and `mxcli syntax` rather than against memory. Of the 13 document types MISSING_CAPABILITIES.md lists as unsupported, 11 now ship. The feature matrix's "Not Yet Implemented" table listed nine implemented features, several with a full row of N. - MDL_FEATURE_MATRIX.md: the section now means what its heading says. Genuinely unimplemented types (microflow rules — not to be confused with the supported CREATE VALIDATION RULE — message definitions, XML schemas, SOAP, data importer, building blocks, extensions, widget packages, system texts) are listed on their own; partial ones are separated out; the fifteen implemented ones move into Core Document Types rather than being deleted. - MISSING_CAPABILITIES.md keeps its measurement — how many documents of each type real apps contain, which has not changed — and gains a Status column, a per-section status line, and a header saying plainly that it is a dated survey and not the current status. Its execution plan is marked done. A `database-connection` syntax topic now exists. Its absence is what made the gap look real: `mxcli syntax` had SHOW DATABASE CONNECTIONS and nothing about CREATE. It documents three traps, all measured on mxbuild 11.13.0, and its example runs to 0 errors on a fresh project: 1. connection string / username / password must reference constants, not literals (a literal makes the project unopenable; mxbuild stays green); 2. username and password are required even when the driver needs neither; 3. a named type needs its JDBC driver declared via ALTER MODULE ... ADD JAR DEPENDENCY, or the build fails CE5278. BYOD skips that check. Two real bugs found while verifying, rather than assumed: - The modelsdk (default) engine never read a query's TableMappings, so `describe database connection` emitted a query with no `returns`/`map (…)` and a describe → exec round trip dropped the entity binding and the column mapping. The renderer was dead code on that engine. The round-trip test existed but was pinned to the legacy backend, so it stayed green while the default engine was lossy; it now runs on both, and fails on modelsdk without this fix. - `CREATE NON_PERSISTENT ENTITY` does not parse — the token is hyphenated (`NON-PERSISTENT`). It was documented in two syntax topics, a skill, and an LSP completion snippet, which inserted unparseable MDL on selection. A drift guard pairs each capability with the syntax topic that proves it ships, so the matrix cannot re-assert a filled gap; controls confirm both guards fail when the exact #20 claim is reintroduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…nst mxcli The previous pass enumerated the feature matrix by inverting what mxcli supports. That can only ever rediscover gaps someone already wrote down. Enumerating Studio Pro's own `Add other` menu instead — the full set of document types the product offers — is the first check able to find a type nobody has thought about, and it found four: data sets, page templates, ML model mappings, change data capture (beta) None appeared in any mxcli document before. All 37 types were probed against the grammar's statement rules, `mxcli syntax`, and a live project. Two corrections in the other direction: - Building blocks are read-only (`show building blocks`), not absent as the table claimed. - Data transformers are fully authorable (createDataTransformerStatement, `mxcli syntax data-transformer`) and were listed nowhere at all — neither as supported nor as a gap. Counted from the menu: 24 of 37 types are authorable, 4 are read-only (layouts, icon collections, building blocks, plus pluggable widgets on the page side), and 12 have no MDL surface. The note now records where the row list came from, because that is the part that decays: values can be re-derived from the code, but a missing row is invisible until someone enumerates the product again. Re-run the pass when onboarding a new Mendix major. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…ilence
`mxcli -c "show page Mod.Home"` exited 0, printed nothing, and did nothing.
That is the worst available failure shape: it is indistinguishable from "the
page is empty", so it reads as a fact about the project rather than as an
unimplemented command. A parse error would have been strictly better.
The cause is not a handler bug. `showOrList PAGE qualifiedName` exists in the
grammar, but ExitShowStatement has no branch for it, so the visitor appends no
statement and the program runs zero statements.
Probing every showStatement alternative through the real parser found three in
that state, not one:
show page Mod.Name -> now an alias for DESCRIBE PAGE, matching how
SHOW ENTITY / SHOW ASSOCIATION read as their describe.
A missing page now reports "page not found".
show connections -> now lists the external SQL connections open in this
session (sql.Manager.List, which already existed).
Distinct from SHOW DATABASE CONNECTIONS, which lists
stored DatabaseConnector documents.
show notebooks -> grammar alternative removed. The notebook grammar has
no AST, visitor, executor or backend behind it, so
there is nothing to wire; it now fails loudly like any
other unsupported noun.
A structural grep cannot find this class. `ctx.PAGE()` DOES appear in
ExitShowStatement — for the unrelated SHOW ACCESS ON PAGE branch — so searching
for the token reports the broken alternative as covered. The guard therefore
synthesizes a probe from each grammar alternative (dropping optional groups,
which nest) and asserts a statement comes out. It covers 81 of 82 alternatives
and fails if that coverage drops.
The guard immediately earned itself: the first version of the SHOW CONNECTIONS
branch swallowed SHOW DATABASE CONNECTIONS, which also matches CONNECTIONS(),
and answered a stored-document query with live session state. Guarded with
ctx.DATABASE() == nil.
Control: reverting the visitor branch makes the guard fail on `show page`.
Still open, reported rather than swept: `create notebook X begin end` is the
same silent no-op, and the whole notebook grammar (createNotebookStatement,
notebookOptions, notebookPage, alterNotebookAction) is dead — no Go code
references any of it. Deleting a subsystem someone may be part-way through
building is the maintainer's call, not a side effect of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
All four notebook statements were silent no-ops: `create notebook X begin end`, `alter notebook X add page Y`, `drop notebook X` and `show notebooks` each parsed cleanly, exited 0, printed nothing and wrote nothing. No AST node, visitor branch, executor handler or backend method existed for any of them — `grep -ri notebook` over mdl/ast, mdl/visitor, mdl/executor, mdl/backend, sdk and modelsdk returns nothing. Dead grammar is not inert. It accepts input and discards it, which is the one outcome worse than rejecting it: `create notebook` looked like it worked. Removed the statements (createNotebookStatement, ALTER NOTEBOOK, DROP NOTEBOOK), their sub-rules (notebookOptions, notebookOption, notebookPage, alterNotebookAction), and the NOTEBOOK/NOTEBOOKS lexer tokens plus their identifierOrKeyword entries. All four now fail as parse errors. Dropping the tokens also frees `notebook` as an ordinary identifier — one fewer reserved word, verified with an entity and attribute of that name. Control for a grammar change this broad: every mdl-examples script was swept through `mxcli check` before and after and the failure SETS diffed. 15 fail either way — all pre-existing — so the count alone would have proved nothing; the set difference is empty. Neighbouring commands (`show pages`, `show page X`, `show connections`, `show database connections`) verified unchanged against a real project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…sizes `RETURNS T AS $Var` names the return variable, and buildFlowGraph sets the final EndEvent's ReturnValue to "$"+Var whenever the AS clause is present — so the return exists whether or not the body spells one out. That is the point of the clause. MDL003 demanded an explicit RETURN on top of it and flagged the documented idiom as broken. It stopped being cosmetic when `exec` began refusing any script whose checks report an error: MDL003 is an error, so seven shipped mdl-examples scripts could no longer run at all without --no-check. The check now skips when returnType.Variable != "", mirroring the builder's own condition rather than inventing a second rule. A companion test keeps the no-AS-clause case failing, so simply disabling the rule would not pass. Evidence the check was wrong rather than the scripts, all on mxbuild 11.13.0: the flagged microflow builds with 0 errors, `describe microflow` renders the synthesized `return $Found;`, and the stored EndEvent carries a ReturnValue. End to end, 343-list-attribute-find-filter.mdl now execs without --no-check and the project checks clean. Control: reverting the one-line condition fails the new test with the reported symptom. The example sweep goes 15 -> 5. The remaining five are not this bug and are left alone: 552-npe-reserved-words.mdl fails on purpose (it is the regression test for reserved-word detection), and four scripts have real defects of their own — three declare a CreatedDate attribute where the AutoCreatedDate pseudo-type is required, and 116-datagrid2 uses a `SET WIDGET x.y (...)` form that has never existed in the grammar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
FINDINGS #25 reported the queue-binding data loss as fixed. It was fixed on one engine. Under `--engine legacy`, `CREATE OR REPLACE MICROFLOW` still silently destroyed a task-queue binding — the exact loss checkNoQueuedCalls exists to prevent, and with the same misleading signature: `mx check` goes quiet afterwards because the configuration its CE1613 referred to is gone. The cause is a shape difference, not a logic error. The two readers return different Go types for the same BSON: modelsdk yields []interface{} for arrays, the legacy mpr reader yields bson.A. bson.A is a NAMED slice type, so `case []any:` does not match it, and the walk never descended into ObjectCollection.Objects — it found no queued calls and allowed the rewrite. Both walks now handle bson.A. Also read QueueSettings back in the legacy parser. It wrote the binding correctly but never parsed it, so on that engine `describe microflow` rendered a queued call as an ordinary one and a describe → exec cycle dropped it. The engines now agree on the same document, verified side by side. Pre-existing, not a regression from the IN QUEUE work: the binary built at 4f893ce (before that merge) drops the binding under legacy too. But my commit claimed the guard protected rewrites without qualifying by engine, and it did not. Verified live on 11.13, seeded binding then rewritten: before legacy dropping rewrite -> "Replaced microflow", binding gone after legacy dropping rewrite -> refused, binding intact legacy restating rewrite -> succeeds, binding intact describe agrees across both engines The same []any-only assumption remains in mdl/backend/mcp/page.go, page_mutator.go and widget_sync_apply.go; noted in the symptom table rather than changed here, since each needs its own reachability check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Close two silent data-loss paths, and stop the docs claiming shipped features are missing
NOTEBOOK/NOTEBOOKS appear nowhere in the grammar -- not the lexer, not the parser, not the domain grammars -- so the committed completion list offers two keywords the parser would reject. `make build` regenerates the file and removes them, which left a clean checkout dirty. Same class as the FIRST drift fixed earlier, in the other direction: the keyword left the grammar and the generated list was never refreshed. Regeneration is stable -- running it twice produces no further change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…ead on the default engine Two bugs found by asking whether the External Database Connector is fully supported on the default engine, and testing the full surface on both rather than answering from what was last changed. It was not. 1. A query written with a $$…$$ dollar-quoted SQL AND a `parameter … default '…'` stored the DEFAULT AS THE QUERY BODY. `describe` rendered `sql 'sales'`, the SELECT text was absent from the BSON entirely, and mx check reported 0 errors on both engines — nothing surfaced it. buildDatabaseConnection took STRING_LITERAL(0) as the SQL before falling back to DOLLAR_STRING. When the SQL is a dollar string, STRING_LITERAL(0) is the first parameter's DEFAULT. The parameter-index logic ten lines below already compensates for exactly this; the SQL extraction did not. Checking DOLLAR_STRING first fixes it. Isolated by bisecting the clause combination: `parameter d: String` alone keeps the SQL, `parameter d: String default 'x'` destroys it. 2. The modelsdk (default) engine read back neither a query's Parameters nor — until #171 — its TableMappings, so `describe database connection` dropped the parameters, the return entity and the column mapping, and a describe → exec round trip silently produced a query without them. The renderer handles all three; it only ever saw empty slices. #171 fixed TableMappings and missed its sibling Parameters, which is the characteristic version of this mistake. Verified with a cross-engine DESCRIBE matrix — write with engine A, read with engine B, all four combinations — because a gap on one engine is invisible from inside that engine. All four now agree on sql/returns/map/parameter, a describe → exec round trip is lossless, and mx check is 0 errors on 11.13.0. Controls: reverting the visitor ordering fails the new test with `SQL = "sales"`; the round-trip test runs on both engines and fails on modelsdk without the reader change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…mendixlabs#884) LOOP and WHILE containers took their Size from measureStatementsSpan, a pre-pass over the AST run BEFORE the body was built, so the box was a function of statement COUNT alone. Varying only the children's positions changed nothing: children at x=150/310 -> 480;160 children at x=1500/2000 -> 480;160 (both children OUTSIDE the box) children at x=160/170 -> 480;160 four children, default -> 800;160 The model carries no geometry rules, so mx check reports nothing and only the eye catches it. An explicit @position on a loop child moved the child but not the container. The box is now sized after the body exists, from the real child bounding box (Position +/- Size/2). Same four cases now measure 420;160, 2110;140, 280;140 and 740;160 -- each tracking its contents, and case B grown to contain children it previously excluded. Nesting needed no extra code: an inner loop is sized when its own addLoopStatement returns, so the outer container measures a correct inner box (verified: inner 580;160 inside outer 1320;260). The proposal called for build-first / size-after / TRANSLATE-once. The translation step is deliberately dropped. A child's position round-trips through DESCRIBE as @position, so translating the body would make a describe->exec cycle store different coordinates than it read, and under ADR-0008 that turns an otherwise-quiet re-run into a write. Growing the box achieves the same containment without moving anything the author placed -- which is what a hand-laid-out loop needs. The proposal records this divergence. Verified: a hand-laid-out loop round-trips byte-identically; re-running a script is a no-op, with the MXCLI_ALWAYS_WRITE=1 control confirming the comparison detects writes; mx check clean on both fixtures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
External DB connector: SQL replaced by a parameter default, and a lossy read on the default engine
Both bundled rules matched on vocabulary the catalog never emits, so both produced false positives on ordinary, correct Mendix patterns. CONV010's ALLOWED_ACTIONS held the Mendix storage names (ShowFormAction, CloseFormAction) while the catalog labels an action with its SDK name, derived from the parsed Go type in getMicroflowActionType -- ShowPageAction, ClosePageAction, MicroflowCallAction. The allowlist matched nothing, so every ACT_ microflow that showed a page, closed one, or called a sub-microflow was flagged. In the banking-app report that was 11 false positives out of 13 findings, which buried the 2 real ones. QUAL004 counted only the 'call' and 'schedule' reference kinds. The builder also emits 'datasource', 'action' and 'calculate' for microflows and 'home_page', 'login_page', 'menu_item' for pages, so a microflow used as a page data source was reported as 'not called from anywhere'. The page half was masked by ENTRY_PAGE_PATTERNS, which happens to cover the pages most likely to be navigation targets. The guard is the point: mdl/catalog/lint_rule_vocabulary_test.go pins CONV010's allowlist to what getMicroflowActionType actually returns, and QUAL004's two kind lists to the RefKind constants, so a rule cannot drift from the vocabulary it matches on again. Both tests fail against the shipped rules with the reported symptom. Verified on a real 11.13 project: an ACT_ microflow doing show-page plus a sub-call and a DS_ microflow behind a datagrid are no longer flagged, while a retrieve in an ACT_ microflow and a genuinely uncalled microflow still are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
The Mendix model carries no geometry rules, so a loop whose children sit outside its box passes mx check with zero errors and builds and runs normally -- it is simply drawn wrong when the flow is opened. That is what let mendixlabs#884 problem 1 survive unnoticed: the container's Size came from a statement count, so children at x=1500/2000 lived in a 480-wide box and nothing said so. The sizing fix stops mxcli producing that, but the condition can still arrive from a project written by an older mxcli, hand-edited in Studio Pro, or a future layout regression. MPR011 catches it either way. Checked in each container's OWN coordinate space, so a nested loop is never compared against an ancestor's canvas -- the same distinction MPR008 needed after it was found comparing a loop child against absolute coordinates. An unpositioned child at the origin is skipped rather than flagged, matching MPR008 and keeping the real cases visible. Verified with a pre-fix control binary: on a project it built, MPR011 reports both escaped children while mx check reports nothing beyond the blank app's pre-existing error. Silent on the same script through the fixed binary, on the stock blank app, and on a nested-loop project. The ID is MPR011, not the MPR009 the proposal named -- MPR009 and MPR010 were already taken (gallery selection listener, dataview layout grid). The proposal is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
FINDINGS #39 (second half) reports that `ORDER BY` on a DateTime is ignored by the Mendix runtime: `order by DealtAt desc limit 5` returns old rows while `order by Id desc` returns genuinely new ones, stably across runs. The recommended workaround is to order by `Id` for recency. Measured on Mendix 11.13.0 against PostgreSQL with statement logging on, that diagnosis is wrong — in a way worth writing down, because the evidence for it is genuinely convincing. With four rows carrying distinct non-null timestamps, the SQL the runtime sends is: SELECT "myfirstmodule$probe"."label" AS "label" FROM "myfirstmodule$probe" ORDER BY "myfirstmodule$probe"."dealtat" DESC LIMIT $1 The ordering is present, on the right column, before the LIMIT, and the result comes back newest-first. It is not dropped, and mxcli is not in the path either: ExecuteOQL passes the query verbatim and parseOQLFeedback preserves row order. Adding two rows whose DealtAt was never set reproduces the report exactly. Mendix emits no null placement, so the database default applies, and on PostgreSQL DESC means NULLS FIRST: order by DealtAt desc limit 3 -> null-a, null-b, newest Running Mendix's own emitted SQL by hand confirms the mechanism: identical with the default, correct with NULLS LAST. The trap is that the wrong answer is *stable*, so it reads as "the engine is ignoring my ORDER BY" rather than "my sort key is empty for some rows". The report anticipates the tie objection and answers it with "stable across runs, so it is not a tie-break artifact" — but stability rules out randomness, not ties. A degenerate sort key is exactly as repeatable as a correct one. Documented in the OQL page and the query-writing skill: use NULLS LAST on an optional attribute rather than falling back to `order by id`, which answers a different question (insertion order, matching recency only when nothing backdates a row). Null placement is database-specific, so being explicit is also the portable choice. No code change — mxcli is not in the path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
cmd/mxcli/lsp_completions_gen.go is generated by `make build` from mdl/grammar/MDLLexer.g4 and is tracked, but the committed copy still carried NOTEBOOK and NOTEBOOKS, which the lexer no longer defines. Every `make build` therefore regenerated the file and left the working tree dirty with a change nobody made. No behaviour change: the two entries were completions the LSP offered for keywords the parser would reject. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…twin Studio Pro can "Exclude from project" any document, and Mendix then allows a second document of the SAME NAME in the same module. Measured on 11.13.0 with two microflows named MyFirstModule.Calc: one of them Excluded -> mx check: 0 errors both active -> [error] CE0122 "Duplicate document name" So exclusion is exactly what makes the duplicate legal, and mxcli got both consequences wrong. Wrong target: every by-name lookup took the first match, so which document it resolved to depended on enumeration order. `create or replace` edited the excluded twin while the live microflow silently kept its old body, and `describe` printed a body the app does not run. Dropped flag: every rebuild wrote Excluded from the AST default (false) rather than carrying the stored value, so the excluded twin became active. One `create or replace` took the probe project from 0 errors to CE0122 — unbuildable, from a statement that never mentioned exclusion. Fixed for every document type that carries the flag. Lookups route through a single selector (mdl/executor/excluded_docs.go: pickLive — live match wins, an all-excluded set still resolves so lookups do not start reporting "not found"), and each create path carries the stored flag next to the ID and roles it already preserved. `@excluded` already existed and round-trips through DESCRIBE, so an absent annotation now means "the script does not say", never "make it active". Four types had no Excluded field to carry at all — model.Enumeration, types.JavaAction, types.ImageCollection, pages.Snippet — so the field is added and populated in both engines' readers, and the backends that hardcoded SetExcluded(false) (enumeration, snippet, image collection) now write the stored value. Pages and snippets additionally collected ALL name matches and deleted the extras, destroying the excluded twin outright; the replace set is now the live documents only. Measured, not read: queues looked correct — reader and writer both carry the flag — and still dropped it, because the executor built a fresh struct. A matrix run over nine authored document types found it. All nine now survive a re-run of an unchanged script, and the original repro rewrites the live microflow at 0 errors. Tests fail with the reported symptoms when the fix is reverted.
A describe -> exec round-trip moved the start. A Studio-Pro-authored flow whose StartEvent sat at 145;200 came back at 100;200, while every other coordinate in it -- five activities, a split and two end events -- survived exactly. It was the one piece of a hand-laid-out microflow mxcli could not preserve. The start has no MDL statement to annotate and DESCRIBE cannot emit its position, so the builder always derives one: (first annotated activity X) minus one spacing unit, 260 - 160 = 100. Studio Pro's 145 is not derivable from anything in the description. Carried over from the microflow being replaced instead, the way the folder and the allowed module roles already are on CREATE OR MODIFY. Nil on a fresh CREATE, which keeps the existing derivation, so only a rebuild of an existing flow changes. Reading the stored flow is best-effort: a backend that cannot answer yields the derived placement rather than failing the statement. Verified on the full coordinate set rather than the start alone -- every RelativeMiddlePoint in Administration.SaveNewAccount dumped before and after, diff empty. Control with the carry-over stubbed reproduces the 100;200 drift. Re-running the round-trip script is a no-op, with the MXCLI_ALWAYS_WRITE=1 control confirming the comparison detects writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
docs(oql): ORDER BY DESC on a nullable column puts the nulls first
fix(lint): stop CONV010 and QUAL004 reporting correct code as violations
chore(lsp): regenerate the completion keywords from the current lexer
fix(executor): keep excluded documents excluded, and target the live twin
Loop layout: size boxes from their contents (mendixlabs#884), add MPR011, keep a replaced StartEvent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs
ako/mxcli:mainintomendixlabs/mxcli:main.59 commits (35 non-merge, 24 merges), 192 files changed, +10905 −771.
Range is a clean fast-forward —
git merge-base --is-ancestorholds, no rebaseor squash divergence.
Most of this came out of triaging two real applications' findings ledgers
(
ako/mxcli-owid,ako/mxcli-banking) against measurement on mxbuild 11.13,so several entries below are corrections to things mxcli reported as working.
Silent data loss — a write that succeeded and dropped the input
Each of these passed
mxcli check, reported success onexec, and produced aproject
mx checkcalled clean. That is the whole problem: nothing downstreamsignalled the loss.
OnChange:was written only ontextbox. The other five input widgetsnever read the property; the legacy writer hardcoded
serializeClientAction(nil)for them; and for pluggable widgets the action-slot matcher missed Combobox's
onChangeEventspelling. Three independently sufficient layers. In the browserthe control produced zero
/xas/requests — no server round-trip at all.describe → edit → exec cycle deleted it (measured: 1
Forms$MicroflowActionbefore, 0 after).
query written as
$…$,STRING_LITERAL(0)is not the query — it is the firstparameter's
DEFAULT '…'. Also a lossy read of query parameters on the defaultengine, where the renderer was dead code.
--engine legacy. The legacyreader returns
bson.A, a named slice type, socase []anysilently did notmatch and the walk never descended into the document. The guard existed and
protected nothing on that engine.
re-run of an unchanged script).
(ALTER PAGE cannot address widgets inside a pluggable widget, and DataGrid2 column edits are silently destructive #891).
New authoring surface
IN QUEUEonCALL MICROFLOW/CALL JAVA ACTION. Binding a call to atask queue was previously unauthorable, which is why rewriting a microflow that
had one had to be refused outright (guard-don't-drop, ADR-0005). The refusal is
now narrow: restating every stored queue is allowed; dropping one, or a retry
policy MDL cannot express, still refuses.
RENAME ATTRIBUTEupdates cross-references, including rewriting XPathconstraints (SET DEFAULT NULL rewrites an attribute's type (Integer becomes Enumeration), and RENAME ATTRIBUTE does not update microflow/page references #910).
versions; optimistic locking is settable from MDL.
MODIFY ATTRIBUTErejects an unknown type;DROP DEFAULTadded.Commands that exited 0 without doing anything
show page Xexited silently — a grammar alternative with no visitorbranch. Now aliased to
DESCRIBE PAGE, with a coverage test that walks thegrammar's
showalternatives so a new one cannot be added without a branch.MDL003demanded aRETURNthe builder synthesizes, failing correctscripts.
Source, not its storage key — mappingthe storage key made
onChangeEvent:accepted MDL that nothing ever read.execnow refuses a script whose own checks report an error instead ofrunning it.
Lint
Test runner and warm loop
@verifyimplemented (it had been parsed and ignored), and the OQL path itruns on fixed.
mxcli testleaves the project byte-identical after a run.run --local --watchwaits for a write to finish before building.Docs corrected against measurement
The failure being fixed here is a document that keeps asserting a gap after the
gap was filled — a reader greps it, believes the feature is unavailable, and
builds a workaround around a blocker that no longer exists. The External Database
Connector had been listed unsupported long after
CREATE DATABASE CONNECTIONshipped.
list, not against mxcli's — auditing against yourself cannot find a missing
document type.
mxcli syntaxtopic, so the matrix cannot re-claim a shipped feature is missing.
MISSING_CAPABILITIES.mdis marked as a dated survey (Mendix 11.6.3) aheadof its first table, with every row carrying an explicit status.
an enum may be a string literal is pinned per context, as measured.
ORDER BY … DESCon a nullable column puts nulls first (documented).Cleanup
completions that still advertised
NOTEBOOK).