Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion .claude/skills/mendix/project-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/language/project-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'`):
Expand Down
32 changes: 32 additions & 0 deletions mdl-examples/bug-tests/759-java-version-value-dialect.mdl
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions mdl-examples/doctype-tests/14-project-settings-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
54 changes: 54 additions & 0 deletions mdl/backend/modelsdk/settings_write_759_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 41 additions & 4 deletions mdl/settingsoverlay/settingsoverlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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
}
Expand All @@ -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
Expand Down
47 changes: 47 additions & 0 deletions mdl/settingsoverlay/settingsoverlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
4 changes: 3 additions & 1 deletion modelsdk/mpr/serialize_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
Loading