Skip to content

fix(odata): read an external entity's OData source, and keep its local-changes flag - #60

Merged
ako merged 3 commits into
mainfrom
claude/odata-external-entity-782
Jul 31, 2026
Merged

fix(odata): read an external entity's OData source, and keep its local-changes flag#60
ako merged 3 commits into
mainfrom
claude/odata-external-entity-782

Conversation

@ako

@ako ako commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Fixes mendixlabs/mxcli#782 — "External Entities: allow_create_change_locally doesn't work". Split out of #57.

(Issue numbers refer to mendixlabs/mxcli, not this fork.)

It turned out to be two independent bugs, and the first is much bigger than the flag.

The real cause — external entities do not read back

entityFromGen recognised only DomainModels$OqlViewEntitySource, so any OData external entity read back with an empty Source and every remote field zeroed. The write path was always fine — it switches on e.Source, which the read never populated.

So alter entity … set allow_create_change_locally = true wrote the flag onto a model that no longer knew the entity was external, and the update rebuilt the entity without its source at all — losing the entity set, remote name, capabilities and remote key, not just the flag. describe external entity didn't work either:

Error: MyFirstModule.Products is not an external entity (source: )

The bisect signal that nailed it: the same commands work under --engine legacy, whose parser handles all three Rest$OData* flavours. This is a modelsdk-engine gap, and modelsdk is the default — so anyone on stock mxcli has it.

All three flavours are now mirrored from the legacy parser: ODataRemoteEntitySource (service, entity set, remote name, capability flags, CreateChangeLocally, remote key), ODataEntityTypeSource (service, type name, IsOpen, key) and ODataPrimitiveCollectionEntitySource (service).

Updatable is deliberately left zero: the storage type has no such field (updatability is per attribute via Rest$ODataMappedValue), there is no gen accessor, and the writer does not emit one — read and write stay symmetric.

The half that matches the reporter's words

Even with the read fixed, create or modify external entities reset the flag on every run: applyExternalEntityFields stamped CreateChangeLocally = false on the update path too.

That is right for the capability flags (Countable/Creatable/Deletable/Skip/Top), which the service's capability annotations describe and which should be refreshed from the metadata. It is wrong for CreateChangeLocally, which no OData contract describes — it is a local modelling choice. The top-level branch now leaves it alone; the entity-type branch still clears it, since that storage type has no such field.

Testing

make build, make test, make lint, make check-mdl pass on this branch alone.

Reproduced the reporter's flow end-to-end against a real project using the repo's own mdl-examples/odata-local-metadata/sample-metadata.xml. Before: describe errored, flag stayed No. After: describe works, alter … = true sticks (confirmed in the raw BSON — CreateChangeLocally: true with the source and key intact), and a re-import preserves it while still refreshing the contract-derived capabilities.

Four round-trip tests in external_entity_read_test.go, verified failing pre-fix — they also caught the source destruction that the reported symptom hides. Plus a re-import test in cmd_contract_test.go and fixture 782-external-entity-create-change-locally.mdl.

Not included

The bulk CREATE EXTERNAL ENTITIES statement still has no syntax to set the flag at import time — you set it afterwards with ALTER ENTITY, which now works and survives re-import. Adding import-time syntax would be a grammar change and a separate feature.

🤖 Generated with Claude Code

https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA


Generated by Claude Code

claude added 2 commits July 30, 2026 22:24
)

`alter entity X set allow_create_change_locally = true` reported success and
left the flag off. So did every other remote setting, and `describe external
entity` rejected the entity outright with "is not an external entity
(source: )".

The write path was never the problem: externalEntitySourceToGen switches on
e.Source and handles all three Rest$OData* flavours. The *read* filled none of
them — entityFromGen recognised only DomainModels$OqlViewEntitySource, so an
external entity came back with an empty Source and every remote field zeroed.
Setting a flag then wrote it onto a model that no longer knew the entity was
external, and the update rebuilt the entity without its source at all.

The legacy engine parses all three flavours (sdk/mpr/parser_domainmodel.go),
so this was a modelsdk-engine gap — and modelsdk is the default. Under
`--engine legacy` the same commands worked, which is a useful bisect signal.

Now mirrored for:

- Rest$ODataRemoteEntitySource — service, entity set, remote name, the
  capability flags, CreateChangeLocally and the remote key
- Rest$ODataEntityTypeSource — service, type name, IsOpen and the remote key
- Rest$ODataPrimitiveCollectionEntitySource — service

Updatable is deliberately left zero: the storage type has no such field
(updatability is per attribute via Rest$ODataMappedValue), there is no gen
accessor, and the writer does not emit one, so read and write stay symmetric.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…y re-import

`create or modify external entities` reset "Allow creating and changing
objects locally" to false on every run, so the setting survived only until the
next import — the other half of mendixlabs#782.

applyExternalEntityFields stamps every field on both the create and the update
path. That is right for the capability flags (Countable / Creatable /
Deletable / SkipSupported / TopSupported), which are derived from the service's
capability annotations and should be refreshed from the metadata. It is wrong
for CreateChangeLocally, which no OData contract describes: it is a local
modelling choice, so the top-level branch now leaves it alone. A newly imported
entity arrives zero-valued, which is Mendix's default.

The entity-type branch still clears it, since that storage type has no such
field — a re-import that reclassifies an entity as a derived type would
otherwise leave a stale value in the model.

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

# Conflicts:
#	.claude/skills/fix-issue.md
@ako
ako merged commit 9a12f3d into main Jul 31, 2026
3 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

Development

Successfully merging this pull request may close these issues.

2 participants