Skip to content

Published OData services could not build from MDL; JAR dependencies never resolved; init/settings/grammar fixes (formula1 findings) - #110

Merged
ako merged 10 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h
Aug 7, 2026
Merged

Published OData services could not build from MDL; JAR dependencies never resolved; init/settings/grammar fixes (formula1 findings)#110
ako merged 10 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h

Conversation

@ako

@ako ako commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Works through the findings from building an F1 data app: a DuckDB-backed, non-persistable entity published over OData v4. Everything was reproduced against real MxBuild before being fixed, and re-verified after (mx check → 0 errors on Mendix 11.12.1 — 11.13.0 was not cached here, so version numbers below are 11.12.1 unless stated).

Two of the reported items did not survive checking; those are called out rather than quietly "fixed".

The headline: every published OData service from MDL was unbuildable

Two defaults CREATE ODATA SERVICE never set, each fatal on its own, neither visible to mxcli check:

CE0729 "The service name should not be empty". Name (the document) and ServiceName (the name in the OData metadata document) are different properties, and CREATE set only the first. The consumed path had defaulted the same field to the document name for CE0339 all along; the published path never got the line. An explicit ServiceName: still wins, and create or modify heals an empty one.

CE7375 "Attribute ID … must be published and be the key when associations are exposed as an associated object id" — firing with no associations exposed at all.

The finding framed this as a non-persistable-entity problem. It is not:

entity key PublishAssociations mx check
persistent unique attribute false (the old default) CE7375
persistent unique attribute true 0 errors
non-persistable attribute false CE7375
non-persistable attribute true 0 errors

false means "associations as an associated object id", which Mendix only allows when the system ID is the published key — while MDL's expose (Attr (KEY)) publishes an ordinary attribute. So the default broke every published service; non-persistable is merely where no workaround exists.

Defaulting to true is not a preference, it is the only value that can build from the MDL people write. Tri-state so an explicit false survives, and create or modify no longer flips a stored value the script never mentioned.

Nothing in-repo caught either default: doctype-tests/10-odata-examples.mdl sets both properties explicitly.

The other silent one: a declared JAR dependency never reached the classpath

ALTER MODULE X ADD JAR DEPENDENCY (…) wrote the coordinate, list jar dependencies reported it, the build went green — and the runtime threw No JDBC driver found in app for URL.

The write was never wrong. Declaring and resolving are separate steps: mx sync-java-dependencies <project.mpr> downloads into vendorlib/, Studio Pro runs it when you edit Module Settings, and nothing ran it headless. Measured on 11.12.1: a full mxbuild --target=deploy emits a build.gradle with no dependencies block and downloads nothing; the sync command then fetches the jar.

That answers the open question in the report — neither "mxbuild skips Maven resolution" nor "mxcli writes where MxBuild cannot read". (Found by strings mx.dll | grep -i dependenc surfacing ISyncJavaDependenciesRunner, then mx --help.)

Wired at three levels so it cannot stay silent:

  • the executor says so the moment it writes a coordinate that is not in vendorlib/;
  • mxcli sync-java-deps does the resolution on demand, with --check exiting non-zero as a build gate;
  • run --local vendors anything missing before boot, so the warm loop works from a fresh clone.

Resolution needs network, so each call site is best-effort and names the command to retry. Falling back to another Mendix version's mx is now stated rather than silent — a mismatch otherwise surfaces as an mpr-format complaint that reads like a corrupt project.

Corrections to the report

  • PublishAssociations is accepted on CREATE today (visitor_odata.go — the grammar takes a generic property list). A one-shot script could always express it; what was missing was documentation. The real defect was the default.
  • Pagesize: is not silently dropped. The visitor lowercases before matching, so casing is never a typo. A test pins that so the new rule cannot start flagging it.

Check-time gaps the build does not cover

Silently discarded OData properties (MDL-ODATA01). The grammar accepts any name: value pair and the visitor's switch had no default, so ReadMicroflow: or ServiceNam: parsed, checked, executed "successfully" — and left the model without the property. The name is lost in the visitor, so that is where it is recorded; a check-time validator reports it before anything is written, guessing the intended property rather than only listing the known ones.

