Skip to content

OData publishing, external-entity imports, dynamic SQL and folders: nine formula1 findings - #114

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

OData publishing, external-entity imports, dynamic SQL and folders: nine formula1 findings#114
ako merged 15 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h

Conversation

@ako

@ako ako commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Nine of the open findings in mxcli-formula1 FINDINGS.md, each verified against real mxbuild on Mendix 11.12.1.

Merged with main after #113 landed. That PR fixed §15a (test --list path resolution) and §19 (after-startup during --local) independently, and its §19 is the better answer — it actually chains the app's startup microflow and adds --skip-app-startup, where mine only said the microflow had been displaced. Both conflicts were resolved in favour of main, so this PR no longer claims either.

Fixed

§ What Verified by
16 A published Integer was written as Edm.Int32; Mendix publishes it as Int64, so every whole-number attribute in a service failed the build mxbuild adjudicated the whole type table at once — see below
25 create or modify external entity touching only an entity-level property detonated every attribute (CE6612 each) 3 errors before, 0 after, against a real contract import
28 An attribute called name was renamed Stg_Drivername / Circuitname, so pages written against the contract would not build and the same field differed per module One contract with a property per reserved-list name, prefixing disabled: CE7247 for seven, silence for name
24 create external entities ignored the contract's Countable / Filterable / Sortable annotations (CE6630) 2 errors before, 0 after
23 create odata client fetched $metadata without the credentials on the statement, creating an empty client with only a warning A real basic-auth server that 401s without credentials and 403s without the custom header — both now arrive
21 execute database query … dynamic $Sql reached the runtime as the literal '$Sql', blocking runtime-built SQL outright Read the stored BSON: DynamicQuery\x00\x05\x00\x00\x00$Sql — five bytes, no quotes
26 create or modify odata service ignored edits to a publish entity block Same script yields Label as 'label' before and Label as 'label' (Filterable, Sortable) after; build stays at 0 errors
32 MOVE JAVA ACTION / MOVE ODATA SERVICE were parse errors, and neither CREATE takes a folder clause — so those documents could never leave the module root Differential Unit count: three new folders (nested path creates two), document count unchanged, project still builds
15b MDL-ODATA01's hint omitted Countable / SkipSupported / TopSupported, telling users accepted properties were unknown The AST struct is now the source; omitting a property fails a test
#14 create module role had no or modify, so a security script could not be re-run Idempotent re-run, mxbuild 0 errors

Three findings turned out to be worth more than reported:

  • §16 caught a second wrong mapping. Publishing one attribute of every Mendix type in a single service and reading the CE5016s off the build confirmed Integer (reported) and Enumeration (only suspected): an enum was written as Edm.String with EnumerationAsString hardcoded false, which is the one combination Mendix rejects — CE5016 plus CE4583. The type and the flag are one setting, so the flag now travels with the attribute.
  • §25 was one nesting level below where it was reported. The executor already preserves attributes it was not asked to change; attributeFromGen handled StoredValue and OqlViewValue but not Rest$ODataMappedValue, so every attribute read back unmapped and the next write emitted a plain StoredValue. This is the attribute-level half of External Entities : allow_create_change_locally does't work mendixlabs/mxcli#782.
  • §28 is one wrong list entry, not a scheme that needs redesigning. Testing the whole reserved list at once — rather than only the reported name — showed the other seven entries are all real CE7247s. They stay, and now earn their place with an error code to point at instead of folklore. The renames that do happen are reported at import time.

Two existing unit tests had pinned the wrong answers (Edm.Int32, and nameAirlineName) and were corrected with the mxbuild evidence.

One correction to the report

§26's role-grant loss did not reproduce. Grants survived a modify on 11.12.1 on both the fixed and the previous build. The carry-through is kept as a guard — a modify cannot express grants, so it should not be able to drop them — and the commit and code comment both say so rather than claiming a fix.

Not done

  • §17 — a lint rule for KEY on a persistable attribute with no unique validation. Needs the project, so it belongs in the check --references pass.
  • §29 — the design-property lint validates against the widget's catalogue, not the applied theme's design-properties.json, so check green-lights values the build rejects.
  • §32 / feat(mdl): SHOW / DESCRIBE BUILDING BLOCKS (read-only discovery) #16 — nothing reads a document's folder back (SHOW STRUCTURE is flat, DESCRIBE omits it), so a move cannot be verified from mxcli.
  • §15c.ai-context/skills/ going stale after a binary upgrade. Needs a versioning or staleness-detection design.

Migration note

Fixing §28 means a re-import renames Stg_Drivername back to name — which is the point, but anything referring to the prefixed name has to follow.

Verification

