Sync ako/mxcli: OData complex types, project-language DESCRIBE, modelling defaults and syntax coverage - #1134
Conversation
CREATE OR MODIFY EXTERNAL ENTITIES FROM dropped every property whose type was an OData ComplexType. The entity imported with only its key, `exec` reported "1 created, 0 failed" and printed nothing, and `mx check` said 0 errors — the loss surfaced later as CE1613 on a page written against the attributes Studio Pro would have made (mendixlabs#1118). mdl/types/edmx.go never parsed <ComplexType> at all, so a property typed `Shared.Uom.Quantity` was indistinguishable from one of an unknown type and fell through createExternalEntities' `!strings.HasPrefix(p.Type, "Edm.")` drop. The report says "only cross-namespace"; in fact every complex type was dropped, since nothing named `Edm.*` is complex. Flatten as Studio Pro does: one attribute per leaf, named `<property>_<leaf>`, read over the OData path `<property>/<leaf>`. Complex types resolve by QUALIFIED name — one document may declare `Quantity` in two namespaces, and a short-name lookup would hand over the other schema's properties. Measured on mxbuild 11.12.1, three copies of one project: RemoteName 'MaxQty/UoMNId' -> The app contains: 0 errors. RemoteName 'MaxQty_UoMNId' -> 4x CE6615 "does not exist in the OData service" RemoteName 'MaxQty_ZZNOTAPATH_…' -> 4x CE6615 so mxbuild resolves the path into the complex type and genuinely validates it. The pre-fix control — attributes simply absent — is 0 errors, which is why the drop was invisible. Flattened attributes are written read-only. Against a contract annotated Insertable=true AND Updatable=true, Mendix still reports them Creatable=False/Updatable=False, so following the entity set costs 2x CE6630 per attribute; this matches Mendix's documented "can only be read or deleted". Whatever still cannot be mapped — a complex type nested in a complex type, an unresolvable type, Edm.Duration — is now named with a reason instead of vanishing. DESCRIBE CONTRACT ENTITY flattens too, replacing the `String(200)` catch-all that masked the complex property in both its output formats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FYdKPGnoWiq2VgNF6ZfX1
Reported as mendixlabs#1113: "Every value comes back with an empty caption: `MyValue ''`, regardless of what's configured in Studio Pro." A caption is a Texts$Text, and Mendix has no language-neutral text — every Texts$Translation carries a LanguageCode. The read asked for a hardcoded "en_US", so on a project whose DefaultLanguageCode is anything else the lookup missed and every caption rendered as ''. The write side already resolves the project's language (mendixlabs#970) and the widget read side was fixed for the same reason (mendixlabs#702); the enumeration read was the site neither sweep reached. Reproduced end to end on an nl_NL copy of testdata/expr-checker, with the captions plainly present in the stored unit (`Texts$Translation LanguageCode nl_NL Text Nieuw`): create or modify enumeration MyFirstModule.OrderStatus ( Fresh '', Shipped '', Cancelled '' ); That output is also destructive, which is the reported impact: fed back through exec it reported "Modified enumeration" and left the stored translations empty. After the fix the same round trip reports "Unchanged enumeration" and the unit is byte-identical. - describeEnumeration and the diff renderer read the project's language (describeDefaultLanguage), falling back to en_US and then to any stored translation, so a caption Studio Pro shows is never reported as empty. - pickTextTranslation's last-resort fallback now sorts by language code. Ranging the map returned a different language per run, which made DESCRIBE output undiffable on a multi-language project — a defect a single-language repro cannot show. - The catalog's ENUMERATION_VALUES.Caption column follows the same order, so the catalog and DESCRIBE no longer disagree about the same value. CATALOG.ENUMERATIONS is unchanged: it is the enumeration-level table and never had a caption column — the captions are in CATALOG.ENUMERATION_VALUES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017m5tisvk1c9D3gZGsNws5A
The enumeration caption fixed in the previous commit was one call site of
a pattern the grep finds everywhere: `GetTranslation("en_US")`, or an
en_US lookup followed by `for _, t := range …Translations { break }`.
Applied consistently, per the review checklist, so the next report is not
the same bug wearing a different doctype. There are now no hardcoded
en_US reads left in mdl/executor.
Two distinct defects were behind the one pattern:
- Sites with no fallback dropped the text entirely. Measured on the same
nl_NL project, pre-sweep binary vs post, `describe entity` silently
loses the validation feedback:
Referentie: String(50) not null
against
Referentie: String(50) not null error 'Referentie is verplicht'
- Sites with a bare map-range fallback returned a different language per
run on a multi-language project. They were never empty, so the damage
is non-deterministic DESCRIBE output rather than loss — invisible to a
single-language repro, and it makes describe output undiffable.
All of them now go through pickTextTranslation: project language, then
en_US, then the non-empty translation with the lowest language code. An
en_US project is unaffected at every site.
Mermaid/ELK diagram details take the language as a parameter — two hops
from renderFlowMermaid and three from emitMicroflowELK — rather than
reading a package global.
Control: reverting the describe-entity site alone (with `_ = lang` to
keep it compiling) makes TestDescribeEntity_ValidationMessageFollows
ProjectLanguage_Issue1113 fail with the feedback message absent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017m5tisvk1c9D3gZGsNws5A
Both are first-class in Mendix and both get reinvented in microflows by
default, because the microflow version works: it passes `check`, it
builds, and nothing flags it. No command can prompt for either — `lint`
cannot know a Status attribute is standing in for a state machine — so
the instruction has to be in the guidance or it reaches an agent only
when the user already knew to ask.
- A business process with human steps is a WORKFLOW, not a status
attribute plus microflows: the state machine, the user-task inbox,
assignment and targeting, timers and boundary events, and a
definition the business can read are otherwise all hand-written, and
the process stops being inspectable.
- An aggregation is a VIEW ENTITY (OQL the database executes, Mendix
10.18+), not a microflow that retrieves every row to produce one
number — which gets slower exactly as the app succeeds.
Stated in all three descriptions of the procedure, as the gates already
are:
- generated CLAUDE.md/AGENTS.md, under "Conventions no command will
tell you" (5,578 bytes, within the 6,000-byte budget);
- bootstrap-app, as two interview follow-ups on Q4 and a section in
the model proposal, where the choice is actually made;
- the bootstrap-prompt docs page.
Pointers verified rather than assumed: `mxcli syntax workflow` exists,
but there is no view-entity topic in the syntax registry (`syntax oql`
is runtime queries), so view entities point at the `write-oql-queries`
skill.
TestModellingDefaultsAreStatedEverywhere holds the three together.
Control: replacing "view entity" in the docs page fails it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK2imrM8M5UbzhTvttb5YC
Two gaps found while wiring the modelling defaults, one real and one
not.
VIEW ENTITY had no `mxcli syntax` topic at all — the registry knew the
words only as a keyword on `errors.execution`. An agent told to prefer a
view entity over a retrieve-and-count microflow, and told to ask the
tool rather than this repo's docs, got nothing back. Three topics now,
under domain-model:
domain-model.view-entity what it is, when to reach for it,
read-only, why ALTER does not apply
domain-model.view-entity.oql both clause orders, association
paths, and the four rules that bite
domain-model.view-entity.association selecting an id under an alias IS
the association
Filed under domain-model rather than at the top level because a view
entity is a domain-model document: it sits on the canvas, declares
attributes, pages bind to it, and its associations come from its own
OQL. The top-level `oql` topic is a different thing — running a query
against a live runtime — and a sibling `view-entity` beside it would
invite that confusion. Aliases (view-entity, view_entity, viewentity,
view-entities) keep it reachable from the word people use; bare "view"
is deliberately not one, since GRANT VIEW ON PAGE owns that word.
Content is taken from measured behaviour in mdl-examples/bug-tests
rather than restated from the skill: the derived-string-length rule
(String(200) always, CE6770), the quoted-source / unquotable-alias split
(MDL072, so an attribute can never be called Month), and the three
consequences of the derived association (MDL080, CE6771, the
module-level name clash). Every example was run through `mxcli check`.
NANOFLOWS were not missing — microflow.nanoflow already existed, and
`mxcli syntax nanoflow` resolves to it. What it said was "same syntax as
microflow", which is the half that never causes a problem. It now lists
what `mxcli check` actually refuses, read off nanoflow_validation.go
rather than from memory: the ten disallowed activity families and every
workflow action, the Binary return type, and the six activities that
reject ON ERROR (CE6035) versus the ones that take it.
One claim was wrong on the first pass and the grammar caught it: there
IS a GRANT EXECUTE ON NANOFLOW. Verified by parsing it, along with
CREATE OR REPLACE NANOFLOW, before the entry claimed either.
Both pointers added in the previous commit now name the topic as well as
the skill, which is what the generated CLAUDE.md's own rule asks for
(5,606 bytes, within the 6,000-byte budget).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK2imrM8M5UbzhTvttb5YC
An audit of the registry against the grammar — every createStatement, alterStatement, dropStatement, dqlStatement and utilityStatement alternative matched against the 163 registered topics — found the CREATE and DROP surfaces fully covered and two shipped, MODEL-MUTATING statements with no registry presence at all: ALTER STYLING ON PAGE|SNIPPET X WIDGET w SET … | CLEAR DESIGN PROPERTIES UPDATE WIDGETS SET … WHERE … [IN Module] [DRY RUN] Both are implemented (mdl/executor/cmd_styling.go, cmd_widgets.go) and both are the in-place alternative to rewriting a whole page, which is exactly the case where being undiscoverable costs the most: an agent that cannot find them reaches for CREATE OR REPLACE PAGE, whose diff is the entire document and which drops anything MDL cannot yet spell. ALTER STYLING joins page.styling rather than getting its own path: the subject is one subject, and the topic previously documented only the half written inside CREATE PAGE. UPDATE WIDGETS gets page.update-widgets, with SHOW WIDGETS beside it since they share the WHERE grammar and the read-only one is how you check the other's pattern. Each claim read off the source rather than assumed: WHERE is mandatory on UPDATE (the grammar has no optional form), DRY RUN threads through to updateWidgetsInContainer and writes nothing, and the catalog is not refreshed by an update — the executor prints that itself, so the topic says it. Both examples were run through `mxcli check`, and the registry's TestExamplesParse now guards them. Not added, as a deliberate line: the session/REPL utilities the grammar also carries — LINT, SHOW LINT RULES, EXECUTE SCRIPT, EXECUTE RUNTIME, USE, INTROSPECT API, DEBUG, session SET. They drive the tool rather than author the model, and each has a CLI equivalent that is documented where CLI commands are documented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HK2imrM8M5UbzhTvttb5YC
The flattening in the previous commit was verified against a synthetic two-property complex type in its own namespace, which passed `mx check` clean. The integration suite's live TripPin contract (mdl-examples/doctype-tests/10-odata-examples.mdl) found four defects in it, at 11 errors on mxbuild. TripPin has what the fixture lacked: a base complex type, two types derived from it, a nested complex property, an Edm.GeographyPoint, and both a top-level entity set and types derived from it. Each fix is measured on 11.12.1: 1. Mendix imports a complex type's OWN properties only. Flattening AirportLocation's inherited `Address` is CE6615 "does not exist in the OData service", while the same `Address` reached through Person.HomeAddress — typed `Location` directly — is accepted, and each derived type's own property (`Loc`, `BuildingInfo`) is accepted. So the line is inheritance, not path syntax. 2. `!strings.HasPrefix(t, "Edm.")` is not the supported-type test. Edm.GeographyPoint passes it and is CE6622 "The type of attribute 'Location_Loc' in the OData service is not supported." The importable primitives are now a closed set. 3. Filterable/Sortable follow the ENTITY SET, not a fixed answer. CE6630 fires in both directions: on Person (entity set People) a flattened attribute must be filterable, on Employee/Manager/Event (derived, no entity set) it must not. Manager.BossOffice is Manager's own property and still unfilterable, which rules out inheritance as the explanation. Creatable/Updatable stay unconditionally false. 4. Inherited and unsupported properties are reported by name and reason rather than dropped, like every other thing the import cannot map. TripPin now imports at 0 errors, down from 11. Each rule has a test with a control, including both directions of the filterability rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FYdKPGnoWiq2VgNF6ZfX1
fix: flatten OData ComplexType properties on external entity import
Make workflows and view entities the default modelling choice, and reachable from `mxcli syntax`
fix: DESCRIBE ENUMERATION drops every caption on a non-en_US project
AI Code ReviewCritical IssuesNone found. Moderate IssuesNone found. Minor Issues
What Looks Good
RecommendationAutomated review via OpenRouter (Nemotron Super 120B) — workflow source |
7 commits from the fork's
main, on top of the last sync (def6d23). Grouped by theme.OData external entities lost every complex-type property
CREATE OR MODIFY EXTERNAL ENTITIES FROMdropped every property whose type was an OData ComplexType. The entity imported with only its key,execreported "1 created, 0 failed" and printed nothing, andmx checkreported 0 errors — the loss surfaced much later as CE1613 on a page written against the attributes Studio Pro would have made.745d24e, fixes CREATE OR MODIFY EXTERNAL ENTITIES FROM silently drops OData ComplexType properties from external namespaces #1118) —mdl/types/edmx.gonever parsed<ComplexType>at all, so a complex property was indistinguishable from an unknown type and fell through the!strings.HasPrefix(p.Type, "Edm.")drop. The report says "only cross-namespace"; in fact every complex type was dropped, since nothing namedEdm.*is complex. Now one attribute per leaf, named<property>_<leaf>, read over the OData path<property>/<leaf>, resolved by qualified name — one document may declareQuantityin two namespaces. Measured on 11.12.1: the path form is 0 errors, the underscore form and a bogus path are both 4× CE6615, so mxbuild genuinely validates into the complex type. The pre-fix control — attributes simply absent — is 0 errors, which is why the drop was invisible. Anything still unmappable is now named with a reason instead of vanishing.318cf90) — the first pass was verified against a synthetic two-property complex type that passedmx checkclean; the integration suite's TripPin contract found four defects in it, at 11 errors. TripPin has what the fixture lacked: a base complex type, two derived from it, a nested complex property, anEdm.GeographyPoint, and both a top-level entity set and types derived from it. (1) Mendix imports a complex type's own properties only — the line is inheritance, not path syntax. (2)!strings.HasPrefix(t, "Edm.")is not the supported-type test:Edm.GeographyPointpasses it and is CE6622; the importable primitives are now a closed set. (3) Filterable/Sortable follow the entity set, and CE6630 fires in both directions. (4) Inherited and unsupported properties are reported by name and reason. TripPin now imports at 0 errors, down from 11, each rule with a control.DESCRIBE read texts in the wrong language
ef99d39, fixes DESCRIBE ENUMERATION returns empty captions for values that have captions defined in Studio Pro #1113) — every value came back asMyValue ''on any project whoseDefaultLanguageCodeis noten_US. A caption is aTexts$Textand Mendix has no language-neutral text, so a hardcodeden_USlookup simply missed. The write side already resolved the project's language (Captions written via MDL always land as en_US — breaks projects whose only/default language isn't en_US #970) and the widget read side was fixed for the same reason (describe PAGE outputs default Dutch placeholder 'Tekst' instead of actual widget text content #702); the enumeration read was the site neither sweep reached. The output was also destructive: fed back throughexecit reported "Modified enumeration" and emptied the stored translations. After the fix the same round trip reports "Unchanged enumeration" and the unit is byte-identical.b6b6560b) — the same pattern appears at everyGetTranslation("en_US")and every en_US lookup followed by a bare map-range. Two distinct defects hid behind it: sites with no fallback dropped the text entirely (describe entitysilently lost a validation feedback message), and sites with a map-range fallback returned a different language per run, making DESCRIBE output undiffable on a multi-language project — invisible to a single-language repro. All now go through one resolver: project language, then en_US, then the non-empty translation with the lowest language code. An en_US project is unaffected at every site, and there are no hardcoded en_US reads left inmdl/executor.Workflows and view entities as the default modelling choice
75da19cd) — both are first-class in Mendix and both get reinvented in microflows by default, because the microflow version works: it passescheck, it builds, and nothing flags it. No command can prompt for either —lintcannot know a Status attribute is standing in for a state machine — so it has to be in the guidance or it reaches an agent only when the user already knew to ask. A business process with human steps is a WORKFLOW, not a status attribute plus microflows; an aggregation is a VIEW ENTITY (OQL the database executes), not a microflow that retrieves every row to produce one number — which gets slower exactly as the app succeeds. Stated in all three descriptions of the procedure, as the gates already are, and held together by a test with a control.mxcli syntaxcoveragec9b9c45) — VIEW ENTITY had no topic at all; the registry knew the words only as a keyword onerrors.execution, so an agent told to prefer one got nothing back. Three topics under domain-model (the document, its OQL, and why selecting an id under an alias is the association), filed there rather than beside the top-leveloqltopic, which is a different thing — running a query against a live runtime. Content taken from measured behaviour inmdl-examples/bug-testsrather than restated from the skill. Separately,microflow.nanoflowexisted but said "same syntax as microflow", which is the half that never causes a problem; it now lists whatmxcli checkactually refuses, read offnanoflow_validation.go. One claim was wrong on the first pass and the grammar caught it: there is aGRANT EXECUTE ON NANOFLOW.c4d69ae) — an audit of all 163 registered topics against every grammar statement alternative found the CREATE and DROP surfaces fully covered and two shipped, model-mutating statements with no registry presence at all. Both are the in-place alternative to rewriting a whole page, which is exactly where being undiscoverable costs the most: an agent that cannot find them reaches forCREATE OR REPLACE PAGE, whose diff is the entire document and which drops anything MDL cannot yet spell. Each claim read off the source rather than assumed. Deliberately not added: the session/REPL utilities, which drive the tool rather than author the model