Unreal database-connection types (MDL-DB01). The skill's table listed Redshift and SQLServer; neither is in Studio Pro's picker, read out of the shipped bundle and identical on 11.10.0, 11.12.1 and 11.13.0: MSSQL, MySQL, Oracle, PostgreSQL, Snowflake, BYOD. mxcli writes the string through and mxbuild does not validate it eithertype 'Redshift' builds 0 errors and simply does not connect. A warning rather than an error: the set is version-specific.

BYOD ("Other") was the missing entry that mattered — it skips the driver-presence check, which is what makes an unsupported JDBC driver usable. Now confirmed working against a live runtime, so the docs were the only thing in the way.

Query options and the DESCRIBE round trip

Countable / SkipSupported / TopSupported were literal true in the BSON writer with no MDL above them. Countable is not cosmetic: it forces every read-microflow-backed resource to declare a System.ODataResponse parameter and compute a count. Now publish-entity properties, tri-state because they default to true. Verified: Countable: No with a read microflow that takes no parameters builds 0 errors.

DESCRIBE emitted MDL it could not parse. Three slips, plus a fourth not in the report: the stored CallMicroflow: spelling, the entity type's exposed name where the entity set's belongs (silently renaming the set on a re-exec), and fully-qualified member names in an expose (...) clause that takes bare ones. Proved by round trip: describe → check parses → drop → exec the output → describe again → byte-identical, and mxbuild reports 0 errors on the rebuilt model.

MDL papercuts

$Total = 5; now parses. It failed with "no viable alternative at input '$Total=5'" while DECLARE $Total Integer = 0; worked — and so did $X = HEAD($List), $X = create M.E (…) and $X = execute database query …. Assignment existed only as a prefix on activity statements plus a SET $Var = … statement, so a plain value needed a keyword nothing suggested. SET is now optional; both spellings produce the same AST.

Regressions were checked with a control binary rather than by reading the grammar: stash the .g4, regenerate, build bin/mxcli-control, sweep every script in mdl-examples with both. Thirteen fail — the same thirteen, all pre-existing.

mxcli test tests/ -p app/App.mpr no longer fails for a tests/ next to the .mpr. The fallback applies only when the CWD-relative path does not exist, so a tests/ in both places still resolves to the one you are standing in.

Smaller ones

  • mxcli init stopped inventing a project. From a solution root it reported success and wrote tooling pointing at project.mpr, a file that does not exist. It now looks one level down and lets the candidate count decide: none → warn; one → name it and initialise that directory; two or more → refuse and print the command naming a specific app.
  • describe settings configuration '<name>' parses (the read form of an existing write statement), and show settings configurations carries ApplicationRootUrl.
  • The ANTLR pin lives where the failure happens — the Makefile holds it, the error quotes it, make -C mdl/grammar bootstrap does the install, and the README says the build needs network and a JVM, not only Go.
  • mxcli new step 6 says "links" — on Linux it hard-links the binary you ran rather than downloading the nightly.
  • Documented ReadMode: microflow …, ServiceName and the query options in mxcli syntax odata publish, with a worked non-persistable example. The skill's own example carried PublishAssociations: No, which cannot build; removed with the reason. Added the two dynamic query rules (every declared parameter still needs a value; a {param} placeholder concatenates into a path).

Not done

  • The solution-level bootstrap hook is a feature, not a fix; this adds only the sibling-project discovery it would need.
  • Ergast being dead is a dataset fact, not an mxcli defect.
  • The Gradle daemon surviving pkill is correct behaviour — it is a daemon, reused across builds.

Verification

🤖 Generated with Claude Code

https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4

claude added 10 commits August 7, 2026 18:39
A published OData service created purely from MDL passed `mxcli check` and then
failed the build on two counts, neither of which the author could see coming.

CE0729 "The service name should not be empty": `Name` (the document) and
`ServiceName` (the name in the OData metadata document) are different
properties, and CREATE set only the first. The consumed path has defaulted the
same field to the document name for CE0339 all along.