Every fix was reverted and re-tested to confirm the new test fails with the reported symptom. Full unit suite green before and after the merge, and the doctype integration gate (all scripts, both engines, real mx check) green — run after each of the two grammar changes, since a check-only sweep proves nothing about what the visitor builds. It earned its keep: it caught a bad first draft of the new folder example that mxcli check passed.

Everything was verified on 11.12.1, not the 11.13.0 the findings were written against.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4

claude added 14 commits August 8, 2026 01:03
`mxcli test tests/ -p app/App.mpr` ran from the solution root and
`--list` on the same command line did not:

    Error: stat tests/: no such file or directory

resolveTestPaths was called below the --list branch, which returned first.
Listing and running now accept the same paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Every whole-number attribute in a published service failed the build, one
CE5016 each:

    Attribute …Stg_Season.Year is has type Integer, but is published as
    Edm.Int32.

Mendix publishes Integer as Int64, same as Long. The mapping's own comment
flagged Integer as an unverified guess, and the existing test pinned the guess.

Publishing every attribute type on 11.12.1 and reading the errors off the build
also caught a second wrong pair the report had only suspected: an enumeration
was written as Edm.String with EnumerationAsString hardcoded false, which is the
one combination Mendix rejects — CE5016 plus CE4583 "Enumeration 'Edm.Colour' is
not published in this service". The type and the flag are one setting, so the
flag now travels with the attribute.

Verified: the same all-types service builds 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`create or modify external entity` touching only an entity-level property
detonated every attribute of the entity:

    [CE6612] "Attribute 'circuitId' of external entity 'Stg_Circuit' is not supported."

one per attribute, leaving a project that cannot build.

The executor already preserves attributes it was not asked to change, so the
loss was a layer down: attributeFromGen handled StoredValue and OqlViewValue but
not Rest$ODataMappedValue. Every attribute of an external entity therefore came
back with no RemoteName, and the writer's `isExternal && a.RemoteName != ""` arm
fell through to a plain StoredValue on the next read-modify-write. The
per-attribute Filterable/Sortable/Creatable/Updatable flags live on the same
value and were lost with it.

This is the attribute-level half of mendixlabs#782, which fixed the entity level only.

Verified on 11.12.1 against a real contract import: three CE6612 before, none
after, and the modify's own change still lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
CREATE EXTERNAL ENTITIES read names, types and navigation properties out of the
contract correctly, then defaulted every capability to true regardless of what
the contract said. Mendix compares the two at build time and refuses:

    'Seasons' is marked Countable=False in the OData service, but True in the app.
    'latitude' is marked Filterable=False in the OData service, but True in the app.

Eight errors from an eight-resource import — on the one command whose whole job
is fidelity to the contract.

Insert/Update/Delete restrictions were already parsed; Count/Filter/Sort were
not, so there was nothing for the import to honour. An unannotated set still
means countable/filterable/sortable, which is OData's own default — silence is
not a restriction.

Verified on 11.12.1: a contract declaring CountRestrictions/Countable=false and
NonFilterableProperties produced two CE6630 before, none after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
CREATE ODATA CLIENT accepts UseAuthentication / HttpUsername / HttpPassword and
stores them for the runtime, but the design-time fetch was a bare client.Get.
Against a service behind `authentication basic` that is a 401 — and since the
failure is only a warning, the client is created with no cached entity types, so
the CREATE EXTERNAL ENTITIES that follows imports nothing from a script that
looks like it succeeded.

The credentials and any HEADERS now go out with the fetch.

Only literals can be used. The visitor strips a quoted literal's quotes, so
'f1api' and Module.ApiUser both arrive as bare strings; the AST now records
which was written. A constant is resolved by the runtime, and sending its *name*
as the password would be worse than sending nothing — so unresolved names are
reported instead, alongside a note that the client was left empty and that
pointing MetadataUrl at a committed contract file avoids the problem entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`execute database query … dynamic $Sql` reached the runtime as the string
literal '$Sql', so the database was asked to execute four characters:

    ERROR - ExternalDatabaseConnector: Parser Error: syntax error at or near "$"

The builder quoted anything not already starting with a quote — correct for
`dynamic 'SELECT …'`, wrong for an expression — and the AST kept no
literal-vs-expression flag, so it could not tell them apart. That blocked
runtime-built SQL, and therefore query pushdown, outright.

Verified by reading the stored BSON: DynamicQuery now holds $Sql, not '$Sql'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The hint said "Known properties here: ReadMode, InsertMode, UpdateMode,
DeleteMode, UsePaging, PageSize" long after the visitor learned Countable,
SkipSupported and TopSupported — so a user typing an accepted property was told
it was unknown.

