Skip to content
Closed
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/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,4 @@
{"area":"mdl/executor","date":"2026-09-15","symptom":"Two check-time validators (validate_import_mapping_find.go, validate_offline_paths.go) imported sdk/mpr directly — which CLAUDE.md's backend-abstraction checklist forbids for the executor. After porting them to the backend, coverage of every ported function was 0.0%: offlineProfilesIn, projectEntityFacts and the new openProjectForValidation were never executed by the suite.","cause":"Both validators FAIL OPEN by design: an unreadable project returns nil and silences the rule rather than failing the check on something it could not inspect. The existing tests only exercised the pure helpers (offlinePathViolations) or passed an empty projectPath, so the reader half never ran.","file":"mdl/executor/validate_project_reader.go","fix":"Added openProjectForValidation as the package's one read-only connect, and tests that exercise all three against a real fixture. Coverage 0.0% -> 72%/78%/100%.","insight":"A FAIL-OPEN validator is the worst shape for an unverified port: break its reader and the rule stops firing, which looks exactly like a project the rule does not apply to. Nothing goes red. So for any fail-open code path, the test must assert the rule FIRES on a project that should trigger it — asserting it stays quiet proves nothing, since quiet is also the failure mode. Two setup details that decide whether such a test is real. (1) The fixture ships only an ONLINE navigation profile, so offlineProfilesIn returns empty on it either way; the test has to SEED an offline profile, and Mendix fixes the legal names (Responsive/Phone/Tablet + the *Offline variants) — an invented name is refused by the executor, which is how the first attempt failed. (2) Assert the control first: the stock fixture reports zero offline profiles, so a reader that invented one is caught before the positive assertion runs. Also worth noting the signature constraint that shaped the fix: ValidateProgram takes a project PATH, not a backend, because `check --references` validates a script against a project it never connects an executor to — so these open their own short-lived read-only connection rather than threading ctx.Backend through a public signature and every caller."}
{"area":"mdl/executor","date":"2026-09-15","symptom":"TestRoundtripPage_MicroflowButtonWithCurrentObject failed on main and on every branch cut from it: 'Expected Target: $currentObject parameter mapping in describe output', while the printed output plainly contained the mapping as \"Target\": $currentObject. Unit tests were green; only the integration suite (-tags integration) caught it.","cause":"Not a describe regression at all. mdl/executor/identifier_quoting.go's mdlIdent quotes any identifier that does not LEX as a bare identifier, running the real ANTLR lexer. #476 (notify workflow ... TARGET) added `TARGET: T A R G E T;` to MDLLexer.g4, so the parameter named Target began lexing as a keyword token and DESCRIBE started quoting it. The output became MORE correct; the test's exact-substring assertion went stale.","file":"mdl/executor/roundtrip_page_test.go","fix":"Made the assertion quoting-agnostic (accepts Target: or \\\"Target\\\":). Controlled by renaming the expected parameter to a name that is absent, which still fails — so the assertion continues to detect a genuinely dropped mapping rather than passing on anything.","insight":"Adding a keyword to MDLLexer.g4 silently reformats DESCRIBE output for every existing element whose NAME matches that keyword, anywhere mdlIdent is used — the grammar change and the broken test are in different packages with no compile-time link, so nothing points from one to the other. When adding a token, grep the test tree for exact-substring assertions containing that word: here `grep -rn '\"Target: '` found the single collision in seconds, where reading the #476 diff never would have. The deeper rule is that an exact-substring assertion on DESCRIBE output encodes a quoting decision the test does not care about; assert the mapping quoting-agnostically, or re-parse the output, since what a roundtrip test means to check is that the mapping survived. Note the input side did NOT break: TARGET was added to the non-reserved-keyword rule, so scripts writing `Target:` unquoted still parse — which is why check-mdl's 544 scripts stayed green and only this one output assertion moved."}
{"area": "mdl/executor", "date": "2026-09-15", "symptom": "check --references rejected a page that mxbuild builds at 0 errors: 'the constraint on Administration.Account names \"System.UserRoles\", which is neither an attribute nor an association of it'. Administration.Account extends System.User and System.UserRoles is declared from System.User, so it IS an association of it, by inheritance. Every inherited association was a false positive, and separately so was every cross-module one. Inherited ATTRIBUTES were fine throughout, which is what made it look like a narrow bug rather than a whole axis.", "cause": "associationTargetFrom matched the start entity against the association's two ends by exact equality and scanned only dm.Associations. Its doc comment said a specialisation deliberately returns false, 'the cost of being wrong is a false error on a working script' - sound while its only caller (resolveMemberOnEntity, typing an association retrieve) treated false as silence. The new XPath constraint checker's noteQualified then called the same helper and treated false as EVIDENCE, so the exact case the comment declined to chase became the finding. noteQualified did carry a three-valued guard, but it tested whether the BASE ENTITY was known - the wrong axis; Administration.Account is known.", "file": "mdl/executor/validate_member_refs.go", "fix": "Replaced the boolean with assocResolution (resolved / missing / notAnEnd / unknown). The start entity is matched through its generalization chain (generalizationChain, built on findEntityByQN), and a chain that could not be walked to its root yields assocUnknown = silence. associationEndsByName also reads dm.CrossAssociations, whose far end is held by NAME not by element ID. noteQualified reports only assocMissing and assocNotAnEnd.", "insight": "A helper whose contract is 'false when it cannot establish the answer' is safe only while every caller treats false as silence; the moment one caller treats it as evidence, the comment promising restraint becomes the specification of a false positive. A boolean cannot carry 'no' and 'don't know' to two callers that need to tell them apart - if the codebase already has a three-valued resolver next door (memberResolution, ten lines up), reusing the two-valued sibling is the smell. Two cheap guards would have caught it: a fixture entity with a GENERALIZATION carrying an association, and one CROSS-MODULE association - a fixture of flat single-module entities cannot distinguish a correct resolver from one comparing two names. Establish the verdict with the build, not by reading: mx check on the rejected page said 0 errors, which settles it in one run."}
{"area":"mdl/executor","date":"2026-09-16","symptom":"A pluggable widget with two or more independent named datasource properties could not be authored or round-tripped: MDL-WIDGET05 rejected real datasource expressions under those keys, generic DataSource fanned one source across all mappings, datasource-dependent modes ignored the named value, and DESCRIBE PAGE retained only one source.","cause":"resolveMapping and the hasDataSource mode condition only read w.GetDataSource and never inspected datasource mappings by property key or alias. The later explicit-property pass did not consider property keys and aliases consumed, so it could overwrite a correctly mapped attribute under the final datasource context. DESCRIBE used a first-source fallback with no place to retain property keys.","file":"mdl/executor/widget_engine.go, mdl/executor/validate_widgets.go, mdl/executor/cmd_pages_describe_pluggable.go","fix":"Resolve a structured DataSourceV3 by datasource mapping key or alias before the generic fallback; let hasDataSource inspect only the active mode's datasource mappings (not datasource-shaped named actions); exclude every mapped spelling from explicit fallback; allow only structured values in named datasource slots; collect and emit all named datasource keys when more than one is populated.","insight":"Datasource is not a widget-wide singleton once a schema exposes several datasource properties. Treat the mapping as the unit of context: select the mode from its datasource mappings, resolve its named source, update entityContext, then resolve dependent attributes. Inspecting arbitrary DataSourceV3 values is unsafe because named microflow and nanoflow actions share that parser shape. A generic convenience keyword may remain for a single-source widget, but DESCRIBE must switch to schema keys when collapsing would lose identity. The earlier #643 guard correctly rejected scalar lookalikes but encoded the deferred multi-source limitation as a permanent rule; validator rules should distinguish an unsupported value shape from an unsupported capability.","refs":["mendixlabs/mxcli#643","mendixlabs/mxcli#1109"]}
28 changes: 27 additions & 1 deletion .claude/skills/mendix/custom-widgets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,32 @@ Object-list *item* action slots (chart series `staticOnClickAction`, popupmenu
item `action`) have mappings generated but the engine still skips them at apply
time. See upstream #956.