CE7375 "Attribute ID ... must be published and be the key when associations are
exposed as an associated object id", firing even with no associations exposed:
PublishAssociations defaults to false, which is object-id mode, and Mendix only
allows that when the system ID is published as the key — while MDL's
`expose (Attr (KEY))` publishes an ordinary attribute.

The finding framed the second as a non-persistable-entity problem. It is not:
measured on 11.12.1, the identical service over a PERSISTENT entity with a
unique key builds 0 errors with true and CE7375 with false. The default broke
every published service; non-persistable is only where no workaround exists,
because publishing the ID of a non-persistable entity is forbidden.

So default an unspecified PublishAssociations to true. That is not a preference,
it is the only value that can build from the MDL people write. An explicit false
is still honoured (tracked separately from "absent" so it survives), warned
about when a published entity is non-persistable, and `create or modify` no
longer flips a stored value the script never mentioned.

Nothing in-repo caught either default: doctype-tests/10-odata-examples.mdl sets
both properties explicitly. Both bug-test scripts now build 0 errors on 11.12.1
with no workarounds — no ServiceName, no follow-up ALTER.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The grammar accepts any `name: value` pair in an OData property list and the
visitor's switch had no default, so `ReadMicroflow:` or `ServiceNam:` parsed
cleanly, checked cleanly, executed "successfully" — and left the model without
the property. The ALTER path has always answered "unknown OData service
property"; CREATE, PUBLISH ENTITY, the OData client and the external entity had
nothing.

The name is lost in the visitor, so the visitor is where it has to be recorded:
each switch grows a default that appends to UnknownProperties, and a check-time
validator reports them as MDL-ODATA01 before anything is written. The message
guesses the intended property (prefix/substring, then one edit) rather than only
listing the known ones — with a list alone the reader is still diffing two
spellings by eye.

One correction to the report: `Pagesize:` is not among the casualties. The
visitor lowercases before matching, so casing is never a typo. A test pins that,
so the rule cannot start flagging it later.

No false positives across every script in mdl-examples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The three OData query options were written as literal true in the BSON writer,
with no MDL above them. Countable is not a cosmetic default: it forces every
read-microflow-backed resource to declare a System.ODataResponse parameter and
compute a count, and over a full CSV scan that count is the expensive part of
the request.

They are now publish-entity properties. Tri-state is load-bearing — they default
to true, so "unset" and "false" cannot share a representation or every existing
script would quietly turn them off. Unset stays nil, the writer resolves nil to
true, and the reader maps a stored true back to nil so DESCRIBE prints what the
author wrote rather than three defaults on every resource.

Verified on 11.12.1: `Countable: No` with a read microflow that takes no
$Response parameter builds 0 errors — the combination that could not be
expressed before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Three slips in one emit block, each of which broke the DESCRIBE-roundtrip
property the review checklist asks for:

- `ReadMode: CallMicroflow:Module.Read` is the backend's storage spelling and
  matches no MDL value. It now prints `microflow Module.Read`, the form the
  author wrote.
- `expose (...)` takes a bare member name; the stored name is fully qualified,
  so `Module.Entity.Attr` was emitted and did not parse. `IsPartOfKey` becomes
  `KEY` for the same reason — both parse, but only one is documented.
- `as '<name>'` printed the entity TYPE's exposed name where the entity SET's
  belongs. That one is invisible until the two differ (Studio Pro exposes the
  type singular and the set plural), and then a describe -> exec cycle silently
  renames the set the $metadata serves.

Proved by round trip rather than by eye: describe -> check parses, then drop the
service, exec the describe output, describe again — byte-identical, and mxbuild
reports 0 errors on the rebuilt model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ptions

`ReadMode: microflow Module.MF` has worked through the whole pipeline —
grammar, visitor, AST, BSON — and was documented nowhere, so the only way to
find it was to read the Go source. Without it, "publish persistent entities" is
the only shape the docs describe, which sends anyone with data outside Mendix
down a materialise-into-the-database path they do not need.

