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
63 changes: 63 additions & 0 deletions .claude/skills/debug-bson.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This skill provides a systematic workflow for debugging BSON serialization error
## When to Use This Skill

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

## Common Error Patterns

### Studio Pro Crash: InvalidOperationException in MprProperty..ctor

**Symptom**: Studio Pro crashes when opening a project with `System.InvalidOperationException: Sequence contains no matching element` at `Mendix.Modeler.Storage.Mpr.MprProperty..ctor`.

**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.

**Diagnosis workflow**:

1. **Collect all (type, property) pairs from the crash project** (requires `pip install pymongo`):
```python
import bson, os
from collections import defaultdict

type_props = defaultdict(set)

def walk_bson(obj, tp):
if isinstance(obj, dict):
t = obj.get("$Type", "")
if t:
for k in obj.keys():
if k not in ("$Type", "$ID"):
tp[t].add(k)
for v in obj.values():
walk_bson(v, tp)
elif isinstance(obj, list):
for item in obj:
walk_bson(item, tp)

for root, dirs, files in os.walk("mprcontents"):
for f in files:
if f.endswith(".mxunit"):
with open(os.path.join(root, f), "rb") as fh:
walk_bson(bson.decode(fh.read()), type_props)
```

2. **Compare against a known-good baseline project** (e.g., GenAIDemo):
```python
# Collect baseline_props the same way, then:
for t, props in crash_props.items():
if t in baseline_props:
extra = props - baseline_props[t]
if extra:
print(f"{t}: EXTRA props = {sorted(extra)}")
```

3. **Extra properties = the crash cause**. The fix is to remove those fields from the writer function.

**Example**: `DomainModels$CrossAssociation` had `ParentConnection` and `ChildConnection` copied from `DomainModels$Association`, but these fields don't exist on `CrossAssociation`. Removing them fixed the crash.

**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.

### CE1613: Selected Attribute/Enumeration No Longer Exists

**Symptom**: `mx check` reports `[CE1613] "The selected attribute 'Module.Entity.AssocName' no longer exists."` or `"The selected enumeration 'Module.Entity' no longer exists."`

**Root cause**: Two variants:

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.

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.

### CE0463: Widget Definition Changed

**Root cause**: Object property values inconsistent with mode-dependent visibility rules.
Expand Down
33 changes: 33 additions & 0 deletions mdl/executor/bugfix_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,36 @@ func TestDeriveColumnName_CaptionSpecialChars(t *testing.T) {
t.Errorf("deriveColumnName(special chars) = %q, want %q", got, "Order__ID__main")
}
}

// =============================================================================
// Issue #50: Association misidentified as Attribute (fallback without reader)
// =============================================================================

// TestResolveMemberChange_FallbackWithoutReader verifies that resolveMemberChange
// falls back to dot-contains heuristic when no reader is available.
// Regression: https://github.com/mendixlabs/mxcli/issues/50
func TestResolveMemberChange_FallbackWithoutReader(t *testing.T) {
fb := &flowBuilder{
// reader is nil — simulates no project context
}

// Without reader: a name without dot should default to attribute
mc := &microflows.MemberChange{}
fb.resolveMemberChange(mc, "Label", "Demo.Child")
if mc.AttributeQualifiedName != "Demo.Child.Label" {
t.Errorf("expected attribute 'Demo.Child.Label', got %q", mc.AttributeQualifiedName)
}
if mc.AssociationQualifiedName != "" {
t.Errorf("expected empty association, got %q", mc.AssociationQualifiedName)
}

// With a dot in the name: should be treated as fully-qualified association (fallback)
mc2 := &microflows.MemberChange{}
fb.resolveMemberChange(mc2, "Demo.Child_Parent", "Demo.Child")
if mc2.AssociationQualifiedName != "Demo.Child_Parent" {
t.Errorf("expected association 'Demo.Child_Parent', got %q", mc2.AssociationQualifiedName)
}
if mc2.AttributeQualifiedName != "" {
t.Errorf("expected empty attribute, got %q", mc2.AttributeQualifiedName)
}
}
31 changes: 16 additions & 15 deletions sdk/mpr/writer_domainmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -921,22 +921,23 @@ func serializeCrossAssociation(ca *domainmodel.CrossModuleAssociation) bson.M {
if storageFormat == "" {
storageFormat = "Column"
}
// CrossAssociation does NOT have ParentConnection/ChildConnection properties
// (unlike Association). Writing them causes Studio Pro to crash with
// InvalidOperationException in MprProperty..ctor.
return bson.M{
"$ID": idToBsonBinary(string(ca.ID)),
"$Type": "DomainModels$CrossAssociation",
"Name": ca.Name,
"Documentation": ca.Documentation,
"ExportLevel": "Hidden",
"GUID": idToBsonBinary(string(ca.ID)),
"ParentPointer": idToBsonBinary(string(ca.ParentID)),
"Child": ca.ChildRef,
"Type": string(ca.Type),
"Owner": string(ca.Owner),
"ParentConnection": "0;50",
"ChildConnection": "100;50",
"StorageFormat": storageFormat,
"Source": nil,
"DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior),
"$ID": idToBsonBinary(string(ca.ID)),
"$Type": "DomainModels$CrossAssociation",
"Name": ca.Name,
"Documentation": ca.Documentation,
"ExportLevel": "Hidden",
"GUID": idToBsonBinary(string(ca.ID)),
"ParentPointer": idToBsonBinary(string(ca.ParentID)),
"Child": ca.ChildRef,
"Type": string(ca.Type),
"Owner": string(ca.Owner),
"StorageFormat": storageFormat,
"Source": nil,
"DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior),
}
}

Expand Down
89 changes: 89 additions & 0 deletions sdk/mpr/writer_domainmodel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: Apache-2.0

package mpr

import (
"testing"

"github.com/mendixlabs/mxcli/sdk/domainmodel"
)

// =============================================================================
// Issue #50: CrossAssociation must NOT include ParentConnection/ChildConnection
// =============================================================================

// TestSerializeCrossAssociation_NoConnectionFields verifies that
// serializeCrossAssociation does NOT emit ParentConnection or ChildConnection.
// These properties only exist on DomainModels$Association, not on
// DomainModels$CrossAssociation. Writing them causes Studio Pro to crash with
// System.InvalidOperationException: Sequence contains no matching element.
func TestSerializeCrossAssociation_NoConnectionFields(t *testing.T) {
ca := &domainmodel.CrossModuleAssociation{
Name: "Child_Parent",
ParentID: "parent-entity-id",
ChildRef: "OtherModule.Parent",
Type: domainmodel.AssociationTypeReference,
Owner: domainmodel.AssociationOwnerDefault,
}
ca.ID = "test-cross-assoc-id"

result := serializeCrossAssociation(ca)

// Must NOT contain these fields
for key := range result {
if key == "ParentConnection" {
t.Error("serializeCrossAssociation must NOT include ParentConnection (only valid for Association)")
}
if key == "ChildConnection" {
t.Error("serializeCrossAssociation must NOT include ChildConnection (only valid for Association)")
}
}

// Must contain all expected fields (exhaustive structural contract)
expectedKeys := []string{"$ID", "$Type", "Name", "Child", "ParentPointer", "Type", "Owner",
"Documentation", "ExportLevel", "GUID", "StorageFormat", "Source", "DeleteBehavior"}
for _, key := range expectedKeys {
if _, ok := result[key]; !ok {
t.Errorf("serializeCrossAssociation missing expected field %q", key)
}
}

// $Type must be CrossAssociation
if got := result["$Type"]; got != "DomainModels$CrossAssociation" {
t.Errorf("$Type = %q, want %q", got, "DomainModels$CrossAssociation")
}
}

// TestSerializeAssociation_HasConnectionFields verifies that the regular
// serializeAssociation DOES include ParentConnection and ChildConnection
// (to ensure we didn't accidentally remove them from the wrong function).
func TestSerializeAssociation_HasConnectionFields(t *testing.T) {
a := &domainmodel.Association{
Name: "Child_Parent",
ParentID: "parent-entity-id",
ChildID: "child-entity-id",
Type: domainmodel.AssociationTypeReference,
Owner: domainmodel.AssociationOwnerDefault,
}
a.ID = "test-assoc-id"

result := serializeAssociation(a)

hasParentConn := false
hasChildConn := false
for key := range result {
if key == "ParentConnection" {
hasParentConn = true
}
if key == "ChildConnection" {
hasChildConn = true
}
}

if !hasParentConn {
t.Error("serializeAssociation must include ParentConnection")
}
if !hasChildConn {
t.Error("serializeAssociation must include ChildConnection")
}
}