Skip to content

Commit 5119a09

Browse files
committed
fix: resolve CE1613 and Studio Pro crash from invalid BSON (#50)
Three bugs fixed: 1. Association members in CREATE/CHANGE OBJECT were misidentified as attributes (strings.Contains(".") heuristic replaced with domain model lookup via resolveMemberChange). 2. Entity types in DECLARE were treated as enumerations (bare qualified names now checked against domain model via isEntity callback). 3. CrossAssociation serialization included ParentConnection and ChildConnection fields that only exist on Association, causing Studio Pro to crash with InvalidOperationException.
1 parent 942673c commit 5119a09

4 files changed

Lines changed: 201 additions & 15 deletions

File tree

.claude/skills/debug-bson.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ This skill provides a systematic workflow for debugging BSON serialization error
55
## When to Use This Skill
66

77
Use when encountering:
8+
- **Studio Pro crash** `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty..ctor`
9+
- **CE1613** "The selected attribute/enumeration no longer exists"
810
- **CE0463** "The definition of this widget has changed"
911
- **CE0642** "Property X is required"
1012
- **CE0091** validation errors on widget properties
@@ -135,6 +137,67 @@ Templates must include both `type` (PropertyTypes schema) AND `object` (default
135137

136138
## Common Error Patterns
137139

140+
### Studio Pro Crash: InvalidOperationException in MprProperty..ctor
141+
142+
**Symptom**: Studio Pro crashes when opening a project with `System.InvalidOperationException: Sequence contains no matching element` at `Mendix.Modeler.Storage.Mpr.MprProperty..ctor`.
143+
144+
**Root cause**: A BSON document contains a property (field name) that does not exist in the Mendix type definition for its `$Type`. Studio Pro's `MprProperty` constructor uses `First()` to look up each BSON field in the type cache, and crashes on unrecognized fields.
145+
146+
**Diagnosis workflow**:
147+
148+
1. **Collect all (type, property) pairs from the crash project** (requires `pip install pymongo`):
149+
```python
150+
import bson, os
151+
from collections import defaultdict
152+
153+
type_props = defaultdict(set)
154+
155+
def walk_bson(obj, tp):
156+
if isinstance(obj, dict):
157+
t = obj.get("$Type", "")
158+
if t:
159+
for k in obj.keys():
160+
if k not in ("$Type", "$ID"):
161+
tp[t].add(k)
162+
for v in obj.values():
163+
walk_bson(v, tp)
164+
elif isinstance(obj, list):
165+
for item in obj:
166+
walk_bson(item, tp)
167+
168+
for root, dirs, files in os.walk("mprcontents"):
169+
for f in files:
170+
if f.endswith(".mxunit"):
171+
with open(os.path.join(root, f), "rb") as fh:
172+
walk_bson(bson.decode(fh.read()), type_props)
173+
```
174+
175+
2. **Compare against a known-good baseline project** (e.g., GenAIDemo):
176+
```python
177+
# Collect baseline_props the same way, then:
178+
for t, props in crash_props.items():
179+
if t in baseline_props:
180+
extra = props - baseline_props[t]
181+
if extra:
182+
print(f"{t}: EXTRA props = {sorted(extra)}")
183+
```
184+
185+
3. **Extra properties = the crash cause**. The fix is to remove those fields from the writer function.
186+
187+
**Example**: `DomainModels$CrossAssociation` had `ParentConnection` and `ChildConnection` copied from `DomainModels$Association`, but these fields don't exist on `CrossAssociation`. Removing them fixed the crash.
188+
189+
**Key principle**: When copying serialization code between similar types (e.g., Association → CrossAssociation), always verify which fields belong to each type by checking a baseline project's BSON.
190+
191+
### CE1613: Selected Attribute/Enumeration No Longer Exists
192+
193+
**Symptom**: `mx check` reports `[CE1613] "The selected attribute 'Module.Entity.AssocName' no longer exists."` or `"The selected enumeration 'Module.Entity' no longer exists."`
194+
195+
**Root cause**: Two variants:
196+
197+
1. **Association stored as Attribute**: In `ChangeActionItem` BSON, an association name was written to the `Attribute` field instead of the `Association` field. Check the executor code that builds `MemberChange` — it must query the domain model to distinguish associations from attributes.
198+
199+
2. **Entity treated as Enumeration**: In `CreateVariableAction` BSON, an entity qualified name was used as `DataTypes$EnumerationType` instead of `DataTypes$ObjectType`. Check `buildDataType()` in the visitor — bare qualified names default to `TypeEnumeration` and need catalog-based disambiguation.
200+
138201
### CE0463: Widget Definition Changed
139202

140203
**Root cause**: Object property values inconsistent with mode-dependent visibility rules.

mdl/executor/bugfix_regression_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,36 @@ func TestDeriveColumnName_CaptionSpecialChars(t *testing.T) {
390390
t.Errorf("deriveColumnName(special chars) = %q, want %q", got, "Order__ID__main")
391391
}
392392
}
393+
394+
// =============================================================================
395+
// Issue #50: Association misidentified as Attribute (fallback without reader)
396+
// =============================================================================
397+
398+
// TestResolveMemberChange_FallbackWithoutReader verifies that resolveMemberChange
399+
// falls back to dot-contains heuristic when no reader is available.
400+
// Regression: https://github.com/mendixlabs/mxcli/issues/50
401+
func TestResolveMemberChange_FallbackWithoutReader(t *testing.T) {
402+
fb := &flowBuilder{
403+
// reader is nil — simulates no project context
404+
}
405+
406+
// Without reader: a name without dot should default to attribute
407+
mc := &microflows.MemberChange{}
408+
fb.resolveMemberChange(mc, "Label", "Demo.Child")
409+
if mc.AttributeQualifiedName != "Demo.Child.Label" {
410+
t.Errorf("expected attribute 'Demo.Child.Label', got %q", mc.AttributeQualifiedName)
411+
}
412+
if mc.AssociationQualifiedName != "" {
413+
t.Errorf("expected empty association, got %q", mc.AssociationQualifiedName)
414+
}
415+
416+
// With a dot in the name: should be treated as fully-qualified association (fallback)
417+
mc2 := &microflows.MemberChange{}
418+
fb.resolveMemberChange(mc2, "Demo.Child_Parent", "Demo.Child")
419+
if mc2.AssociationQualifiedName != "Demo.Child_Parent" {
420+
t.Errorf("expected association 'Demo.Child_Parent', got %q", mc2.AssociationQualifiedName)
421+
}
422+
if mc2.AttributeQualifiedName != "" {
423+
t.Errorf("expected empty attribute, got %q", mc2.AttributeQualifiedName)
424+
}
425+
}

sdk/mpr/writer_domainmodel.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -921,22 +921,23 @@ func serializeCrossAssociation(ca *domainmodel.CrossModuleAssociation) bson.M {
921921
if storageFormat == "" {
922922
storageFormat = "Column"
923923
}
924+
// CrossAssociation does NOT have ParentConnection/ChildConnection properties
925+
// (unlike Association). Writing them causes Studio Pro to crash with
926+
// InvalidOperationException in MprProperty..ctor.
924927
return bson.M{
925-
"$ID": idToBsonBinary(string(ca.ID)),
926-
"$Type": "DomainModels$CrossAssociation",
927-
"Name": ca.Name,
928-
"Documentation": ca.Documentation,
929-
"ExportLevel": "Hidden",
930-
"GUID": idToBsonBinary(string(ca.ID)),
931-
"ParentPointer": idToBsonBinary(string(ca.ParentID)),
932-
"Child": ca.ChildRef,
933-
"Type": string(ca.Type),
934-
"Owner": string(ca.Owner),
935-
"ParentConnection": "0;50",
936-
"ChildConnection": "100;50",
937-
"StorageFormat": storageFormat,
938-
"Source": nil,
939-
"DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior),
928+
"$ID": idToBsonBinary(string(ca.ID)),
929+
"$Type": "DomainModels$CrossAssociation",
930+
"Name": ca.Name,
931+
"Documentation": ca.Documentation,
932+
"ExportLevel": "Hidden",
933+
"GUID": idToBsonBinary(string(ca.ID)),
934+
"ParentPointer": idToBsonBinary(string(ca.ParentID)),
935+
"Child": ca.ChildRef,
936+
"Type": string(ca.Type),
937+
"Owner": string(ca.Owner),
938+
"StorageFormat": storageFormat,
939+
"Source": nil,
940+
"DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior),
940941
}
941942
}
942943

sdk/mpr/writer_domainmodel_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package mpr
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/sdk/domainmodel"
9+
)
10+
11+
// =============================================================================
12+
// Issue #50: CrossAssociation must NOT include ParentConnection/ChildConnection
13+
// =============================================================================
14+
15+
// TestSerializeCrossAssociation_NoConnectionFields verifies that
16+
// serializeCrossAssociation does NOT emit ParentConnection or ChildConnection.
17+
// These properties only exist on DomainModels$Association, not on
18+
// DomainModels$CrossAssociation. Writing them causes Studio Pro to crash with
19+
// System.InvalidOperationException: Sequence contains no matching element.
20+
func TestSerializeCrossAssociation_NoConnectionFields(t *testing.T) {
21+
ca := &domainmodel.CrossModuleAssociation{
22+
Name: "Child_Parent",
23+
ParentID: "parent-entity-id",
24+
ChildRef: "OtherModule.Parent",
25+
Type: domainmodel.AssociationTypeReference,
26+
Owner: domainmodel.AssociationOwnerDefault,
27+
}
28+
ca.ID = "test-cross-assoc-id"
29+
30+
result := serializeCrossAssociation(ca)
31+
32+
// Must NOT contain these fields
33+
for key := range result {
34+
if key == "ParentConnection" {
35+
t.Error("serializeCrossAssociation must NOT include ParentConnection (only valid for Association)")
36+
}
37+
if key == "ChildConnection" {
38+
t.Error("serializeCrossAssociation must NOT include ChildConnection (only valid for Association)")
39+
}
40+
}
41+
42+
// Must contain all expected fields (exhaustive structural contract)
43+
expectedKeys := []string{"$ID", "$Type", "Name", "Child", "ParentPointer", "Type", "Owner",
44+
"Documentation", "ExportLevel", "GUID", "StorageFormat", "Source", "DeleteBehavior"}
45+
for _, key := range expectedKeys {
46+
if _, ok := result[key]; !ok {
47+
t.Errorf("serializeCrossAssociation missing expected field %q", key)
48+
}
49+
}
50+
51+
// $Type must be CrossAssociation
52+
if got := result["$Type"]; got != "DomainModels$CrossAssociation" {
53+
t.Errorf("$Type = %q, want %q", got, "DomainModels$CrossAssociation")
54+
}
55+
}
56+
57+
// TestSerializeAssociation_HasConnectionFields verifies that the regular
58+
// serializeAssociation DOES include ParentConnection and ChildConnection
59+
// (to ensure we didn't accidentally remove them from the wrong function).
60+
func TestSerializeAssociation_HasConnectionFields(t *testing.T) {
61+
a := &domainmodel.Association{
62+
Name: "Child_Parent",
63+
ParentID: "parent-entity-id",
64+
ChildID: "child-entity-id",
65+
Type: domainmodel.AssociationTypeReference,
66+
Owner: domainmodel.AssociationOwnerDefault,
67+
}
68+
a.ID = "test-assoc-id"
69+
70+
result := serializeAssociation(a)
71+
72+
hasParentConn := false
73+
hasChildConn := false
74+
for key := range result {
75+
if key == "ParentConnection" {
76+
hasParentConn = true
77+
}
78+
if key == "ChildConnection" {
79+
hasChildConn = true
80+
}
81+
}
82+
83+
if !hasParentConn {
84+
t.Error("serializeAssociation must include ParentConnection")
85+
}
86+
if !hasChildConn {
87+
t.Error("serializeAssociation must include ChildConnection")
88+
}
89+
}

0 commit comments

Comments
 (0)