Skip to content

Commit 7fba5e4

Browse files
akoclaude
andcommitted
fix(modelsdk): rewrite inline OqlViewEntitySource OQL on MOVE ENTITY (10.24 nightly)
MOVE ENTITY of an entity that a view entity's OQL joins through left a stale module reference on the modelsdk engine, so `mx check` on Mendix 10.x aborted with CE0174 "path '…/DmTest.Customer' does not resolve to an entity" at the view entity. The 10.24 nightly failed 01-domain-model-examples/modelsdk this way (11.12 modelsdk and both legacy runs were clean — a 10.x-only, modelsdk-only bug). Root cause: Mendix < 11 stores a view entity's OQL in TWO places — the top-level DomainModels$ViewEntitySourceDocument unit AND inline on the entity's DomainModels$OqlViewEntitySource (entityToGen writes the inline copy only when major < 11). modelsdk's UpdateOqlQueriesForMovedEntity rewrote only the source document, leaving the inline copy pointing at the old module. 11.x keeps the OQL only on the source document, so it was never affected. Legacy updates the inline copy incidentally by re-serializing the domain model. Fix: after the ViewEntitySourceDocument sweep, also walk every domain model (ListDomainModels → loadDomainModelGen), find each entity whose Source() is an OqlViewEntitySource with an Oql() referencing the moved entity, SetOql the rewritten text, and persistDM — mirroring UpdateEnumerationRefsInAllDomainModels. Verified on the real toolset: 01-domain-model-examples now passes on Mendix 10.24 (both engines) and still passes on 11.12 (both engines). Repro: mdl-examples/bug-tests/move-entity-oql-view-inline-ref.mdl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 57bfd90 commit 7fba5e4

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