**Widgets with multiple datasources use each datasource property's own key.**
The friendly `DataSource:` spelling is for a widget with one logical source.
When a widget exposes independent lists, name them exactly as `describe widget`
does:

```sql
multisource dashboard (
primarySource: microflow Dashboard.DS_PrimaryRows,
primaryLabelAttribute: Label,

secondarySource: microflow Dashboard.DS_SecondaryRows,
secondaryDateAttribute: OccurredAt,

summarySource: database from Dashboard.SummaryRow,
summaryValueAttribute: Total
)
```

Each named property accepts the normal datasource expressions (`database`,
`microflow`, `nanoflow`, page parameter, selection, or association). Attribute
mappings resolve against the matching source's entity context; in `.def.json`,
put each datasource mapping before the attribute mappings that depend on it.
Do not replace the named properties with one generic `DataSource:`: that loses
which entity owns each attribute. `DESCRIBE PAGE` preserves the named form
whenever a widget has more than one populated datasource.

### Step 2 -- Extract BSON template from Studio Pro

The .def.json only describes mapping rules. The engine also needs a **template JSON** with the complete Type + Object BSON structure.
Expand Down Expand Up @@ -512,7 +538,7 @@ Modes are evaluated in definition order -- first match wins. A mode with no `con
| Source | Resolution logic |
|--------|-----------------|
| `attribute` | `w.GetAttribute()` -> `pageBuilder.resolveAttributePath()` |
| `datasource` | `w.GetDataSource()` -> `pageBuilder.buildDataSourceV3()` -> also updates `entityContext` |
| `datasource` | Named datasource property matching the mapping key/alias, otherwise `w.GetDataSource()` -> `pageBuilder.buildDataSourceV3()` -> also updates `entityContext` |
| `association` | `w.GetAttribute()` -> `pageBuilder.resolveAssociationPath()` + uses current `entityContext` |
| `selection` | `w.GetSelection()` or `mapping.Default` fallback |
| `CaptionAttribute` | `w.GetStringProp("CaptionAttribute")` -> auto-prefixed with `entityContext` if relative |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **Multi-source pluggable widgets can bind each datasource by its own property name.** Independent sources on one widget may return different entities, but the generic `DataSource:` spelling fanned one source across every datasource mapping, while the named form was rejected by MDL-WIDGET05 or overwritten during the explicit-property pass. Named datasource expressions now select datasource-dependent widget modes, resolve by mapping key or alias, retain their own entity context, and round-trip through `DESCRIBE PAGE`; scalar lookalikes remain an error. Single-source widgets keep the generic spelling.

- **`check --references` rejected every XPath constraint that hops an INHERITED or a CROSS-MODULE association** (ako/mxcli-sudoku FINDINGS #57). The constraint-member check added in `3aa2ee0e` reported the stock `Administration.Account_Overview` page — `Administration.Account extends System.User`, so `System.UserRoles` (declared from `System.User`) is an association of it by inheritance, and `mx check` on the rejected page says 0 errors. Because the false positive lands on a Marketplace module almost every app has, `check` stopped being usable as a gate for anyone whose entities inherit, which is the normal case for anything extending `System.User`, `System.Image` or `System.FileDocument`.

The lookup matched the start entity against the association's two ends by exact equality and read only `dm.Associations`. Its comment said the specialisation case was deliberately not chased — "the cost of being wrong is a false error on a working script" — and that was sound while its only caller treated `false` as *silence*; the new check treated the same `false` as *evidence*, so the precise case the comment declined to chase became the finding. It is three-valued now (resolved / missing / not-an-end / unknown): the start entity is matched through its generalization chain, a cross-module association is found where it is actually stored (`CrossAssociations`, far end held by name), and a chain that could not be walked to its root is silence rather than a report. The check still fires on an association the entity genuinely lacks, including a specialisation's association named on its generalization.
Expand Down
43 changes: 43 additions & 0 deletions mdl-examples/bug-tests/mapping-1-named-widget-datasources.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- A datasource mapping may be addressed by its schema key.
--
-- ComboBox is the public, embedded regression fixture. Its association mode is
-- selected by hasDataSource and maps optionsSourceAssociationDataSource to the
-- widget's datasource slot. Before the fix, the structured named expression
-- below was rejected by MDL-WIDGET05; allowing it without updating mode
-- selection would instead choose the enumeration mode and still drop it.
--
-- The executor unit tests extend this same mapping behavior to a synthetic
-- widget with three simultaneously populated datasource mappings and prove
-- that DESCRIBE PAGE preserves all three schema keys.

create module NamedDS;
create module role NamedDS.User;

create persistent entity NamedDS.Customer (
Name: string(200)
);

create persistent entity NamedDS.Order (
Number: string(80)
);

create association NamedDS.Order_Customer
from NamedDS.Order to NamedDS.Customer;

create or replace page NamedDS.Order_Edit (
Title: 'Named datasource mapping',
Layout: Atlas_Core.Atlas_Default,
Params: { $Order: NamedDS.Order }
) {
dataview orderView (DataSource: $Order) {
pluggablewidget 'com.mendix.widget.web.combobox.Combobox' customer (
optionsSourceAssociationDataSource: database from NamedDS.Customer,
Association: Order_Customer,
CaptionAttribute: Name
)
}
}

grant view on page NamedDS.Order_Edit to NamedDS.User;

describe page NamedDS.Order_Edit;
38 changes: 24 additions & 14 deletions mdl/executor/cmd_pages_describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,20 +609,25 @@ type rawWidget struct {
// named variants are spelled identically, so a reader that keeps the name
// without the kind cannot avoid converting one into the other
// (mendixlabs/mxcli#1059). Same split as types.NavMenuItem's.
Icon string // e.g. Atlas_Core.Atlas_Filled.pencil
IconType string // storage $Type, "" when the widget carries no icon
IconCode int // Forms$GlyphIcon's Code — the only identity a glyph icon has
Selection string // For Gallery selection mode (Single, Multi, None)
Class string // CSS class from Appearance
Style string // Inline CSS style from Appearance
DynamicClasses string // Dynamic-classes expression from Appearance
Parameters []string
Children []rawWidget
FilterWidgets []rawWidget // For Gallery filter widgets
ControlBar []rawWidget // For DataGrid2 CONTROLBAR widgets
Rows []rawWidgetRow
DataSource *rawDataSource
DataGridColumns []rawDataGridColumn // For DataGrid2 widgets
Icon string // e.g. Atlas_Core.Atlas_Filled.pencil
IconType string // storage $Type, "" when the widget carries no icon
IconCode int // Forms$GlyphIcon's Code — the only identity a glyph icon has
Selection string // For Gallery selection mode (Single, Multi, None)
Class string // CSS class from Appearance
Style string // Inline CSS style from Appearance
DynamicClasses string // Dynamic-classes expression from Appearance
Parameters []string
Children []rawWidget
FilterWidgets []rawWidget // For Gallery filter widgets
ControlBar []rawWidget // For DataGrid2 CONTROLBAR widgets
Rows []rawWidgetRow
DataSource *rawDataSource
// NamedDataSources preserves widgets that expose more than one datasource.
// A single datasource continues to use DataSource and its friendly MDL
// keyword; multiple sources must retain their schema property keys or a
// describe -> exec round trip would fan one source out over every mapping.
NamedDataSources []rawNamedDataSource
DataGridColumns []rawDataGridColumn // For DataGrid2 widgets
// Input widget properties
Editable string // "Always", "Never", "Conditional"
ReadOnlyStyle string // "Inherit", "Control", "Text"
Expand Down Expand Up @@ -724,6 +729,11 @@ type rawNamedAction struct {
MDL string
}

type rawNamedDataSource struct {
Key string
DataSource *rawDataSource
}

type rawExplicitProp struct {
Key string
Value string // attribute short name or primitive value
Expand Down
13 changes: 13 additions & 0 deletions mdl/executor/cmd_pages_describe_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,19 @@ func appendDataSourceProp(props []string, ds *rawDataSource) []string {
return props
}

func appendNamedDataSourceProps(props []string, sources []rawNamedDataSource) []string {
for _, source := range sources {
if expr := dataSourceExpr(source.DataSource); expr != "" {
props = append(props, fmt.Sprintf("%s: %s", source.Key, expr))
continue
}
if comment := dataSourceComment(source.DataSource); comment != "" {
props = append(props, fmt.Sprintf("-- %s: %s", source.Key, strings.TrimPrefix(comment, "-- ")))
}
}
return props
}

// xpathConstraintClause renders a stored XPath constraint as the MDL the page
// grammar accepts after WHERE, or "" when there is no constraint.
//
Expand Down
Loading