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
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 @@ -881,3 +881,4 @@
{"area": "mdl-executor", "date": "2026-10-07", "refs": ["mendixlabs/mxcli#1324"], "symptom": "A widget directly inside data view `dvP` that reads `$dvP` OUTSIDE an action — a nested data view's `DataSource: microflow M.F(Gate = $dvP)`, `Visible: $dvP/Name != ''`, `Editable: …`, `DynamicClasses: …`, or a nested list's `database from M.E where [Name = $dvP/Name]` — passes `check` and `exec`, then `mx check` reports `[CE0117] \"Error(s) in expression.\"` at the widget (CE0161 \"Error(s) in XPath constraint.\" for the `where`)", "cause": "MDL-BUTTON02 (the #1324 fix) only looked at action arguments, though every slot evaluated in the widget's enclosing context has the same scope: a container's widget-name variable exists only one data container below it", "file": "`mdl/executor/validate_page_button_context.go` (`checkOwnContainerName` now takes the widget and walks action args, `GetDataSource().Args` / `.Where`, and `ownNameExprProps`)", "insight": "**A scope rule belongs to the context, not to the slot the report happened to use.** The follow-up question that settled it was one probe per slot with a control one data view deeper: five of five slots failed in the own context and all five controls built clean, so the rule is 'anything evaluated in this widget's enclosing context' — enumerate those slots rather than wait for one report each. Two things that would have wasted a build: (1) `Visible:`/`Editable:` arrive in the AST as `VisibleIf`/`EditableIf` strings (dump `w.Properties` before keying on what the author wrote), and (2) text-template parameters (`ContentParams ({1} = $dvP/Name)`) are already refused by MDL-WIDGET24 for an unrelated reason (a template parameter is an attribute name, not a variable path), so they are not a scope case at all. The XPath slot reports a different code (CE0161), which is why the message carries the code per slot. Control: stubbing the data-source and property slots fails the new test with `flagged \"\"`; the `$currentObject` rewrite of all five builds at 0 errors. Repro `mdl-examples/bug-tests/1324-own-data-container-name-outside-actions.fail.mdl`"}
{"area": "mdl/executor", "date": "2026-10-07", "symptom": "`describe structure depth 2|3` / `mxcli structure -d 3` never annotates a page with its data widgets (`Page M.P [DataView<Customer>, …]`), on any project and with a full catalog — every page prints bare. Evora Factory Management: 0 of 54 pages annotated, although CATALOG.widgets holds their data views and grids.", "cause": "structurePages queried `widgets … where ParentWidget = ''`; widgets_data has never had a ParentWidget column (the tree position, added later, is ParentWidgetId + Depth). The query failed and the error was discarded (`if err == nil { … }`), so the annotation was dead code from the initial commit. Every other catalog query in cmd_structure.go swallowed errors the same way (`if err != nil || len(rows) == 0 { return }`).", "file": "`mdl/executor/cmd_structure.go` (`structureQuery`, `structurePages`, `queryCountByModule`, `shortWidgetType`), `mdl/executor/cmd_structure_page_widgets_test.go`", "insight": "A fixed SQL query against a table the builder owns can only fail through drift inside mxcli, never because of the user's project, so swallowing its error converts a schema mismatch into silent absence — the same shape as the depth-1 flow-count casing bug (#717), which a swallowed error also hid. Route such queries through one helper that returns the error; then a column rename fails the first test that runs the command against a real built catalog. That test must build the catalog with catalog.NewBuilder in FULL mode (SetFullMode(true)) over raw page BSON from GetRawUnitFunc — widgets_data is empty in fast mode, and a fast-mode test passes against the broken query because 'no widgets' and 'query failed' both print a bare page. Mock gotcha: the builder dereferences GetNavigation, whose mock default is (nil, nil), so stub it with an empty NavigationDocument. Semantics choice: 'top-level' cannot mean Depth = 0 — real pages wrap content in a layout grid, so a root filter lists almost nothing; list data widgets with no data-widget ancestor instead (walk ParentWidgetId). Measured on Evora after the fix: 45 of 54 pages annotated (181 of 212 with `all`), and the 9 bare pages have no data widget in CATALOG.widgets.", "refs": ["#717"]}
{"area": "mdl/executor", "date": "2026-10-07", "symptom": "`mxcli check script.mdl -p app.mpr --references` printed \"Check passed!\" for `grant read * on entity System.Nope to M.R`, for a grant to `M.NopeRole`, and for every other GRANT/REVOKE form naming a missing entity, document, member or role; also for `create user role R ( ModuleRoles: (M.NopeRole) )`, `alter user role … add module roles`, and a demo user with an unknown user role or entity (measured on Evora Factory Management, 10.24.15). exec refused the grants after the earlier statements were written; the user-role and demo-user shapes it wrote unresolved (CE1613 at build).", "cause": "No validate path resolved a security statement's names. validateWithContext's switch had no case for any of them (only the role-free validateCrossModuleGrant ran), and the user-role/demo-user executors store module and user role names verbatim without resolving them, so neither check nor exec stood between a typo and the model.", "file": "`mdl/executor/validate_grant_refs.go` (`validateGrantReferences`), wired into `validateProgramWithWarnings` in `validate.go`; test `check_grant_references_pedapp_test.go`", "insight": "Two things made the obvious version wrong. (1) What check refuses has to be what exec refuses, form by form: exec refuses an unknown role on a GRANT and on an entity REVOKE, but a document REVOKE (and `alter user role … drop module roles`) of an unknown role is a reported no-op, which keeps cleanup scripts re-runnable after the role is dropped — refusing it would make check louder than exec. Likewise every System entity is refused by exec (refuseSystemEntityGrant), so `grant … on entity System.User` must be refused by check too, while System MODULE roles in a user role must pass. (2) A script's own effects are not only its CREATE statements: creating a microflow/nanoflow/page in a module with no module roles auto-creates `<Module>.User` (defaultDocumentAccessRoles), and doctype-tests/02b grants to exactly that role — the first version flagged 10 statements there. Walking the program in statement order (not the whole-program scriptContext) gives the forward-reference hint for free. Performance trap: reading every module's security costs ~5s on Evora; read only the modules a statement names (~0.1s).", "refs": ["mdl-examples/bug-tests/check-grant-unknown-reference-refused.mdl", "mdl-examples/bug-tests/check-grant-unknown-reference.mdl", "ako/mxcli#1020"]}
{"area": "mdl/executor", "date": "2026-10-07", "symptom": "A DataGrid 2 column's `Visible:` expression is stored as `true` (always visible) for every spelling but the old quoted one: `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)`, `Visible: [cond]`. check clean, exec reports \"Created page\", describe shows no Visible. `alter page … set (Visible: <expression>) on grid column(…)` is refused as \"column property VisibleIf not found\"; `insert` of such a column drops it, and drops `Visible: false` too.", "cause": "The visitor lowers every expression spelling of Visible to the key VisibleIf (the page-widget conditional-visibility key) and keeps only plain values under Visible. The column writers read only visible/Visible: the widget engine by schema key + types.ItemPropertyAliases (no VisibleIf alias), the ALTER mutator's resolveColumnPropertyKey the same table, and widgetobj.BuildDataGrid2Column (ALTER insert/replace via buildColumnSpecFromAST) the exact key \"Visible\" as a string only, so a bool false fell to the default too. The mirror image of widget-visible-expression.mdl, where page widgets read VisibleIf and dropped Visible.", "file": "`mdl/types/widget_item_aliases.go` (`visible` ← `VisibleIf`), `mdl/executor/widget_engine.go` (WidgetDefGeneratorVersion 18), `mdl/executor/cmd_pages_builder_v3_widgets.go` (`columnSpecProperties`), `mdl/executor/cmd_pages_describe_output.go` (column Visible via widgetConditionMDL), `mdl/executor/validate_column_visible_scope.go` (MDL-WIDGET43)", "insight": "When the visitor lowers one MDL property to two AST keys by value shape, every consumer must read both — grep the consumers of the key the visitor writes, not of the property name. Making the value persist exposed what the drop had hidden: a column's visible expression has no row object (no dataSource in the widget schema, unlike columnClass), so every $currentObject example became CE0117 at build — measured on 11.14.0 — and needed a check rule (MDL-WIDGET43) in the same change. The widget def's per-item `dataSource` field is the signal for whether $currentObject exists.", "refs": ["mdl-examples/bug-tests/datagrid-column-visible-expression.mdl", "mdl-examples/bug-tests/widget-visible-expression.mdl"]}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Fixed