The lists are separate by design (the visitor decides, the hint displays), but
nothing kept them in step. The AST struct is now the source: every field of
PublishedEntityDef and CreateExternalEntityStmt must be advertised or explicitly
listed as structural, so adding a property and forgetting the hint is a test
failure instead of a wrong message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mxcli test --local` sets the after-startup microflow to its own endpoint
registrar and restores it afterwards — deliberate, because a test run wants a
known starting state. But it said only:

    After-startup set to MxTest.RegisterEndpoint (registers the endpoint; runs no tests)

so a suite that needs startup state passes under --attach and fails under
--local against an empty scratch database, with nothing in the failure pointing
at the cause. The tests were asking for state the runner had prevented.

The run now names the displaced microflow and says --attach is the way to test
against an app that has actually started up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Re-running a `create or modify odata service` after editing a `publish entity`
block did not apply the change. Marking a member Filterable and re-executing
left the served $metadata exactly as it was; only `drop odata service` + create
picked it up. The modify branch updated the service's scalar properties and
never touched EntityTypes or EntitySets.

Supplied entities now replace the stored ones wholesale. Replacing rather than
merging is what makes the script the description of the service: a member
removed from the script is removed from the service, which merging could not
express.

The same change carries AllowedModuleRoles across a modify. That is a guard, not
a reproduction — the reported grant loss (mxcli-formula1 #26) did not reproduce
on 11.12.1, on either the fixed or the previous build — but a modify cannot
express grants, so it must not be able to drop them.

Verified on 11.12.1: the same script yields `Label as 'label'` before and
`Label as 'label' (Filterable, Sortable)` after, and the build stays at 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`create module role` had no `or modify` form, so re-running a security script
failed on the first role that already existed and role creation had to live in
its own run-once file.

`create or modify module role` now updates an existing role's description
instead of failing. AddModuleRole already overwrites, so it also adopts the
caller's casing — the same path the auto-provisioned-role branch above it uses.

`createModuleRoleStatement` carries its own CREATE keyword (it is dispatched
from securityStatement, not from the shared createStatement rule), so the
optional OR MODIFY goes in that rule and has to stay distinguishable from
`create or modify module`. Both spellings are covered by a test, and the full
doctype integration gate is green — the lesson from the last grammar change is
that a `check`-only sweep proves nothing about what the visitor builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
An attribute named `name` came out of CREATE EXTERNAL ENTITIES prefixed with the
remote type — Stg_Drivername, Circuitname — so a page written against the
published $metadata failed with "The selected attribute 'F1Live.Drivers.name'
no longer exists", and the same field carried a different name in every module
because the remote type names differ.

`name` was simply not reserved. Adjudicated on 11.12.1 by importing a contract
with a property for each name on the list and prefixing disabled: Mendix answered
CE7247 "The name 'x' is a reserved word" for id, owner, changedBy, changedDate,
createdDate, type and context — and said nothing about `name`. So this is one
wrong entry, not a scheme that needs redesigning; the other seven earn their
place and keep it.

The remaining renames are now reported at the end of an import instead of being
discovered later when a page will not build.

Note for existing projects: a re-import renames `Stg_Drivername` back to `name`,
which is the point, but anything referring to the old name has to follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MOVE accepted seven doctypes and rejected the rest at parse time
(`no viable alternative at input 'MOVEJAVA'`). Neither CREATE JAVA ACTION nor
CREATE ODATA SERVICE takes a folder clause either, so those documents could
never leave the module root from MDL — five of the reporting project's documents
were stuck there while the other 36 sorted into folders.

Both are plain document units, so each reduces to the existing reparent
primitive: the executor sets ContainerID and calls the backend, which persists
the containment row and touches nothing else. sdk/mpr's moveUnitByID is exported
for the doctypes that have no dedicated writer method of their own.

Verified on 11.12.1: `move java action` and `move odata service` into
'Support' and 'Api/Published' created exactly three folders (two levels for the
nested path), left the document count unchanged, and the project still loads and
builds. Full doctype integration gate green — mandatory for a grammar change,
and it caught a bad first draft of the new example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
@ako ako changed the title OData publishing, external-entity imports and dynamic SQL: nine formula1 findings OData publishing, external-entity imports, dynamic SQL and folders: eleven formula1 findings Aug 8, 2026
…stics-spike-emta6h

# Conflicts:
#	cmd/mxcli/cmd_test_run.go
#	cmd/mxcli/testrunner/runner.go
@ako
ako merged commit c76d4b7 into main Aug 8, 2026
3 checks passed
@ako ako changed the title OData publishing, external-entity imports, dynamic SQL and folders: eleven formula1 findings OData publishing, external-entity imports, dynamic SQL and folders: nine formula1 findings Aug 8, 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