Skip to content

Sync ako/mxcli: OData complex types, project-language DESCRIBE, modelling defaults and syntax coverage - #1134

Merged
ako merged 11 commits into
mendixlabs:mainfrom
ako:main
Sep 17, 2026
Merged

ako merged 11 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

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 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 reported 0 errors — the loss surfaced much later as CE1613 on a page written against the attributes Studio Pro would have made.

  • Flatten as Studio Pro does (745d24e, fixes CREATE OR MODIFY EXTERNAL ENTITIES FROM silently drops OData ComplexType properties from external namespaces #1118) — mdl/types/edmx.go never 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 named Edm.* is complex. Now one attribute per leaf, named <property>_<leaf>, read over the OData path <property>/<leaf>, resolved by qualified name — one document may declare Quantity in 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.
  • Corrected against the live TripPin contract (318cf90) — the first pass was verified against a synthetic two-property complex type that passed mx check clean; 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, an Edm.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.GeographyPoint passes 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

  • Enumeration captions (ef99d39, fixes DESCRIBE ENUMERATION returns empty captions for values that have captions defined in Studio Pro #1113) — every value came back as MyValue '' on any project whose DefaultLanguageCode is not en_US. A caption is a Texts$Text and Mendix has no language-neutral text, so a hardcoded en_US lookup 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 through exec it reported "Modified enumeration" and emptied the stored translations. After the fix the same round trip reports "Unchanged enumeration" and the unit is byte-identical.
  • Swept across the executor (b6b6560b) — the same pattern appears at every GetTranslation("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 entity silently 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 in mdl/executor.

Workflows and view entities as the default modelling choice

  • Stated in the guidance (75da19cd) — 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 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 syntax coverage

  • View entities and nanoflow restrictions (c9b9c45) — VIEW ENTITY had no topic at all; the registry knew the words only as a keyword on errors.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-level oql topic, which is a different thing — running a query against a live runtime. Content taken from measured behaviour in mdl-examples/bug-tests rather than restated from the skill. Separately, microflow.nanoflow existed but said "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. One claim was wrong on the first pass and the grammar caught it: there is a GRANT EXECUTE ON NANOFLOW.
  • ALTER STYLING and UPDATE WIDGETS (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 for CREATE 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

claude and others added 11 commits September 17, 2026 18:20
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
@github-actions

Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • Syntax coverage completeness: While the PR adds syntax topics for VIEW ENTITY, ALTER STYLING, and UPDATE WIDGETS, I don't see corresponding additions in the doctest files (mdl-examples/doctype-tests/). For full-stack consistency, syntax features should have working examples in doctest files to verify they parse and execute correctly. The PR does add bug test files which are appropriate for the fixes, but the syntax features themselves would benefit from positive doctest examples.

  • PR scope breadth: The PR covers four distinct themes (OData complex types, language handling, guidance documentation, syntax coverage). While all are valuable improvements, they address largely unrelated areas of the codebase. Following the atomicity principle, these might be better split into separate PRs for easier review and tracking.

What Looks Good

  • OData complex types fix: Excellent resolution of a serious silent failure mode. The fix properly parses ComplexType elements, flatt them as Studio Pro does (using <property>_<leaf> naming), uses qualified names for resolution across namespaces, correctly handles inheritance (importing own properties only), uses a closed set of supported types, properly handles filterable/sortable semantics, and reports unmappable properties with reasons instead of silently dropping them. Comprehensive tests verify the behavior against both synthetic and real-world (TripPin) contracts.

  • Language handling fix: Thoroughly addresses the issue of DESCRIBE returning texts in the wrong language. Replaces all hardcoded "en_US" lookups with a consistent resolver (project language → en_US → lowest language code), makes the output deterministic by sorting language codes, and applies the fix broadly across enumerations, entities, validation rules, and other text-bearing elements. Tests verify correct behavior in mono-lingual, multi-lingual, and fallback scenarios.

  • Guidance documentation: Appropriately elevates WORKFLOW and VIEW ENTITY as first-class modelling choices in the documentation, helping prevent users from reinventing these patterns in microflows.

  • Syntax coverage improvements: Adds missing syntax topics for already-implemented features (VIEW ENTITY, ALTER STYLING, UPDATE WIDGETS, nanoflow restrictions), improving discoverability and reducing the tendency to use less optimal patterns like CREATE OR REPLACE PAGE.

  • Code quality: The changes follow established patterns, use proper error handling, and include comprehensive test coverage. The language fix in particular shows good engineering practice by identifying a systemic issue and applying a consistent solution across the codebase.

  • Test coverage: Adds meaningful unit and integration tests for both fixes, including testing against real-world contracts (TripPin) and verifying error reporting behavior.

Recommendation


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@ako
ako merged commit 0185a39 into mendixlabs:main Sep 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants