Skip to content
Open
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
4 changes: 2 additions & 2 deletions mdl/executor/cmd_microflows_builder_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID {
memberChange := &microflows.MemberChange{
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
Type: microflows.MemberChangeTypeSet,
Value: fb.exprToString(change.Value),
Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute),
}
fb.resolveMemberChange(memberChange, change.Attribute, entityQN)
action.InitialMembers = append(action.InitialMembers, memberChange)
Expand Down Expand Up @@ -257,7 +257,7 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
memberChange := &microflows.MemberChange{
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
Type: microflows.MemberChangeTypeSet,
Value: fb.exprToString(change.Value),
Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute),
}
fb.resolveMemberChange(memberChange, change.Attribute, entityQN)
action.Changes = append(action.Changes, memberChange)
Expand Down
66 changes: 66 additions & 0 deletions mdl/executor/cmd_microflows_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/domainmodel"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

Expand Down Expand Up @@ -184,12 +185,77 @@ func expressionToXPath(expr ast.Expression) string {
return "empty"
}
return expressionToString(expr)
case *ast.QualifiedNameExpr:
return qualifiedNameToXPath(e)
default:
// For all other expression types, the standard serialization is correct
return expressionToString(expr)
}
}

// qualifiedNameToXPath converts a QualifiedNameExpr to XPath format.
// For enum value references (3-part: Module.EnumName.Value), XPath requires
// just the value name in quotes: 'Value'. For 2-part names (associations,
// entity references), returns the qualified name as-is.
func qualifiedNameToXPath(e *ast.QualifiedNameExpr) string {
// 3-part names (Name contains a dot) are enum references: Module.EnumName.Value
if dotIdx := strings.LastIndex(e.QualifiedName.Name, "."); dotIdx >= 0 {
valueName := e.QualifiedName.Name[dotIdx+1:]
return "'" + valueName + "'"
}
return e.QualifiedName.String()
}

// memberExpressionToString converts an AST Expression to a Mendix expression string,
// resolving enum string literals to qualified enum names when the attribute type is known.
// For example, 'Processing' becomes MyModule.ENUM_Status.Processing when the attribute
// is of type Enumeration(MyModule.ENUM_Status).
func (fb *flowBuilder) memberExpressionToString(expr ast.Expression, entityQN, attrName string) string {
// Only transform string literals for enum attributes
if lit, ok := expr.(*ast.LiteralExpr); ok && lit.Kind == ast.LiteralString {
if enumRef := fb.lookupEnumRef(entityQN, attrName); enumRef != "" {
// Convert 'Value' to Module.EnumName.Value
return enumRef + "." + fmt.Sprintf("%v", lit.Value)
}
}
return fb.exprToString(expr)
}

// lookupEnumRef returns the enumeration qualified name (e.g., "MyModule.ENUM_Status")
// for an attribute if it is an enumeration type. Returns "" if the attribute is not
// an enumeration or if the domain model is not available.
func (fb *flowBuilder) lookupEnumRef(entityQN, attrName string) string {
if fb.reader == nil || entityQN == "" || attrName == "" {
return ""
}
parts := strings.SplitN(entityQN, ".", 2)
if len(parts) != 2 {
return ""
}
mod, err := fb.reader.GetModuleByName(parts[0])
if err != nil || mod == nil {
return ""
}
dm, err := fb.reader.GetDomainModel(mod.ID)
if err != nil || dm == nil {
return ""
}
for _, entity := range dm.Entities {
if entity.Name == parts[1] {
for _, attr := range entity.Attributes {
if attr.Name == attrName {
if enumType, ok := attr.Type.(*domainmodel.EnumerationAttributeType); ok {
return enumType.EnumerationRef
}
return ""
}
}
return ""
}
}
return ""
}

// xpathPathExprToString serializes an XPathPathExpr to an XPath path string.
func xpathPathExprToString(path *ast.XPathPathExpr) string {
var parts []string
Expand Down
75 changes: 75 additions & 0 deletions mdl/executor/cmd_microflows_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
)

