From d46d2ad04339286b0496a087c35eaa66c2f4e2af Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 12:40:10 +0000 Subject: [PATCH] fix: write the Java version in the dialect the stored key expects The #759 fix followed the JavaVersion -> JavaMajorVersion key rename but wrote the caller's value through verbatim. The rename changed the value format too: 11.6 stores the enum member "Java21", 11.12 the bare major "21". So `alter settings model JavaVersion = 'Java21'` on an 11.12 project put "Java21" into JavaMajorVersion, and mxbuild refuses to load it: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'majorVersion is an unsupported value: Java21') at Mendix.Modeler.Settings.JavaVersionExtensions.fromString This is a harder failure than the original #759 shape. That one wrote an unknown property, which mxbuild tolerates, so only Studio Pro broke; a wrong value for a known enum fails the whole project load, taking every check downstream of the settings unit with it. It is why the nightly went red on 14-project-settings-examples.mdl at 11.12 rather than surfacing as a user report. settingsoverlay.JavaVersionValue renders the value per stored key, so either spelling is accepted on input and stored in the project's own dialect. A value with no recognisable major version passes through untouched, so a typo surfaces as a Mendix error rather than as a silently mangled setting. Verified on real projects with mx check: 11.12.2 given 'Java21' stores '21' and checks clean on both engines (pre-fix binary reproduces the nightly error exactly); 11.6.6 given '21' stores 'Java21', 0 errors. Also routes the unused third copy in modelsdk/mpr/serialize_services.go through the same helper, so wiring it up later cannot reintroduce this. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA --- .claude/skills/fix-issue.md | 1 + .claude/skills/mendix/project-settings.md | 11 +++- docs-site/src/language/project-settings.md | 5 ++ .../759-java-version-value-dialect.mdl | 32 +++++++++++ .../14-project-settings-examples.mdl | 4 ++ .../modelsdk/settings_write_759_test.go | 54 +++++++++++++++++++ mdl/settingsoverlay/settingsoverlay.go | 45 ++++++++++++++-- mdl/settingsoverlay/settingsoverlay_test.go | 47 ++++++++++++++++ modelsdk/mpr/serialize_services.go | 4 +- 9 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 mdl-examples/bug-tests/759-java-version-value-dialect.mdl diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 5ff838e0d..0536a7267 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -319,6 +319,7 @@ cases for these three BSON types — they fell to `default: return nil`. | `UPDATE SECURITY`, `CREATE ASSOCIATION` or any `GRANT` silently strips **inherited** members from a specialized entity's access rules; `GRANT` naming an inherited member reports success and persists nothing, so REVOKE+GRANT cannot repair it. `mx check` shows only CE0066, hiding the CE2729 "No read access to attribute" cascade until Studio Pro's Update security is clicked | Mendix inheritance is multi-table: all of a parent's attributes are members of the child, so an access rule needs a MemberAccess entry for every member, own **and** inherited, each qualified against the entity that **declares** it. Both the GRANT builder and `ReconcileMemberAccesses` enumerated only `entity.Attributes`, so an inherited reference matched nothing and was deleted as stale — and reconciliation runs **immediately after every GRANT**, deleting what the grant had just written correctly | `mdl/executor/entity_hierarchy.go` (`EntityMembers`), `mdl/executor/cmd_security_write.go` (`execGrantEntityAccess`), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`, `attrRefBelongsTo`), `sdk/mpr/writer_security.go` (legacy engine) | Walk the generalization chain and qualify each member against its declaring entity; in the reconciler, only strip a reference qualified to **this** entity — an ancestor may live in another module or System, neither loaded there, so preserve what cannot be validated. **Two facts must be established against `mx check`, never inferred**: (a) the child-qualified form is CE1613 "attribute no longer exists" while the declaring-entity form validates clean; (b) `System.User`'s members are the exception — entities specialising it are *user entities* whose platform members Mendix manages, and listing them turns a clean rule into CE0066, while omitting `System.FileDocument`'s six members is CE0066 until all are present. **Generalisable**: when a post-write reconcile pass validates against a narrower model than the writer used, it will quietly undo correct writes — check what runs *after* a write before concluding the writer is at fault. Repro `mdl-examples/bug-tests/758-inherited-member-access.mdl`. Issues #758, #765 (umbrella; #451 is the same declaring-entity rule in the change-object writer) | | `describe` (and `context` / `diff-local`) renders a Retrieve's XPath with only its **first** predicate group — `where A/B[EndDate = $X];` when the BSON holds `[A/B[EndDate = $X]][Status != 'Completed'][CompletionDate = empty]`. No warning; the output reads as a complete but materially *less restrictive* query, so correct defensive code looks buggy | The grammar's `xpathConstraint` rule matches ONE bracket group, and Mendix concatenates siblings. `ParseXPathConstraint` removes the error listeners, so ANTLR parsed group 1, left the rest on the token stream, and **still returned ok=true**; `enrichXPathConstraintForDescribe` treated that as a full parse and re-rendered only what came back. The `if !ok { return original }` fallback never fired | `mdl/visitor/visitor_xpath_public.go` (`ParseXPathConstraint`), `mdl/visitor/xpath_groups.go` (`SplitXPathPredicateGroups`), `mdl/executor/cmd_microflows_format_action.go` (`enrichXPathGroups`, and the render-path split) | Two layers. (1) Reject a partial parse — after the rule, require `stream.LA(1) == antlr.TokenEOF`; that alone stops the loss, since the caller then falls back to the stored string. (2) Split into top-level groups and enrich each, so enrichment still reaches groups after the first. The splitter must track **nesting depth and quoting**: a naive `][` split mangles a nested `[A/B[x = 1]]` and a literal containing `]`. **Generalisable**: a parser that silently accepts a prefix is worse than one that fails — any `ok` returned by a rule that can match less than its input must be checked against EOF before callers treat it as lossless. Repro `mdl-examples/bug-tests/772-xpath-constraint-groups.mdl`; A/B against a pre-fix binary on the same project shows the two dropped groups. Issue #772 | | An import/export mapping over an entity created with `EXTENDS` maps only its **own** attributes; every inherited field shows unmapped in Studio Pro, and `mx check` reports CE1613 "The selected attribute 'Mod.Child.Attr' no longer exists". An inherited Boolean/DateTime element also gets `DataType=String` | The mapping builder prefixed the entity being mapped unconditionally (`attr = parentEntity + "." + attr`), but a member reference is qualified against the entity that **declares** it — the same rule as entity access rules (#758) and the change-object writer (#451). Separately `resolveAttributeType` scanned only the entity's own attributes and fell through to its `"String"` default | `mdl/executor/cmd_import_mappings.go` and `cmd_export_mappings.go` (both carry the same two lines), `mdl/executor/entity_hierarchy.go` (`ResolveMemberRef`, `ResolveMemberType`) | Route both sites through the generalization walk added for #758: `ResolveMemberRef` returns the declaring-entity reference and `ResolveMemberType` finds the type up the chain, each falling back to the old behaviour when the member cannot be resolved. **Watch for the sibling defect**: the old `resolveAttributeType` matched entities **by name across every domain model**, so a same-named entity in another module could win — resolve the module by name instead. **Generalisable**: when one rule has several call sites, a fix at one of them proves nothing about the others; grep for the *pattern* (`range entity.Attributes`, `parentEntity + "."`) rather than the reported symptom. Repro `mdl-examples/bug-tests/703-mapping-inherited-attributes.mdl`; A/B on the same project shows `Map703.Contract.DocName` (CE1613) become `Map703.DocumentBase.DocName`. Issue #703, umbrella #765 | +| `alter settings model JavaVersion = 'Java21'` on Mendix 11.12+ produces a project mxbuild refuses to **load**: `mx check` reports `System.ArgumentOutOfRangeException ... (Parameter 'majorVersion is an unsupported value: Java21')` at `JavaVersionExtensions.fromString`. Every check downstream of the settings unit is lost with it | Mendix renamed the property between 11.6 (`JavaVersion` = `"Java21"`) and 11.12 (`JavaMajorVersion` = `"21"`) — and the rename changed the **value format** as well as the key. The #759 fix followed only the key, writing the caller's value through verbatim, so the 11.6 spelling landed in the 11.12 key | `mdl/settingsoverlay/settingsoverlay.go` (`JavaVersionValue`, `SetJavaVersion`) — shared by both engines; the dead third copy in `modelsdk/mpr/serialize_services.go` carried it too | Render the value in the dialect the stored key expects: strip/add the `Java` prefix per key, and pass an unrecognisable value through untouched so a typo surfaces as a Mendix error instead of a mangled setting. **Generalisable**: a renamed property is not only a renamed key — check whether the value encoding moved with it, and cover *both* directions (either spelling in, document's dialect out). Note the sharper failure mode: the original #759 shape was an unknown property, which mxbuild **tolerates**, so only Studio Pro broke; a wrong *value* for a known enum is a hard build failure, which is why this one surfaced as a red nightly rather than a user report. Repro `mdl-examples/bug-tests/759-java-version-value-dialect.mdl`. Issue #759 (follow-up) | **Key insight:** `microflows$ListRange` stores offset/limit inside a nested `CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before diff --git a/.claude/skills/mendix/project-settings.md b/.claude/skills/mendix/project-settings.md index f6a10b2e8..ce601688f 100644 --- a/.claude/skills/mendix/project-settings.md +++ b/.claude/skills/mendix/project-settings.md @@ -30,12 +30,21 @@ alter settings model BeforeShutdownMicroflow = 'Module.MF_Shutdown'; alter settings model HealthCheckMicroflow = 'Module.MF_HealthCheck'; alter settings model HashAlgorithm = 'BCrypt'; alter settings model BcryptCost = 12; -alter settings model JavaVersion = 'Java21'; +alter settings model JavaVersion = 'Java21'; -- or '21'; see note below alter settings model RoundingMode = 'HalfUp'; alter settings model AllowUserMultipleSessions = true; alter settings model ScheduledEventTimeZoneCode = 'Etc/UTC'; ``` +**JavaVersion spelling.** Mendix renamed this property between versions: up to 11.6 +it stores `JavaVersion` = `'Java21'`, from 11.12 it stores `JavaMajorVersion` = +`'21'`. Write either spelling — mxcli reads which one the project uses and stores +the value in that dialect. Getting this wrong is not a cosmetic difference: 11.12 +parses the bare major and rejects the project outright with +`ArgumentOutOfRangeException: majorVersion is an unsupported value: Java21`. +`describe settings` always emits the project's own spelling, so its output replays +cleanly. + ### Modify Configuration Settings ```sql diff --git a/docs-site/src/language/project-settings.md b/docs-site/src/language/project-settings.md index 5ecd76212..3e5c58103 100644 --- a/docs-site/src/language/project-settings.md +++ b/docs-site/src/language/project-settings.md @@ -32,6 +32,11 @@ ALTER SETTINGS MODEL HashAlgorithm = 'BCrypt'; ALTER SETTINGS MODEL JavaVersion = '17'; ``` +Mendix renamed the Java version property between versions — up to 11.6 it is stored +as `JavaVersion` = `Java21`, from 11.12 as `JavaMajorVersion` = `21`. Write either +spelling (`'17'` or `'Java17'`): mxcli stores the value in whichever dialect the +project already uses. + ### Configuration Settings Server configuration settings like database type, URL, and HTTP port. Each configuration is identified by name (commonly `'default'`): diff --git a/mdl-examples/bug-tests/759-java-version-value-dialect.mdl b/mdl-examples/bug-tests/759-java-version-value-dialect.mdl new file mode 100644 index 000000000..423684f33 --- /dev/null +++ b/mdl-examples/bug-tests/759-java-version-value-dialect.mdl @@ -0,0 +1,32 @@ +-- Bug #759 (follow-up): the Java version *value* format is version-specific too +-- +-- Symptom: on Mendix 11.12 an `alter settings model JavaVersion = 'Java21'` +-- produced a project mx check refuses to load at all: +-- +-- ERROR: System.ArgumentOutOfRangeException: Specified argument was out of the +-- range of valid values. (Parameter 'majorVersion is an unsupported value: Java21') +-- at Mendix.Modeler.Settings.JavaVersionExtensions.fromString(String majorVersion) +-- at Mendix.Modeler.Settings.RuntimeSettings.get_JavaVersion() +-- +-- Cause: the #759 fix followed the *key* rename ("JavaVersion" up to 11.6, +-- "JavaMajorVersion" from 11.12) but wrote the value through verbatim. The rename +-- changed the value format as well: 11.6 stores the enum member "Java21", 11.12 +-- the bare major "21". Writing "Java21" into JavaMajorVersion is what +-- JavaVersionExtensions.fromString throws on. +-- +-- Before the fix this was worse than a wrong value: unlike the unknown-property +-- shape of the original #759, this one is a *hard* mxbuild failure — the whole +-- project fails to load, so nothing downstream of it can be checked either. +-- +-- Fix: settingsoverlay.JavaVersionValue renders the value in the dialect the +-- stored key expects. Either spelling is accepted on input. +-- +-- Verify: run against an 11.12+ project, then `mx check` — it must load and +-- report no settings error. On an 11.6 project the same script stores 'Java21'. + +-- Both spellings are accepted; both land as the project's own dialect. +alter settings model JavaVersion = 'Java21'; +alter settings model JavaVersion = '21'; + +-- Round-trip: describe emits the project's spelling, which must replay cleanly. +describe settings; diff --git a/mdl-examples/doctype-tests/14-project-settings-examples.mdl b/mdl-examples/doctype-tests/14-project-settings-examples.mdl index 25520c5a9..5d59cbac0 100644 --- a/mdl-examples/doctype-tests/14-project-settings-examples.mdl +++ b/mdl-examples/doctype-tests/14-project-settings-examples.mdl @@ -36,6 +36,10 @@ alter settings model AfterStartupMicroflow = 'MyModule.ASU_Startup'; /** * Example 1.2: Configure hash algorithm and Java version + * + * Either spelling of the Java version is accepted -- 'Java21' or '21'. Mendix + * stores it as "JavaVersion" = "Java21" up to 11.6 and as "JavaMajorVersion" = + * "21" from 11.12; mxcli writes whichever dialect the project already uses. */ alter settings model HashAlgorithm = 'BCrypt', JavaVersion = 'Java21'; diff --git a/mdl/backend/modelsdk/settings_write_759_test.go b/mdl/backend/modelsdk/settings_write_759_test.go index 51a475cca..ba9e6a5ee 100644 --- a/mdl/backend/modelsdk/settings_write_759_test.go +++ b/mdl/backend/modelsdk/settings_write_759_test.go @@ -97,6 +97,60 @@ func TestUpdateProjectSettings_JavaVersionKeyFollowsDocument(t *testing.T) { } } +// TestUpdateProjectSettings_JavaVersionValueMatchesKey covers the follow-up to +// #759: the rename changed the value format along with the key. Mendix 11.12 +// parses JavaMajorVersion with JavaVersionExtensions.fromString, which throws +// ArgumentOutOfRangeException ("majorVersion is an unsupported value: Java21") on +// the 11.6 spelling — so writing the value through verbatim produced a project +// mx check refuses to load. Either spelling on input, the document's own dialect +// on disk. +func TestUpdateProjectSettings_JavaVersionValueMatchesKey(t *testing.T) { + tests := []struct { + name string + storedKey string + seed string + set string + want string + }{ + {"11_12_given_enum_spelling", "JavaMajorVersion", "21", "Java17", "17"}, + {"11_12_given_bare_major", "JavaMajorVersion", "21", "17", "17"}, + {"11_6_given_enum_spelling", "JavaVersion", "Java21", "Java17", "Java17"}, + {"11_6_given_bare_major", "JavaVersion", "Java21", "17", "Java17"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + proj := copyFixture(t) + seedJavaVersionKey(t, proj, tc.storedKey, tc.seed) + + setJavaVersion(t, proj, tc.set) + + if got := readModelSettings(t, proj)[tc.storedKey]; got != tc.want { + t.Errorf("%s = %v, want %q (set %q)", tc.storedKey, got, tc.want, tc.set) + } + }) + } +} + +// setJavaVersion drives one ALTER SETTINGS MODEL JavaVersion through the backend. +func setJavaVersion(t *testing.T, proj, v string) { + t.Helper() + b := New() + if err := b.Connect(proj); err != nil { + t.Fatalf("connect: %v", err) + } + ps, err := b.GetProjectSettings() + if err != nil { + t.Fatalf("GetProjectSettings: %v", err) + } + if ps.Model == nil { + t.Fatal("fixture has no model settings") + } + ps.Model.JavaVersion = v + if err := b.UpdateProjectSettings(ps); err != nil { + t.Fatalf("UpdateProjectSettings: %v", err) + } +} + // seedJavaVersionKey rewrites the fixture's Settings$ModelSettings part so it // carries exactly one Java-version key, standing in for the Mendix version that // spells it that way. diff --git a/mdl/settingsoverlay/settingsoverlay.go b/mdl/settingsoverlay/settingsoverlay.go index a3a7c52a1..615eafe5f 100644 --- a/mdl/settingsoverlay/settingsoverlay.go +++ b/mdl/settingsoverlay/settingsoverlay.go @@ -134,6 +134,13 @@ func newServerConfiguration(cfg *model.ServerConfiguration, siblings []map[strin } } +// The two spellings of the runtime Java version property. They differ in value +// format as well as in name — see JavaVersionValue. +const ( + JavaMajorVersionKey = "JavaMajorVersion" // Mendix 11.12+, e.g. "21" + JavaVersionEnumKey = "JavaVersion" // Mendix 11.6, e.g. "Java21" +) + // JavaVersionKey returns the storage key a Settings$ModelSettings part uses for // the runtime Java version, or "" when it carries neither. // @@ -146,7 +153,7 @@ func newServerConfiguration(cfg *model.ServerConfiguration, siblings []map[strin // (mendixlabs/mxcli#759). Read the key off the document instead of assuming one, // and never invent a key the document does not already have. func JavaVersionKey(raw map[string]any) string { - for _, k := range []string{"JavaMajorVersion", "JavaVersion"} { + for _, k := range []string{JavaMajorVersionKey, JavaVersionEnumKey} { if _, ok := raw[k]; ok { return k } @@ -165,12 +172,42 @@ func JavaVersion(raw map[string]any) string { return v } -// SetJavaVersion writes the runtime Java version back to the key it was read from. -// A part carrying neither key is left untouched. +// SetJavaVersion writes the runtime Java version back to the key it was read from, +// in the value format that key expects. A part carrying neither key is left +// untouched. func SetJavaVersion(raw map[string]any, v string) { if k := JavaVersionKey(raw); k != "" { - raw[k] = v + raw[k] = JavaVersionValue(k, v) + } +} + +// JavaVersionValue renders a Java version in the form the given storage key holds: +// "JavaVersion" carries the enum member ("Java21"), "JavaMajorVersion" the bare +// major ("21"). +// +// The rename in #759 changed the value format along with the key, and following +// only the key is not enough: Mendix 11.12 parses JavaMajorVersion with +// JavaVersionExtensions.fromString, which throws ArgumentOutOfRangeException +// ("majorVersion is an unsupported value: Java21") on the 11.6 spelling. So an +// `alter settings model JavaVersion = 'Java21'` written verbatim onto an 11.12 +// document produces a project mx check refuses to load. Either spelling is +// accepted on input and stored in the document's own dialect. +// +// A value that is neither spelling — no recognisable major version — is passed +// through untouched, so a typo surfaces as a Mendix error rather than as a +// silently mangled setting. +func JavaVersionValue(key, v string) string { + major := strings.TrimSpace(v) + if len(major) >= 4 && strings.EqualFold(major[:4], "Java") { + major = major[4:] + } + if major == "" || strings.TrimLeft(major, "0123456789") != "" { + return v + } + if key == JavaMajorVersionKey { + return major } + return "Java" + major } // ConstantValues rebuilds a configuration's ConstantValues list, updating each diff --git a/mdl/settingsoverlay/settingsoverlay_test.go b/mdl/settingsoverlay/settingsoverlay_test.go index eef02b4d9..989ab1498 100644 --- a/mdl/settingsoverlay/settingsoverlay_test.go +++ b/mdl/settingsoverlay/settingsoverlay_test.go @@ -365,3 +365,50 @@ func TestJavaVersionKey_FollowsDocument(t *testing.T) { }) } } + +// TestJavaVersionValue_MatchesKeyDialect is the follow-up to +// TestJavaVersionKey_FollowsDocument: the rename changed the value format along +// with the key, so following only the key still produced a project Mendix 11.12 +// refuses to load. Its JavaVersionExtensions.fromString parses JavaMajorVersion as +// a bare major and throws ArgumentOutOfRangeException ("majorVersion is an +// unsupported value: Java21") on the 11.6 spelling. +func TestJavaVersionValue_MatchesKeyDialect(t *testing.T) { + tests := []struct { + key string + in string + want string + }{ + {JavaMajorVersionKey, "Java21", "21"}, + {JavaMajorVersionKey, "21", "21"}, + {JavaMajorVersionKey, "java17", "17"}, + {JavaVersionEnumKey, "Java21", "Java21"}, + {JavaVersionEnumKey, "21", "Java21"}, + {JavaVersionEnumKey, " 17 ", "Java17"}, + // Not a recognisable version: passed through so the typo surfaces as a + // Mendix error rather than as a silently mangled setting. + {JavaMajorVersionKey, "Temurin", "Temurin"}, + {JavaVersionEnumKey, "Java-21", "Java-21"}, + {JavaMajorVersionKey, "", ""}, + } + for _, tc := range tests { + if got := JavaVersionValue(tc.key, tc.in); got != tc.want { + t.Errorf("JavaVersionValue(%q, %q) = %q, want %q", tc.key, tc.in, got, tc.want) + } + } +} + +// TestSetJavaVersion_ConvertsToStoredDialect: one MDL statement must work on both +// Mendix versions, whichever spelling the author used. +func TestSetJavaVersion_ConvertsToStoredDialect(t *testing.T) { + mendix1112 := map[string]any{JavaMajorVersionKey: "21"} + SetJavaVersion(mendix1112, "Java17") + if got := mendix1112[JavaMajorVersionKey]; got != "17" { + t.Errorf("%s = %#v, want %q", JavaMajorVersionKey, got, "17") + } + + mendix116 := map[string]any{JavaVersionEnumKey: "Java21"} + SetJavaVersion(mendix116, "17") + if got := mendix116[JavaVersionEnumKey]; got != "Java17" { + t.Errorf("%s = %#v, want %q", JavaVersionEnumKey, got, "Java17") + } +} diff --git a/modelsdk/mpr/serialize_services.go b/modelsdk/mpr/serialize_services.go index 7f493b4bc..a2cb94f4d 100644 --- a/modelsdk/mpr/serialize_services.go +++ b/modelsdk/mpr/serialize_services.go @@ -7,6 +7,7 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" + "github.com/mendixlabs/mxcli/mdl/settingsoverlay" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" ) @@ -191,7 +192,8 @@ func serPSModelSettings(ms *model.ModelSettings, raw map[string]any) map[string] raw["AllowUserMultipleSessions"] = ms.AllowUserMultipleSessions raw["HashAlgorithm"] = ms.HashAlgorithm raw["BcryptCost"] = serPSInt64(ms.BcryptCost) - raw["JavaVersion"] = ms.JavaVersion + // Version-specific key AND value format — see settingsoverlay.JavaVersionValue. + settingsoverlay.SetJavaVersion(raw, ms.JavaVersion) raw["RoundingMode"] = ms.RoundingMode raw["ScheduledEventTimeZoneCode"] = ms.ScheduledEventTimeZoneCode raw["FirstDayOfWeek"] = ms.FirstDayOfWeek