.claude/skills/fix-issue.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
100100
| `mx check` reports CE6621 "Attribute 'X' has max length '200' which is not the same as max length in the OData service which is 'unlimited'" on every unlimited-length string attribute of a `create external entities from …` import — **only under the default modelsdk engine**; `--engine legacy` is clean | The codec's encoder only emits *dirty* properties for a new element. `attributeTypeToGen` skipped `SetLength` when `Length == 0` (unlimited), so the `Length` property stayed non-dirty and the field was omitted from the BSON entirely. Studio Pro then applies its own UI default of 200, which contradicts the OData service's "unlimited" type. Legacy's `writer_domainmodel.go` always writes `Length: t.Length` (including 0), so it never regressed | `mdl/backend/modelsdk/domainmodel_write.go` → `attributeTypeToGen` (`*domainmodel.StringAttributeType` case) | Always call `g.SetLength(int32(at.Length))` — drop the `if at.Length > 0` guard — so `Length: 0` is emitted explicitly, matching the legacy serializer. General rule for the codec write path: any field whose *zero value is meaningful* (0 = unlimited here) must be Set unconditionally, because an omitted field falls back to Studio Pro's own default, not to zero. Assert on the serialized `NewType` map (not DESCRIBE) — the reader treats a missing Length as unlimited so it hides the damage. Test `TestEntityToGen_ODataRemoteEntitySource` in `external_entity_write_test.go`. Issue #718 |
101101
| `mxcli oql` against a docker-deployed app returns **0 rows** for tables that clearly have data (false negative — tester concluded "saving is broken") | NOT a database-config problem (that's a red herring — proven: aligning the runtime to Postgres with data still returned 0). The OQL preview servlet (`/dev/preview_execute_oql`) is registered by `com.mendix.basis.livepreview.LivePreviewRegistry` → `AppContainer.addDevelopmentServlet`, which **only mounts the servlet when the JVM system property `mendix.running.locally.by.studiopro=true` is set** (Studio Pro sets it; a deployed `bin/start` PAD doesn't). Without it the endpoint returns HTTP 200 `{"result":-5,"message":"Action not found"}`, which mxcli silently parsed as empty data. (Found by decompiling `com.mendix.mxruntime.jar` / `com.mendix.appcontainer.jar`.) | `cmd/mxcli/docker/templates/docker-compose.yml` (the `command`) + `cmd/mxcli/docker/oql.go` (`oqlDevError`) | Add `-J -Dmendix.running.locally.by.studiopro=true` to the start command (each JVM opt needs its own `-J`), next to the existing `-Dmendix.live-preview=enabled` — this mounts the dev servlets and `mxcli oql` returns live data (verified end-to-end; DTAP mode is NOT required, DB config is irrelevant — the preview runs in-process against the runtime DataStore). Separately, make `oqlDevError` surface `{"result":<0,"message":…}` (not just `{"error":…}`) so the failure isn't swallowed as 0 rows. **Related startup bug found:** the generated `etc/Default` inherits the project's ports, which are `0` when unset → the runtime rejects port 0 and won't start; `patchRuntimePorts` (patch.go) forces `runtime.http { port = 8080 }` / `admin { port = 8090 }`. Verified with a full docker build/run/seed cycle |
102102
| `GRANT VIEW ON PAGE Module.Page TO Module.Role` prints success but the page's allowed roles appear to stay empty: `mxcli lint` still reports `CE0557`/`MPR007` ("used as home page … but has no allowed roles") across a freshly rebuilt catalog, while entity-access grants on the same roles verify fine (`describe entity` shows them). Reported on **modelsdk** engine (default), Mendix 11.11 | NOT a write bug — the grant **does** persist (`AllowedModuleRoles: [3, "Module.Role"]` on disk; `mx check` = 0 errors; Studio Pro would load clean). The regression was on the **read** path: modelsdk `pageFromGen` didn't populate `Page.AllowedRoles`, and `mxcli lint`'s CE0557 rule counts `len(pg.AllowedRoles)` from `ListPages()` — so it read every page as having zero roles and false-fired. `SHOW ACCESS ON PAGE` / `SHOW SECURITY MATRIX` under-reported the same way. The entity-vs-page asymmetry the reporter saw = different read paths (entity `describe` worked; page `lint` didn't), not different writes | `mdl/backend/modelsdk/page.go` → `pageFromGen` | Already fixed by `88a2d50b1` (issue #722): populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()`, mirroring `microflowFromGen`. Guard for the reporter's exact end-to-end flow: `TestUpdateAllowedRoles_PagePersistsForLintRead` (`mdl/backend/modelsdk/page_roles_test.go`) writes a page role then reopens fresh and asserts the lint read path (`ListPages`→`AllowedRoles`) sees it. **Diagnosis tip**: when a write "reports success but the value stays empty" *only in a read/lint/show command*, dump the raw unit (`mxcli bson dump`) — if the bytes are there, it's a read-adapter (`*FromGen`) gap, not a write bug; confirm by reverting the suspected read commit and re-running |
103+
| `MOVE ENTITY X to OtherModule` where a **view entity's OQL** joins through `X`: passes `mxcli check`, but `mx check` on **Mendix 10.x** reports `CE0174 "…path '…/DmTest.Customer' does not resolve to an entity"` at the view entity — **only on the modelsdk engine**, only on 10.x (11.x is clean; legacy is clean) | Mendix <11 stores a view entity's OQL in **two** places: the top-level `DomainModels$ViewEntitySourceDocument` unit AND inline on the entity's `DomainModels$OqlViewEntitySource` (see `entityToGen`, gated `major < 11`). modelsdk's `UpdateOqlQueriesForMovedEntity` rewrote only the source document, leaving the inline copy pointing at the old module. 11.x doesn't write the inline copy (OQL lives only on the source doc), so it was never affected — hence 10.x-only. Legacy updates the inline copy incidentally by re-serializing the domain model | `mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity` | After the ViewEntitySourceDocument sweep, also iterate every domain model (`ListDomainModels` → `loadDomainModelGen`), find each entity whose `Source()` is `*genDm.OqlViewEntitySource` with an `Oql()` containing the old qualified name, `SetOql(strings.ReplaceAll(...))`, and `persistDM`. Mirrors the `UpdateEnumerationRefsInAllDomainModels` walk. **Diagnosis tip**: when a value updates in one storage location but a stale copy elsewhere still breaks `mx check`, dump raw `mprcontents` and grep for the value across `$Type`s — Mendix often duplicates a field (OQL, captions) and a version gate (`major < 11`) can make one copy version-conditional. Repro: `mdl-examples/bug-tests/move-entity-oql-view-inline-ref.mdl` |
103104
| On **Mendix 11.12**, `mx check` fails to load projects on the **legacy** engine for several doctypes (`01-domain-model`, `10-odata`, `14-project-settings`, `22-published-rest`) with `System.InvalidOperationException: Expected '$ID' as the first property of a storage object, but got 'X'` at `StreamingBsonUnitReader`. The bad first-key varies run-to-run (`ThemeModuleOrder`, `EnableRspackBundler`, `Oql`, `MarkAsUsed`, `PopupResizable`…) — the tell-tale of random map-key order. modelsdk passes; ≤ 11.11 tolerates any order | Several legacy `sdk/mpr` writers preserve round-trip fidelity by carrying parsed subtrees as Go maps (`ProjectSettings.RawParts`, ref-marking `raw`, rename `raw`, domain-model `raw`) and marshalling them back. `bson.Marshal` emits **map keys in random order**, so `$ID` only lands first by luck. 11.12 rejects any storage object whose first key isn't `$ID`. The earlier `$ID`-first fix (bson.D reorder in writer_domainmodel/security) missed every map-passthrough site | `sdk/mpr/writer_settings.go` (+ `writer_refs`, `writer_rename`, `writer_domainmodel`, `writer_odata`, `writer_rest`, `writer_customblob`, `writer_security`, `writer_modules`, `writer_microflow`, … — all unit-boundary marshals) via new `sdk/mpr/writer_order.go` | Wrap every unit-boundary `bson.Marshal(doc)` in `marshalUnitIDFirst(doc)` = `bson.Marshal(bsonutil.HoistStorageID(doc))`. **Use the non-sorting `HoistStorageID`, NOT `OrderStorageValue`**: `OrderStorageValue` *sorts* the non-`$ID` keys, which corrupts template-derived pluggable-widget/datagrid page trees (mx then aborts with `got 'LabelTemplate'`, regressing `29-datagrid`/`33-alter-page`). `HoistStorageID` only lifts `$ID`/`$Type` to the front and preserves every other key's order → no-op on already-correct writers, fixes only the map-passthrough ones. **Do NOT normalize `writer_pages.go` or the page/workflow mutators** — their output is already `$ID`-first and they embed delicate widget maps the hoist would sort-corrupt. Verify on 11.12 with the doctype gate; regression-check `29`/`33` specifically. (Left unfixed: `03-page` LabelTemplate — a separate pre-existing issue where on-disk bytes read `$ID`-first yet mx still objects) |
104105
| A `NUMBERFILTER` in a DataGrid column passes `mxcli check` but corrupts the `.mpr`: MxBuild / Studio Pro **fails to load** the whole project on **Mendix 11.12** with `System.InvalidOperationException: Type …CustomWidgets.WidgetProperty does not contain a constructor with a parameter of type …CustomWidgets.WidgetValue` at `StreamingBsonUnitReader.ConstructObject`. `TEXTFILTER`/`DATEFILTER`/`DROPDOWNFILTER` in identical grids load fine; ≤ 11.11 loads even the number filter. Isolating it needs dropping widgets one-by-one under `mx check` | The embedded `datagrid-number-filter.json` template authored its 3 placeholder / screen-reader `Forms$ClientTemplate` blocks with **markerless empty arrays** — `"Items": []` and `"Parameters": []`. Every Mendix list serializes with a leading marker int (`Texts$Text.Items`→`[3]`, `ClientTemplate.Parameters`→`[2]`, `Widgets`/`Objects`→`[2]`); a bare `[]` has no marker. 11.12's stricter streaming reader mis-parses the markerless array and mis-associates the following bytes, so a nested `WidgetValue` lands where the reader is constructing a `WidgetProperty`. The template shape is otherwise identical to the working filters — the only diff is the missing markers (the blocks had hand-authored `dd2b3c4d…` placeholder IDs, i.e. not cleanly extracted from Studio Pro) | `sdk/widgets/templates/mendix-11.6/datagrid-number-filter.json` **and** `modelsdk/widgets/templates/mendix-11.6/datagrid-number-filter.json` (both engines embed their own copy) | Add the marker int to every empty `TextTemplate` array: `"Items": []`→`"Items": [3]`, `"Parameters": []`→`"Parameters": [2]`. Rebuild (`go:embed`), re-exec, verify with `scripts/mx-check.sh --version 11.12.0` (0 errors; regression-check 11.9 still clean). Guard: `TestTemplates_NoMarkerlessEmptyArrays` (in both `sdk/widgets` and `modelsdk/widgets`) walks every embedded template and fails on any bare `[]` — a Mendix list must always carry its marker. Repro: `mdl-examples/bug-tests/datagrid-numberfilter-array-marker.mdl`. **Diagnosis tip**: when a widget "passes check, fails 11.12 load" with the WidgetProperty/WidgetValue constructor error, suspect a markerless empty array in the template (shape-diff the failing widget's template against a working sibling; the smoking gun is `[]` vs `[<int>]`) |
105106
| `mxcli check --references` flags `attribute 'Type' is a reserved word (CE7247) [MDL021]` even though the attribute is **quoted** (`"Type": String`), and the skills say "always quote to avoid reserved-word conflicts" — tester assumed quoting should exempt it | NOT a bug — the check is correct. Quoting only escapes **MDL parser** keywords (`unquoteString` strips the quotes and the *bare* name is validated). `Type`, `ID`, `GUID`, `CurrentUser`, and the audit names `CreatedDate`/`ChangedDate`/`Owner`/`ChangedBy` are reserved by the Mendix **platform**, so they fail regardless of quoting. The gap was documentation: several skills claimed quoting is "always safe" without the platform-name carve-out | `mdl/executor/cmd_enumerations.go` (`mendixReservedWords`, `mendixSystemAttributeNames`) — no code change needed; docs only | Add the carve-out to the "always quote" guidance (`check-syntax.md`, `generate-domain-model.md`, `demo-data.md`, docs-site `lexical-structure.md`) and a CLAUDE.md note: quoting is *parser*-safe, not *platform*-safe. Rename `Type`→`ResourceType`; use `AutoCreatedDate`/… pseudo-types for audit fields. Adjacent Mendix rule documented alongside: the after-startup microflow must return `Boolean` (CE0142) — a void seed microflow fails the build |
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
-- Bug: MOVE ENTITY left a stale reference in a view entity's OQL on the modelsdk
2+
-- engine, so `mx check` on Mendix 10.x reported CE0174 "does not resolve to an
3+
-- entity" (the 10.24 nightly failed 01-domain-model-examples/modelsdk this way).
4+
--
5+
-- Root cause: Mendix < 11 stores the OQL in TWO places — the top-level
6+
-- ViewEntitySourceDocument AND inline on the entity's OqlViewEntitySource.
7+
-- UpdateOqlQueriesForMovedEntity only rewrote the source document, leaving the
8+
-- inline copy pointing at the old module. (11.x keeps the OQL only on the source
9+
-- document, so it was unaffected — hence 10.x-only.) Fix: also rewrite the inline
10+
-- OqlViewEntitySource.Oql in every domain model.
11+
--
12+
-- Verify in Studio Pro (Mendix 10.x): open the project after exec — the
13+
-- InvoiceDetails view entity's query must join through DmTest2.Customer (the new
14+
-- module), and the consistency check must be clean (no CE0174).
15+
16+
create module DmTest2;
17+
18+
create entity DmTest.Customer ( FirstName: string(50), LastName: string(50), Email: string(100) );
19+
create entity DmTest.Invoice ( InvoiceNumber: string(50), IssueDate: datetime );
20+
create association DmTest.Invoice_Customer
21+
from DmTest.Invoice to DmTest.Customer
22+
type reference owner default;
23+
24+
create view entity DmTest.InvoiceDetails (
25+
InvoiceNumber: string(50),
26+
CustomerName: string(200)
27+
) as
28+
select
29+
i.InvoiceNumber as InvoiceNumber,
30+
c.FirstName + ' ' + c.LastName as CustomerName
31+
from DmTest.Invoice as i
32+
inner join i/DmTest.Invoice_Customer/DmTest.Customer as c;
33+
34+
-- The move must rewrite BOTH OQL copies to DmTest2.Customer.
35+
move entity DmTest.Customer to DmTest2;