func TestQualifiedNameToXPath_EnumValue(t *testing.T) {
// 3-part names (Module.EnumName.Value) should emit just the value in quotes
expr := &ast.QualifiedNameExpr{
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "ENUM_Status.Processing"},
}
got := qualifiedNameToXPath(expr)
want := "'Processing'"
if got != want {
t.Errorf("qualifiedNameToXPath(%q) = %q, want %q", expr.QualifiedName.String(), got, want)
}
}

func TestQualifiedNameToXPath_NonEnum(t *testing.T) {
// 2-part names (Module.AssocName) should pass through as-is
expr := &ast.QualifiedNameExpr{
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "SomeAssoc"},
}
got := qualifiedNameToXPath(expr)
want := "MyModule.SomeAssoc"
if got != want {
t.Errorf("qualifiedNameToXPath(%q) = %q, want %q", expr.QualifiedName.String(), got, want)
}
}

func TestExpressionToXPath_EnumInComparison(t *testing.T) {
// WHERE Status = Module.ENUM.Value should produce: Status = 'Value'
expr := &ast.BinaryExpr{
Left: &ast.IdentifierExpr{Name: "Status"},
Operator: "=",
Right: &ast.QualifiedNameExpr{
QualifiedName: ast.QualifiedName{Module: "BST", Name: "ComplianceStatus.Rectified"},
},
}
got := expressionToXPath(expr)
want := "Status = 'Rectified'"
if got != want {
t.Errorf("expressionToXPath = %q, want %q", got, want)
}
}

func TestExpressionToXPath_StringLiteralPreserved(t *testing.T) {
// WHERE Status = 'Pending' should stay as Status = 'Pending'
expr := &ast.BinaryExpr{
Left: &ast.IdentifierExpr{Name: "Status"},
Operator: "=",
Right: &ast.LiteralExpr{Value: "Pending", Kind: ast.LiteralString},
}
got := expressionToXPath(expr)
want := "Status = 'Pending'"
if got != want {
t.Errorf("expressionToXPath = %q, want %q", got, want)
}
}

func TestExpressionToString_QualifiedNameUnchanged(t *testing.T) {
// In expression context, qualified names should remain as-is (correct for enum refs)
expr := &ast.QualifiedNameExpr{
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "ENUM_Status.Processing"},
}
got := expressionToString(expr)
want := "MyModule.ENUM_Status.Processing"
if got != want {
t.Errorf("expressionToString = %q, want %q", got, want)
}
}
6 changes: 6 additions & 0 deletions mdl/visitor/visitor_page_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,12 @@ func xpathExprToString(expr ast.Expression) string {
case *ast.IdentifierExpr:
return e.Name
case *ast.QualifiedNameExpr:
// For enum value references (3-part: Module.EnumName.Value), XPath requires
// just the value name in quotes: 'Value'.
if dotIdx := strings.LastIndex(e.QualifiedName.Name, "."); dotIdx >= 0 {
valueName := e.QualifiedName.Name[dotIdx+1:]
return "'" + valueName + "'"
}
return e.QualifiedName.String()
default:
return ""
Expand Down
33 changes: 33 additions & 0 deletions mdl/visitor/visitor_xpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,39 @@ func TestXPath_ComplexExpressions(t *testing.T) {
}
}

func TestXPath_EnumValueReference(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
"3-part enum value becomes quoted value",
"[Status = BST.ComplianceStatus.Rectified]",
"[Status = 'Rectified']",
},
{
"2-part qualified name preserved",
"[Module.Association = $object]",
"[Module.Association = $object]",
},
{
"string literal enum preserved",
"[Status = 'Active']",
"[Status = 'Active']",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := roundTripXPath(tt.input)
if got != tt.want {
t.Errorf("roundTripXPath(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

func TestXPath_ASTTypes(t *testing.T) {
t.Run("bare path creates XPathPathExpr", func(t *testing.T) {
expr := parseXPathConstraint("[Module.Assoc/Module.Entity/Attr = $val]")
Expand Down
Loading