- **The catalog names a call's and a delete's target** (mendixlabs/mxcli#1305) — `activities_for()` and `CATALOG.ACTIVITIES` returned every microflow, nanoflow, Java action and JavaScript action call with `action_ref=""`, and every delete with `entity_ref=""`, although `refs_from()` had both targets; a loop-scoped lint rule could not follow a call out of the loop. `action_ref` / `ActionRef` is now the called document, `entity_ref` / `EntityRef` a delete's entity (when the flow types the variable: a parameter, a create or retrieve output, a loop iterator), and the new `queue_ref` / `QueueRef` the task queue a microflow or Java action call runs in, so a rule can skip a call that runs asynchronously. A nanoflow's JavaScript action call now has a `refs_from()` `call` row (`target_type` `"JAVASCRIPT_ACTION"`), so `show callers` sees it. The catalog schema is bumped to 23; a cached catalog rebuilds.
- **A DataGrid 2 column's `Visible:` expression is written** — `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)` and `Visible: [cond]` on a column passed `check` and `exec` and were stored as `true`, so the column was always visible; only the old quoted `Visible: '<expr>'` was kept. `alter page … set (Visible: <expression>) on grid column(…)` was refused as "column property VisibleIf not found", and an inserted column dropped `Visible: false` too. `describe` now prints the bare expression. A column's visibility is evaluated once for the grid, with no row object, so `$currentObject` there is CE0117 at build; `check` refuses it as **MDL-WIDGET43** (measured on 11.14.0). Use a page variable or parameter. Projects regenerate their widget definitions (generator version 18).
- **`set $Param = …` on a parameter is refused** — a Change variable cannot target a parameter, and mxbuild rejects it with CE7247 "Parameter 'N' cannot be changed." (measured on 11.14.0 for Integer and String parameters in a microflow, a nanoflow and a rule). `check` and `exec` passed it. MDL-SET01 now refuses it for every parameter but a list (`set` on a list parameter is a Change list Replace, which builds), and `exec` enforces the rule. Copy the parameter into a variable first: `declare $Value Integer = $N;`.
- **`check` refuses a button that passes its own data container by widget name** (mendixlabs/mxcli#1324) — `actionbutton btnOwn (Action: call microflow M.F(Gate = $dvGate))` directly inside data view `dvGate` passed `check --references` and `exec`, then `mx check` reported `[CE0117] "Error(s) in expression." at Action button 'btnOwn'`. A data container's name is a variable only for the containers nested below it; in its own context the object is `$currentObject`. Reported as **MDL-BUTTON02** (error, so `exec` refuses it), for data views, list views, galleries and data grids alike, including attribute paths (`$dvGate/Name`) and a control bar inside the container. The rule covers every slot evaluated in that context, not only action arguments: a nested widget's microflow data-source arguments, `Visible:`, `Editable:` and `DynamicClasses:` (CE0117), and a nested list's XPath `where` (CE0161). A grid's own name from its control bar (the selection) and an enclosing container's name from a nested one are not flagged. The flagged set matches mxbuild 11.14.0's CE0117s widget for widget on a ten-button probe page.
- **`set $Obj = …` on an object variable is refused** (mendixlabs/mxcli#1323) — with both variables single objects (e.g. a Reference retrieved from its FROM entity), `set $Cursor = $Next;` passed `check --references` and `exec` and was written as a Change variable action, which mxbuild refuses with CE7247 "Variable 'Cursor' does not have a primitive type". Mendix has no action that reassigns an object variable, so `check` (MDL-SET01, for the objects it can see without a project), `check --references` and `exec` now refuse it and name the alternatives (a sub-microflow that returns the next object, `change $Obj (…)`). `check --references` now types a Reference retrieve from its FROM entity as the object it is. `set` on a list variable stays a Change list Replace (ako/mxcli#949).
Expand Down
2 changes: 1 addition & 1 deletion cmd/mxcli/syntax/features_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ LIST IMPACT OF htmlelement;
"column width", "alignment", "wrap text", "visible",
"dynamic cell class", "tooltip", "associated attribute", "association column",
},
Syntax: "COLUMN name (\n Attribute: AttrName, -- own attribute\n -- or an attribute over an association (bare association name):\n -- Attribute: Assoc/Attr e.g. Order_Customer/Name\n Caption: 'Header'\n [, Sortable: true|false]\n [, Resizable: true|false]\n [, Draggable: true|false]\n [, Hidable: yes|hidden|no]\n [, ColumnWidth: autoFill|autoFit|manual]\n [, Size: integer]\n [, Alignment: left|center|right]\n [, WrapText: true|false]\n [, Visible: 'expression']\n [, DynamicCellClass: 'expression']\n [, Tooltip: 'text']\n)",
Syntax: "COLUMN name (\n Attribute: AttrName, -- own attribute\n -- or an attribute over an association (bare association name):\n -- Attribute: Assoc/Attr e.g. Order_Customer/Name\n Caption: 'Header'\n [, Sortable: true|false]\n [, Resizable: true|false]\n [, Draggable: true|false]\n [, Hidable: yes|hidden|no]\n [, ColumnWidth: autoFill|autoFit|manual]\n [, Size: integer]\n [, Alignment: left|center|right]\n [, WrapText: true|false]\n [, Visible: <expression>] -- once for the grid: a page variable or parameter, never $currentObject (MDL-WIDGET43)\n [, DynamicCellClass: <expression>] -- per row: $currentObject is the row\n [, Tooltip: 'text']\n)",
Example: "COLUMN colPrice (\n Attribute: Price, Caption: 'Price',\n Alignment: right, Sortable: false,\n ColumnWidth: manual, Size: 150,\n Tooltip: 'Price in USD'\n)\n\n-- Associated attribute (attribute over a reference association):\nCOLUMN colCustomer (Attribute: Order_Customer/Name, Caption: 'Customer')",
SeeAlso: []string{"page.widgets"},
})
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/appendixes/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,8 @@ MDL uses explicit property declarations for pages:
| `Hidable` | `yes`, `hidden`, `no` | `yes` | `Hidable: no` |
| `ColumnWidth` | `autoFill`, `autoFit`, `manual` | `autoFill` | `ColumnWidth: manual` |
| `Size` | integer (px) | `1` | `Size: 200` |
| `Visible` | expression string | `true` | `Visible: '$showColumn'` (page variable, not $currentObject) |
| `DynamicCellClass` | expression string | (empty) | `DynamicCellClass: if(...) then ... else ...` |
| `Visible` | expression | `true` | `Visible: $showColumn` — evaluated once for the grid: a page variable or parameter, never `$currentObject` (MDL-WIDGET43, CE0117) |
| `DynamicCellClass` | expression | (empty) | `DynamicCellClass: if $currentObject/Stock < 10 then 'text-danger' else ''` |
| `Tooltip` | text string | (empty) | `Tooltip: 'Price in USD'` |

**Page Example:**
Expand Down
10 changes: 10 additions & 0 deletions docs-wiki/bug-patterns/silent-property-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ answer "this shape does not fit" (MDL-WIDGET42, and an error at build) instead o
falling to `default: continue`. When a grammar alternative is chosen by the
*first token*, audit which other meanings that token starts.

**One property, two keys: a consumer that reads one drops the other.** The
visitor lowers `Visible:` by value shape — an expression to `VisibleIf`, a plain
value to `Visible` — and the two writers each read only one: page widgets read
`VisibleIf` (and once dropped `Visible: false`), DataGrid 2 columns read
`Visible` (and dropped every expression). Grep the consumers of the *key the
visitor writes*, not of the property name. And expect persisting a value to
surface a rule its absence hid: a column's visibility has no row object, so the
`$currentObject` examples that had always "worked" became CE0117 the moment they
were written, and the fix needed a check rule to go with it.

**Children drop the same way properties do.** A widget's body is distributed by
several passes that each skip what they do not recognise, so a child matching no
container, no slot and no catch-all is built and discarded exactly as an
Expand Down
54 changes: 54 additions & 0 deletions mdl-examples/bug-tests/datagrid-column-visible-expression.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
mdl 1;
-- ============================================================================
-- A DataGrid 2 column's Visible expression was silently dropped
-- ============================================================================
--
-- Symptom (before fix):
-- `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)` and
-- the bracketed `Visible: [cond]` on a column passed check, exec reported
-- "Created page", and the column was stored always visible (Expression
-- "true"). Only the old quoted spelling `Visible: '<expr>'` was kept.
-- `alter page … set (Visible: <expression>) on grid column(…)` was refused as
-- "column property VisibleIf not found".
--
-- Root cause:
-- The mirror image of widget-visible-expression.mdl. The visitor lowers
-- every expression spelling of Visible to the key VisibleIf, the key a page
-- widget's conditional visibility reads. The column writers read only
-- `visible` / `Visible`: the widget engine by schema key plus
-- types.ItemPropertyAliases, ALTER's insert path by the exact key.
--
-- After fix:
-- `visible` <- `VisibleIf` alias (create and alter set; widget defs
-- regenerate at generator version 18), the insert/replace path normalises
-- the column's properties, and describe prints the bare expression.
-- A column's visibility is evaluated once for the grid: it has no row
-- object, so `$currentObject` there is CE0117 — refused at check time as
-- MDL-WIDGET43. Use a page variable or parameter.
-- Verified on Mendix 11.14.0: mx check = 0 errors; describe round-trips.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/datagrid-column-visible-expression.mdl -p app.mpr
-- ============================================================================

create entity MyFirstModule.ColVisItem ( Name: String(100), Price: Decimal );

create or modify page MyFirstModule.P_ColumnVisible
( Title: 'Column visible', Layout: Atlas_Core.Atlas_Default,
Variables: ( $showPrices: boolean = 'true' ) )
{
datagrid dg (datasource: database MyFirstModule.ColVisItem) {
column (attribute: Name, caption: 'Var', Visible: $showPrices)
column (attribute: Price, caption: 'If', Visible: if $showPrices then true else false)
column (attribute: Price, caption: 'Not', visible: not($showPrices))
column (attribute: Name, caption: 'Hidden', Visible: false)
column (attribute: Name, caption: 'Later')
}
};

-- Previously refused: "column property VisibleIf not found".
alter page MyFirstModule.P_ColumnVisible {
set (Visible: $showPrices) on dg column('Later')
};

describe page MyFirstModule.P_ColumnVisible;
2 changes: 1 addition & 1 deletion mdl-examples/doctype-tests/03-page-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -2249,7 +2249,7 @@ create page PgTest.P033b_DataGrid_ColumnProperties folder 'DataGrid'
column (
attribute: Stock, caption: 'In Stock',
Alignment: center,
visible: '$showStockColumn',
visible: $showStockColumn,
DynamicCellClass: if($currentObject/Stock < 10) then 'text-danger' else ''
)

Expand Down
Loading
Loading