mdl/backend/modelsdk/move_view_write.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,45 @@ func (b *Backend) UpdateOqlQueriesForMovedEntity(oldQualifiedName, newQualifiedN
6666
}
6767
updated++
6868
}
69+
70+
// Mendix < 11 also stores the OQL inline on the entity's OqlViewEntitySource
71+
// (see entityToGen), a second copy the ViewEntitySourceDocument sweep above
72+
// does not touch. Without this the moved-entity reference stays stale there and
73+
// `mx check` on 10.x reports CE0174 (11.x keeps the OQL only on the source
74+
// document, so it isn't affected). Rewrite the inline copy in every domain model.
75+
dms, err := b.ListDomainModels()
76+
if err != nil {
77+
return updated, fmt.Errorf("UpdateOqlQueriesForMovedEntity: list domain models: %w", err)
78+
}
79+
for _, info := range dms {
80+
gdm, err := b.loadDomainModelGen(info.ID)
81+
if err != nil {
82+
return updated, err
83+
}
84+
changed := false
85+
for _, el := range gdm.EntitiesItems() {
86+
ent, ok := el.(*genDm.Entity)
87+
if !ok {
88+
continue
89+
}
90+
src, ok := ent.Source().(*genDm.OqlViewEntitySource)
91+
if !ok {
92+
continue
93+
}
94+
oql := src.Oql()
95+
if oql == "" || !strings.Contains(oql, oldQualifiedName) {
96+
continue
97+
}
98+
src.SetOql(strings.ReplaceAll(oql, oldQualifiedName, newQualifiedName))
99+
changed = true
100+
updated++
101+
}
102+
if changed {
103+
if err := b.persistDM(info.ID, gdm); err != nil {
104+
return updated, err
105+
}
106+
}
107+
}
69108
return updated, nil
70109
}
71110

0 commit comments

Comments
 (0)