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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-21", "symptom": "Over --mcp, `create microflow M.X ($T: System.UserType) begin … end;` fails: `ped_create_document … {\"/parameters/0/entity\":\"Reference with qualified name System.UserType of type DomainModels$Entity not found.\"}`. A module enumeration (`Rules.RuleResult`) works. On the local engines the same script reports success and DESCRIBE prints a plausible bare `$T: System.UserType`, but mx check gives `[CE1613] \"The selected entity 'System.UserType' no longer exists.\"` for the parameter and for a `returns System.DeviceType`. The explicit `$T: enum System.UserType` (what DESCRIBE emits for an enumeration) was refused: \"enumeration 'System.UserType' not found for parameter 'T'\"", "cause": "The visitor types every bare qualified name in a flow parameter or return position as TypeEntity. buildMicroflowFromStmt / buildNanoflowFromStmt / execCreateRule fall back from entity to enumeration only after the entity lookup misses, and skip that lookup entirely for the System module (isBuiltinModuleEntity: its entities are not stored in the project), so a System enumeration kept the entity guess and was written as DataTypes$ObjectType with Entity \"System.UserType\". The explicit form failed separately: findEnumeration reads ListEnumerations, which never contains the System module's enumerations", "fix": "settleSystemEnumeration retypes a TypeEntity whose qualified name is in meta.SystemEnumerations to TypeEnumeration before validation, at all six sites (microflow, nanoflow, rule × parameter, return type); isKnownEnumeration accepts a System enumeration where findEnumeration cannot see it. A System entity (System.User) stays an object type", "file": "mdl/executor/cmd_microflows_create.go (settleSystemEnumeration, isKnownEnumeration), mdl/executor/cmd_microflows_build.go, mdl/executor/cmd_rules_create.go", "insight": "It was never an MCP bug: the local engines wrote the same wrong object type and only mx check (CE1613) noticed; MCP surfaced it because PED resolves references at write time. DESCRIBE hid it — it renders an enumeration type as `enum X` and an object type as bare `X`, so a bare `System.UserType` in describe output IS the symptom, not proof of a correct write. Dump the stored DataTypes$*Type (`mxcli bson dump --type microflow`) rather than reading describe. The general lesson for references into System: 'do not judge what you cannot list' covers existence, not kind — whether a name is an entity or an enumeration changes the stored shape, so it must still be decided, and modelsdk/meta.SystemEnumerations is the list to decide it from. Verified live over --mcp on Studio Pro 11.15.0-rc.4 (ped_read_document reads back DataTypes$EnumerationType for parameter and return type), with a stubbed-fix control reproducing the PED error", "refs": ["ako/mxcli#1534"], "ce": ["CE1613"]}
48 changes: 48 additions & 0 deletions mdl-examples/bug-tests/mcp-system-enumeration-parameter.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
mdl 1;
-- ============================================================================
-- A System enumeration named bare in a flow signature is an enumeration
-- ============================================================================
--
-- `$T: System.UserType` used to be written as DataTypes$ObjectType with Entity
-- "System.UserType" on every engine. The visitor types every bare qualified
-- name in a parameter or return position as an entity; the executor's
-- entity-then-enumeration fallback was skipped for the System module, whose
-- entities are not in the project to look up. The local engines wrote the
-- object type without complaint, and DESCRIBE rendered it as a plausible bare
-- `System.UserType` (an enumeration renders as `enum System.UserType`).
-- Studio Pro's PED refused it over --mcp:
--
-- {"/parameters/0/entity":"Reference with qualified name System.UserType
-- of type DomainModels$Entity not found."}
--
-- The explicit `enum System.UserType` spelling — what DESCRIBE emits — was
-- refused outright ("enumeration 'System.UserType' not found"), because the
-- System module's enumerations are not in ListEnumerations.
--
-- Verified over --mcp against Studio Pro 11.15.0-rc.4: both microflows create,
-- and ped_read_document reads back DataTypes$EnumerationType for the
-- parameters and the return type. The System.User parameter is the control: it
-- must stay an object type.
-- ============================================================================

CREATE MODULE BugSysEnumParam;

CREATE OR MODIFY MICROFLOW BugSysEnumParam.ACT_ByUserType ($T: System.UserType)
RETURNS System.DeviceType
BEGIN
IF $T = System.UserType.Internal THEN
RETURN System.DeviceType.Desktop;
ELSE
RETURN System.DeviceType.Phone;
END IF;
END;

CREATE OR MODIFY MICROFLOW BugSysEnumParam.ACT_ExplicitEnum ($T: enum System.UserType)
BEGIN
LOG INFO NODE 'BugSysEnumParam' 'explicit enum parameter';
END;

CREATE OR MODIFY MICROFLOW BugSysEnumParam.ACT_SystemEntity ($U: System.User)
BEGIN
LOG INFO NODE 'BugSysEnumParam' 'System entity parameter stays an object';
END;
14 changes: 10 additions & 4 deletions mdl/executor/cmd_microflows_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b

// Validate and add parameters
for i, p := range s.Parameters {
p.Type = settleSystemEnumeration(p.Type)
s.Parameters[i].Type = p.Type
// Validate entity references for List and Entity types.
// Built-in modules (e.g. System) are not stored in the MPR domain models;
// their types are serialized by qualified name and resolved at runtime.
Expand All @@ -337,7 +339,7 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b
}
// Validate enumeration references for Enumeration types
if p.Type.Kind == ast.TypeEnumeration && p.Type.EnumRef != nil {
if found := findEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name) {
return nil, mdlerrors.NewNotFoundMsg("enumeration", p.Type.EnumRef.Module+"."+p.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for parameter '%s'", p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name))
}
Expand All @@ -356,6 +358,7 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b

// Validate and set return type
if s.ReturnType != nil {
s.ReturnType.Type = settleSystemEnumeration(s.ReturnType.Type)
// Validate entity references for return type.
// Built-in modules (e.g. System) are not stored in the MPR domain models;
// their types are serialized by qualified name and resolved at runtime.
Expand All @@ -373,7 +376,7 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b
}
// Validate enumeration references for return type
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
if found := findEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name) {
return nil, mdlerrors.NewNotFoundMsg("enumeration", s.ReturnType.Type.EnumRef.Module+"."+s.ReturnType.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for return type", s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name))
}
Expand Down Expand Up @@ -630,6 +633,8 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui

// Validate and add parameters
for i, p := range s.Parameters {
p.Type = settleSystemEnumeration(p.Type)
s.Parameters[i].Type = p.Type
if p.Type.EntityRef != nil && !isBuiltinModuleEntity(p.Type.EntityRef.Module) {
entityID := entityResolver(*p.Type.EntityRef)
if entityID == "" {
Expand All @@ -645,7 +650,7 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui
}
}
if p.Type.Kind == ast.TypeEnumeration && p.Type.EnumRef != nil {
if found := findEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name) {
return nil, mdlerrors.NewNotFoundMsg("enumeration", p.Type.EnumRef.Module+"."+p.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for parameter '%s'", p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name))
}
Expand All @@ -664,6 +669,7 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui

// Validate and set return type
if s.ReturnType != nil {
s.ReturnType.Type = settleSystemEnumeration(s.ReturnType.Type)
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
if entityID == "" {
Expand All @@ -677,7 +683,7 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui
}
}
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
if found := findEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name) {
return nil, mdlerrors.NewNotFoundMsg("enumeration", s.ReturnType.Type.EnumRef.Module+"."+s.ReturnType.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for return type", s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name))
}
Expand Down
33 changes: 33 additions & 0 deletions mdl/executor/cmd_microflows_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/mendixlabs/mxcli/mdl/ast"
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/modelsdk/meta"
)

// isBuiltinModuleEntity returns true for modules whose entities are defined
Expand All @@ -20,6 +21,38 @@ func isBuiltinModuleEntity(moduleName string) bool {
return moduleName == "System"
}

// systemEnumerations holds the qualified name of every enumeration the System
// module declares. Like its entities they are not stored in the project, so
// ListEnumerations never returns them and findEnumeration cannot see them.
var systemEnumerations = func() map[string]bool {
set := make(map[string]bool, len(meta.SystemEnumerations))
for _, e := range meta.SystemEnumerations {
set[e.Name] = true
}
return set
}()

// isKnownEnumeration reports whether module.name is an enumeration — one of the
// project's, or one of the System module's.
func isKnownEnumeration(ctx *ExecContext, module, name string) bool {
return systemEnumerations[module+"."+name] || findEnumeration(ctx, module, name) != nil
}

// settleSystemEnumeration retypes a bare System enumeration in a flow
// parameter or return type. The visitor types every bare qualified name there
// as an entity, and the entity-then-enumeration fallback the callers apply is
// skipped for the System module (its entities are not in the project to look
// up), so without this `$T: System.UserType` was written as an ObjectType with
// Entity "System.UserType" — accepted by the local engines, refused by Studio
// Pro ("Reference with qualified name System.UserType of type
// DomainModels$Entity not found").
func settleSystemEnumeration(dt ast.DataType) ast.DataType {
if dt.Kind == ast.TypeEntity && dt.EntityRef != nil && systemEnumerations[dt.EntityRef.Module+"."+dt.EntityRef.Name] {
return ast.DataType{Kind: ast.TypeEnumeration, EnumRef: dt.EntityRef}
}
return dt
}

// execCreateMicroflow handles CREATE MICROFLOW statements.
// loadRestServices returns all consumed REST services, or nil if no backend.
func loadRestServices(ctx *ExecContext) ([]*model.ConsumedRestService, error) {
Expand Down
7 changes: 5 additions & 2 deletions mdl/executor/cmd_rules_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {

// Validate and add parameters
for i, p := range s.Parameters {
p.Type = settleSystemEnumeration(p.Type)
s.Parameters[i].Type = p.Type
if p.Type.EntityRef != nil && !isBuiltinModuleEntity(p.Type.EntityRef.Module) {
entityID := entityResolver(*p.Type.EntityRef)
if entityID == "" {
Expand All @@ -178,7 +180,7 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {
}
}
if p.Type.Kind == ast.TypeEnumeration && p.Type.EnumRef != nil {
if found := findEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name) {
return mdlerrors.NewNotFoundMsg("enumeration", p.Type.EnumRef.Module+"."+p.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for parameter '%s'", p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name))
}
Expand All @@ -197,6 +199,7 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {

// Validate and set return type
if s.ReturnType != nil {
s.ReturnType.Type = settleSystemEnumeration(s.ReturnType.Type)
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
if entityID == "" {
Expand All @@ -210,7 +213,7 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {
}
}
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
if found := findEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name); found == nil {
if !isKnownEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name) {
return mdlerrors.NewNotFoundMsg("enumeration", s.ReturnType.Type.EnumRef.Module+"."+s.ReturnType.Type.EnumRef.Name,
fmt.Sprintf("enumeration '%s.%s' not found for return type", s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name))
}
Expand Down
141 changes: 141 additions & 0 deletions mdl/executor/system_enum_flow_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/mdl/backend/mock"
"github.com/mendixlabs/mxcli/mdl/visitor"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

// writtenMicroflowFromMDL parses one CREATE MICROFLOW statement and returns the
// microflow handed to the backend. Parsing (rather than building the AST by
// hand) is the point: the visitor types every bare qualified name in a
// parameter or return position as an entity, and the executor is what has to
// tell an enumeration apart.
func writtenMicroflowFromMDL(t *testing.T, src string) *microflows.Microflow {
t.Helper()
prog, errs := visitor.Build(src)
if len(errs) > 0 {
t.Fatalf("parse error: %v", errs[0])
}
stmt, ok := prog.Statements[0].(*ast.CreateMicroflowStmt)
if !ok {
t.Fatalf("expected CreateMicroflowStmt, got %T", prog.Statements[0])
}
ctx, written := microflowWriteProbe(t, nil, model.ID("module-1"))
if err := execCreateMicroflow(ctx, stmt); err != nil {
t.Fatalf("CREATE MICROFLOW failed: %v", err)
}
if *written == nil {
t.Fatal("no microflow was written")
}
return *written
}

func wantEnumerationType(t *testing.T, what string, dt microflows.DataType, qn string) {
t.Helper()
et, ok := dt.(*microflows.EnumerationType)
if !ok {
t.Fatalf("%s: got %T (%s), want *microflows.EnumerationType %s", what, dt, dt.GetTypeName(), qn)
}
if et.EnumerationQualifiedName != qn {
t.Fatalf("%s: enumeration = %q, want %q", what, et.EnumerationQualifiedName, qn)
}
}

// TestCreateMicroflow_SystemEnumerationParameter: a System enumeration named
// bare in a parameter used to be written as DataTypes$ObjectType with Entity
// "System.UserType" — on every engine. The System module is not stored in the
// project, so the entity-then-enumeration lookup was skipped for it outright
// and the visitor's entity guess stood. The local engines wrote that without
// complaint (and describe rendered it as a plausible bare `System.UserType`);
// Studio Pro's PED refused it over --mcp: "Reference with qualified name
// System.UserType of type DomainModels$Entity not found."
func TestCreateMicroflow_SystemEnumerationParameter(t *testing.T) {
mf := writtenMicroflowFromMDL(t, `create microflow MyModule.MF ($T: System.UserType)
begin
end;`)
if len(mf.Parameters) != 1 {
t.Fatalf("got %d parameters, want 1", len(mf.Parameters))
}
wantEnumerationType(t, "parameter $T", mf.Parameters[0].Type, "System.UserType")
}

func TestCreateMicroflow_SystemEnumerationReturnType(t *testing.T) {
mf := writtenMicroflowFromMDL(t, `create microflow MyModule.MF ()
returns System.DeviceType
begin
return System.DeviceType.Desktop;
end;`)
wantEnumerationType(t, "return type", mf.ReturnType, "System.DeviceType")
}

// The explicit spelling is what DESCRIBE emits for an enumeration parameter, so
// it must round-trip. It used to be refused outright: the System module's
// enumerations are not in ListEnumerations, so validation reported
// "enumeration 'System.UserType' not found".
func TestCreateMicroflow_ExplicitSystemEnumerationParameter(t *testing.T) {
mf := writtenMicroflowFromMDL(t, `create microflow MyModule.MF ($T: enum System.UserType)
begin
end;`)
wantEnumerationType(t, "parameter $T", mf.Parameters[0].Type, "System.UserType")
}

// Control: a System *entity* must stay an object type, or the fix has merely
// flipped the guess.
func TestCreateMicroflow_SystemEntityParameterStaysObject(t *testing.T) {
mf := writtenMicroflowFromMDL(t, `create microflow MyModule.MF ($U: System.User)
begin
end;`)
ot, ok := mf.Parameters[0].Type.(*microflows.ObjectType)
if !ok {
t.Fatalf("parameter $U: got %T, want *microflows.ObjectType", mf.Parameters[0].Type)
}
if ot.EntityQualifiedName != "System.User" {
t.Fatalf("parameter $U: entity = %q, want System.User", ot.EntityQualifiedName)
}
}

// The nanoflow builder carries its own copy of the parameter resolution, so it
// needs its own case.
func TestCreateNanoflow_SystemEnumerationParameter(t *testing.T) {
prog, errs := visitor.Build(`create nanoflow MyModule.NF ($D: System.DeviceType)
begin
end;`)
if len(errs) > 0 {
t.Fatalf("parse error: %v", errs[0])
}
stmt, ok := prog.Statements[0].(*ast.CreateNanoflowStmt)
if !ok {
t.Fatalf("expected CreateNanoflowStmt, got %T", prog.Statements[0])
}
const moduleID = model.ID("module-1")
var written *microflows.Nanoflow
mb := &mock.MockBackend{
IsConnectedFunc: func() bool { return true },
ListModulesFunc: func() ([]*model.Module, error) {
return []*model.Module{{BaseElement: model.BaseElement{ID: moduleID}, Name: "MyModule"}}, nil
},
GetModuleByNameFunc: func(name string) (*model.Module, error) {
return &model.Module{BaseElement: model.BaseElement{ID: moduleID}, Name: "MyModule"}, nil
},
ListNanoflowsFunc: func() ([]*microflows.Nanoflow, error) { return nil, nil },
CreateNanoflowFunc: func(nf *microflows.Nanoflow) error {
written = nf
return nil
},
}
ctx, _ := newMockCtx(t, withBackend(mb))
if err := execCreateNanoflow(ctx, stmt); err != nil {
t.Fatalf("CREATE NANOFLOW failed: %v", err)
}
if written == nil {
t.Fatal("no nanoflow was written")
}
wantEnumerationType(t, "parameter $D", written.Parameters[0].Type, "System.DeviceType")
}
Loading