The syntax topic now carries ReadMode/InsertMode/UpdateMode/DeleteMode in their
microflow form, ServiceName and PublishAssociations with their defaults, and the
three query options. The skill gains a worked non-persistable example — read
microflow, no copy of the data, no refresh job — with the two things that bite
first: the $Response parameter Countable requires, and why PublishAssociations
must stay at its default there.

The skill's existing example also carried `PublishAssociations: No`, which
cannot build: object-id mode requires the system ID as the published key, and
that example publishes an ordinary attribute. Removed, with the reason.

The new example was executed against a real .mpr and builds 0 errors on 11.12.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The skill's table listed `Redshift` and `SQLServer`. Neither is in Studio Pro's
connector picker on any version available here — read out of the shipped bundle
at modeler/ide-client/database-connector-editor/, identical on 11.10.0, 11.12.1
and (per the report) 11.13.0: MSSQL, MySQL, Oracle, PostgreSQL, Snowflake, BYOD.

mxcli writes the type string through unchanged and mxbuild does not validate it
either — `type 'Redshift'` builds 0 errors — so a wrong value hides behind a
green build and only shows up as a connection that does not connect. `check` now
says so. A warning rather than an error: the set is version-specific and mxcli
cannot prove a string wrong on a version it has not seen.

The table was also missing the entry that matters most. `BYOD` ("Other") forces
connection-string configuration and skips the driver-presence check, which makes
any JDBC driver Mendix ships no picker entry for usable by dropping the JAR in
userlib/. That is what the finding needed for DuckDB, and it read as impossible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Run from a solution root — one folder per app, no .mpr at the root — `mxcli
init` reported success and wrote tooling pointing at `project.mpr`, a file that
does not exist. The hardcoded default made a missing project look like a found
one.

It now looks one level down and lets the candidate count decide: none, warn that
the generated paths are placeholders; exactly one, say which project it is and
initialise that directory; more than one, refuse, list them, and print the
command naming a specific app. A solution repo should not get one app's tooling
by coin flip.

One level only. A Mendix app keeps its .mpr at its own root, so walking deeper
would start finding deployment copies and backups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`alter settings configuration 'Default' …` is the write form; the read form was
a parse error. `describe settings configuration '<name>'` now parses and prints
that one configuration as re-executable MDL, naming the configurations that do
exist when the name is wrong. Where MDL has `alter X <selector>`, reaching for
`describe X <selector>` and getting a parse error teaches the wrong lesson.

`show settings configurations` also gains ApplicationRootUrl. It decides the
host the app answers on, so "did my root URL land?" is the obvious question to
ask that command, and the summary could not answer it — `describe settings |
grep` was the only way. An empty DatabaseUrl is skipped too, rather than
rendering as a bare comma that reads like a bug in the reader.

Both dumps go through one emit helper, so the whole-settings output and the
single-configuration output cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`make build` needs an `antlr4` launcher, and the error when it is missing said
only "install ANTLR4" — while the version is load-bearing. CI pins
antlr4-tools 0.2.2 with generator 4.13.2 against the 4.13.1 runtime in go.mod,
and a generator/runtime mismatch is a classic ANTLR failure mode that surfaces
as a generated parser that will not compile. That pin lived only in the workflow
YAML, where nobody hits it.

The pin now lives in mdl/grammar/Makefile, the error message quotes it, and
`make -C mdl/grammar bootstrap` does the pip install. `generate` also notes when
ANTLR4_TOOLS_ANTLR_VERSION is unset, since the default is whatever antlr4-tools
last downloaded.

The README build-from-source recipe gains the bootstrap step and says what the
build actually needs — network and a JVM, not only Go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Step 6 was documented as "downloads the correct mxcli binary", and on Linux it
does not download: it hard-links the mxcli you ran into the project, sharing the
inode. That is the right behaviour for a from-source build — the app folder gets
your binary, not the nightly — but the help text described the opposite, which
is what led one report to delete the linked binary before moving files around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
@ako
ako merged commit 4646a40 into main Aug 7, 2026
5 checks passed
@ako ako changed the title Published OData services could not build from MDL — plus init/settings/database-type fixes (formula1 findings) Published OData services could not build from MDL; JAR dependencies never resolved; init/settings/grammar fixes (formula1 findings) Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants