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/grammar", "date": "2026-10-26", "symptom": "ako/mxcli#1685: `retrieve $Items from Shop.Item where [Total > @Shop.C_Limit];` was a parse error (`mismatched input 'Shop' expecting ']'`), read as 'MDL cannot write a constant in an XPath'. Worse, silent: a page data source written `where Total > @Shop.C_Limit` stored `[Total > ]`; describe of any stored constraint holding a constant on an entity with an enum attribute printed `where Total > @;`; and a long one written in the quoted form was laid out and STORED as `[Total > @]`", "cause": "No XPath rule for a constant: `@` (AT) was admitted by the negated xpathWord set as a one-word name. ParseXPathConstraint (error listeners off) recovered past the module name and returned ok with IdentifierExpr \"@\", so every re-render of the parse (enum enrichment, FormatXPathConstraint, check's member walk, which reported an attribute named \"@\") lost the constant; separately the page-side xpathExprToString had no ConstantRefExpr case and rendered \"\" for the expression-grammar form", "file": "mdl/grammar/domains/MDLPage.g4 (xpathConstantRef, AT out of xpathWord), mdl/visitor/visitor_xpath.go (buildXPathConstantRef), mdl/visitor/visitor_page_v3.go (xpathExprToString), mdl/executor/validate_xpath_constants.go (MDL-XPATH03), validate_user_role_tokens.go (walkXPathSlots)", "fix": "`@Module.Name` is an xpathValueExpr (a value, not a step) and AT is no longer a name word; the visitor builds ast.ConstantRefExpr and the page renderer prints it. check refuses a constant in any XPath slot (MDL-XPATH03, ValidateProgram, no project needed) with the variable workaround; excluded flows warn", "test": "mdl/visitor/visitor_xpath_constant_test.go, mdl/executor/validate_xpath_constants_test.go, mdl/executor/xpath_constant_1685_test.go (PedApp: stored verbatim in retrieve, list view and access rule, describe -> exec writes nothing, rename constant rewrites all three). Reverting the grammar fails every test with the reported parse error and `[Total > @]`; reverting only the renderer case gives `[Total > ]`", "insight": "The issue's premise was wrong and only a measurement showed it: mxbuild 11.13.0 rejects `@Mod.C` in a retrieve (Integer, String, Boolean constant), a list view data source, an access rule and a user task's targeting with CE0161, and an UNKNOWN constant is the same CE0161 rather than CE1613 (11.15.0 agrees for the retrieve) - XPath never resolves the name; the variable form is clean. So the right fix for this parse error was to parse it and REFUSE it with the workaround, not to store it. The Mendix runtime's MxPath grammar does have a CONSTANT token (com.mendix.languages.jar), which is why the syntax looks plausible. Access-rule XPaths are not validated by mx check at all while project security is Off - a garbage `[Total >]` passes - so measure access rules under Production security, with a bad-constraint control. A lenient parse that returns ok is not evidence it read everything: grep a renderer for `default: return \"\"` before trusting a new AST node to round-trip", "refs": ["ako/mxcli#1685", "ako/mxcli#1671"], "ce": ["CE0161"], "rules": ["MDL-XPATH03"]}
28 changes: 20 additions & 8 deletions .claude/skills/mendix/xpath-constraints/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ description: "XPath constraint syntax for MDL — retrieve WHERE clauses, page d
| lines | section |
|---|---|
| 28-34 | When to Use This Skill |
| 35-75 | XPath vs Mendix Expressions |
| 76-119 | Syntax Reference |
| 120-297 | How a Constraint Is Laid Out on Disk |
| 298-348 | Usage Contexts |
| 349-388 | Enumeration Attributes |
| 389-430 | Common Patterns |
| 431-442 | Validation |
| 443-452 | Troubleshooting |
| 35-87 | XPath vs Mendix Expressions |
| 88-131 | Syntax Reference |
| 132-309 | How a Constraint Is Laid Out on Disk |
| 310-360 | Usage Contexts |
| 361-400 | Enumeration Attributes |
| 401-442 | Common Patterns |
| 443-454 | Validation |
| 455-464 | Troubleshooting |

<!-- /skill-index -->
# XPath Constraints in MDL
Expand All @@ -45,6 +45,7 @@ This skill provides reference for writing XPath constraint expressions in MDL RE
| Token quoting | `'[%CurrentUser%]'` (quoted) | `[%CurrentUser%]` (unquoted) |
| Nested filter | `Assoc/entity[pred]` | Not applicable |
| Arithmetic on value | **not supported** — pre-compute into a variable | `+`, `-`, `*`, `div`, `mod` |
| Constant | **not supported** — assign it to a variable | `@Module.Constant` |

> **XPath constraints cannot compute values.** `where [Seq = $Game/MoveSeq + 1]`
> is a parse error (Mendix XPath has no arithmetic on the value side). Compute the
Expand Down Expand Up @@ -73,6 +74,17 @@ This skill provides reference for writing XPath constraint expressions in MDL RE
> retrieve $L from Mod.T where [DueDate > $Cutoff];
> ```

> **Constants are not available in XPath either.** `[Total > @Mod.C_Limit]`
> parses, but mxbuild rejects a constant in a retrieve, a data source, an access
> rule and a user task's targeting alike (`CE0161`, measured on 11.13.0) — an
> unknown one too, so it is never a missing-constant error. `mxcli check`
> refuses it (**MDL-XPATH03**). In a flow, assign it to a variable first:
> ```mdl
> -- ❌ WRONG: retrieve $L from Mod.T where [Total > @Mod.C_Limit];
> declare $Limit integer = @Mod.C_Limit;
> retrieve $L from Mod.T where [Total > $Limit];
> ```

## Syntax Reference

### Simple Comparisons
Expand Down
5 changes: 3 additions & 2 deletions cmd/mxcli/syntax/features_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,9 +617,10 @@ DESCRIBE DATABASE CONNECTION Ops.Erp;`,
"xpath", "constraint", "where", "predicate",
"filter", "retrieve", "association path", "enumeration",
"formatting", "readable", "line length", "wrap", "multi-line",
"constant", "@",
},
Syntax: "WHERE [condition]\nWHERE [cond1][cond2] -- implicit AND\nWHERE [cond1] AND [cond2]\nWHERE [cond1] OR [cond2]\n\nHow it is STORED is mxcli's choice, not your whitespace: a constraint is\nrebuilt from its parse tree on every write, so the layout is derived from the\nexpression. 80 columns or fewer is stored exactly as written. Longer is broken\nat its top-level and/or joints, one clause per line, so it can be read in\nStudio Pro's XPath editor without scrolling sideways:\n\n [\n Archived = false\n and Status = 'Open'\n and ReportedOn > '[%BeginOfCurrentDay%]'\n ]\n\nWhere and/or meet, the and-runs get explicit parentheses (Mendix binds `and`\ntighter). A clause with no boolean joint — one long comparison or association\npath — is left whole and over width. DESCRIBE puts it back on one line, and\nre-executing that re-derives the same stored text.",
Example: "RETRIEVE $Orders FROM Module.Order\n WHERE [State = 'Completed'][IsPaid = true]\n SORT BY OrderDate DESC;\n\n-- Enumeration attribute: qualified name preferred (mxcli converts to 'Open' in BSON)\nRETRIEVE $Open FROM Module.Order\n WHERE [Status = Module.OrderStatus.Open];\n\n-- OR: string literal form also accepted\nRETRIEVE $Open FROM Module.Order\n WHERE [Status = 'Open'];\n\n-- Association path traversal\nWHERE [Module.Order_Customer/Module.Customer/Name = $Name]\n\n-- Mendix tokens\nWHERE [System.owner = '[%CurrentUser%]']",
Syntax: "WHERE [condition]\nWHERE [cond1][cond2] -- implicit AND\nWHERE [cond1] AND [cond2]\nWHERE [cond1] OR [cond2]\n\nA constraint cannot reference a constant: [Total > @Module.C_Limit] is\nrefused (MDL-XPATH03) — mxbuild rejects it with CE0161 in a retrieve, a data\nsource, an access rule and a user task's targeting. Assign the constant to a\nvariable and compare with the variable.\n\nHow it is STORED is mxcli's choice, not your whitespace: a constraint is\nrebuilt from its parse tree on every write, so the layout is derived from the\nexpression. 80 columns or fewer is stored exactly as written. Longer is broken\nat its top-level and/or joints, one clause per line, so it can be read in\nStudio Pro's XPath editor without scrolling sideways:\n\n [\n Archived = false\n and Status = 'Open'\n and ReportedOn > '[%BeginOfCurrentDay%]'\n ]\n\nWhere and/or meet, the and-runs get explicit parentheses (Mendix binds `and`\ntighter). A clause with no boolean joint — one long comparison or association\npath — is left whole and over width. DESCRIBE puts it back on one line, and\nre-executing that re-derives the same stored text.",
Example: "RETRIEVE $Orders FROM Module.Order\n WHERE [State = 'Completed'][IsPaid = true]\n SORT BY OrderDate DESC;\n\n-- Enumeration attribute: qualified name preferred (mxcli converts to 'Open' in BSON)\nRETRIEVE $Open FROM Module.Order\n WHERE [Status = Module.OrderStatus.Open];\n\n-- OR: string literal form also accepted\nRETRIEVE $Open FROM Module.Order\n WHERE [Status = 'Open'];\n\n-- Association path traversal\nWHERE [Module.Order_Customer/Module.Customer/Name = $Name]\n\n-- Mendix tokens\nWHERE [System.owner = '[%CurrentUser%]']\n\n-- A constant, through a variable\nDECLARE $Limit Integer = @Module.C_Limit;\nRETRIEVE $Big FROM Module.Order\n WHERE [Total > $Limit];",
SeeAlso: []string{"xpath.functions"},
})

Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/language/activity-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ RETRIEVE $RecentOrders FROM Sales.Order

With `FIRST` the result is a single entity object (Mendix's "First object" range). Otherwise the result is a list, including with `LIMIT`/`OFFSET`. A bare `LIMIT 1` is a list of one (a script file without the header reads it differently; see [Language Versions and Migration](versions.md)). `describe` prints the object range as `FIRST`.

A WHERE clause cannot reference a constant. `WHERE Total > @Sales.MinimumTotal` parses, and `mxcli check` refuses it (MDL-XPATH03): Mendix rejects a constant in any XPath constraint with CE0161, in a data source and an access rule as much as in a retrieve. Assign the constant to a variable and compare with the variable:

```sql
DECLARE $Minimum Decimal = @Sales.MinimumTotal;
RETRIEVE $LargeOrders FROM Sales.Order
WHERE Total > $Minimum;
```

### RETRIEVE by Association

Retrieves objects by following an association from an existing object:
Expand Down
1 change: 1 addition & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ it is for pages.
| Rollback | `rollback $entity [refresh];` | Reverts uncommitted changes |
| Retrieve (DB) | `retrieve $Var from Module.Entity [where condition] [sort by Attr asc\|desc, ...] [first \| [limit n] [offset n]];` | Database XPath retrieve. `first` binds a single **object** (Mendix's "First object" range); `limit`/`offset` bind a list. `limit 1` without `offset` is a list of one under `mdl 1;`, and without the header keeps its old meaning, the object, with warning MDL-V1-LIMIT1 |
| Retrieve (DB), sorted | `sort by Attr asc` / `sort by Module.Other.Attr asc` / `sort by Module.Assoc/Module.Other.Attr asc` | A bare name is qualified with the entity **declaring** it, which may be an ancestor. `describe` prints the bare name whenever it resolves back to the stored attribute, and the qualified name otherwise. A sort may also navigate associations — one `/` per hop, the last segment is the attribute — and mxcli stores the hops as the `EntityRef` Mendix needs; without them the build is **CE7247**. **Name the hop when more than one association reaches the same entity**: a bare `Module.Other.Attr` is resolved by inference, which walks the generalization chain across modules (`Administration.Account` reaches `System.Language.Code` through `System.User_Language`) but cannot tell `Order_ShipTo` from `Order_BillTo` — measured, a sort on the billing address round-tripped into one on the shipping address at 0 errors both sides (mendixlabs/mxcli#1152). The same spelling works in a page datasource's `sort by` |
| Retrieve (DB), constant | `declare $Limit Integer = @Module.C_Limit;` then `retrieve $L from Module.Entity where [Total > $Limit];` | An XPath constraint cannot reference a constant: `[Total > @Module.C_Limit]` parses and is refused by check (**MDL-XPATH03**), because mxbuild rejects it with CE0161 in a retrieve, a data source, an access rule and a user task's targeting alike (ako/mxcli#1685). Assign the constant to a variable and compare with that |
| Retrieve (Assoc) | `retrieve $list from $Parent/Module.AssocName;` | Retrieve by association |
| Add to list | `add expression to $list;` | Also accepts existing `add $item to $list;` form |
| Remove from list | `remove $item from $list;` | Change list: Remove |
Expand Down
48 changes: 48 additions & 0 deletions mdl-examples/bug-tests/xpath-1685-constant-in-constraint.fail.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
mdl 1;
-- ============================================================================
-- ako/mxcli#1685 — a constant inside an XPath constraint
-- ============================================================================
--
-- NEGATIVE TEST (.fail.mdl): `mxcli check` MUST reject this file
-- (MDL-XPATH03). An unexpected pass means the refusal has regressed.
--
-- Symptom: `where [Total > @Shop.C_Limit]` was a parse error,
-- line 4:49 mismatched input 'Shop' expecting ']'
-- which reads as "MDL cannot spell this". `@` was an XPath name word of its
-- own, so the lenient re-parse behind describe and the long-constraint layout
-- turned a stored `[Total > @Shop.C_Limit]` into `Total > @` (and wrote
-- `[Total > @]` back for a long one), and a data source written
-- `where Total > @Shop.C_Limit` stored `[Total > ]`.
--
-- The constraint now parses, and check gives mxbuild's answer: an XPath
-- constraint cannot reference a constant. Measured on 11.13.0, each slot below
-- alone in a fresh app (production security, or access rules are not checked):
--
-- [error] [CE0161] "Error(s) in XPath constraint." at Retrieve object(s) activity …
-- [error] [CE0161] "Error(s) in XPath constraint." at List view 'lvItems'
-- [error] [CE0161] "Error(s) in XPath constraint." at Access rule of entity 'Bug1685.Item'
--
-- An unknown constant is the same CE0161, not CE1613: XPath never resolves
-- the name. The variable form that builds is
-- xpath-1685-constant-through-variable.mdl.
--
-- mxcli check mdl-examples/bug-tests/xpath-1685-constant-in-constraint.fail.mdl
-- ============================================================================

create module Bug1685;
create constant Bug1685.C_Limit (Type: Integer, DefaultValue: 10);
create persistent entity Bug1685.Item (Total: Integer, Name: String(100));
create module role Bug1685.User;

create microflow Bug1685.FetchAboveLimit ()
begin
retrieve $Items from Bug1685.Item where [Total > @Bug1685.C_Limit];
end;

create page Bug1685.Items (Title: 'Items', Layout: Atlas_Core.Atlas_Default) {
listview lvItems (DataSource: database from Bug1685.Item where [Total > @Bug1685.C_Limit]) {
dynamictext txtName (Content: 'x')
}
};

grant read * on entity Bug1685.Item to Bug1685.User where [Total > @Bug1685.C_Limit];
24 changes: 24 additions & 0 deletions mdl-examples/bug-tests/xpath-1685-constant-through-variable.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
mdl 1;
-- ============================================================================
-- ako/mxcli#1685 — a constant in a retrieve's constraint, the way that builds
-- ============================================================================
--
-- An XPath constraint cannot reference a constant: `[Total > @Bug1685V.C_Limit]`
-- is refused by `mxcli check` (MDL-XPATH03) because mxbuild rejects it with
-- CE0161 — see xpath-1685-constant-in-constraint.fail.mdl. Assign the constant
-- to a variable and compare with the variable.
--
-- mxcli exec mdl-examples/bug-tests/xpath-1685-constant-through-variable.mdl -p app.mpr
-- mx check app.mpr # 0 errors (measured on 11.13.0 and 11.15.0)
-- mxcli -p app.mpr -c "describe microflow Bug1685V.FetchAboveLimit"
-- ============================================================================

create module Bug1685V;
create constant Bug1685V.C_Limit (Type: Integer, DefaultValue: 10);
create persistent entity Bug1685V.Item (Total: Integer, Name: String(100));

create microflow Bug1685V.FetchAboveLimit ()
begin
declare $Limit Integer = @Bug1685V.C_Limit;
retrieve $Items from Bug1685V.Item where [Total > $Limit];
end;
29 changes: 29 additions & 0 deletions mdl-examples/doctype-tests/16-xpath-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,35 @@ begin
return true;
end;

-- ============================================================================
-- MARK: Constants
-- ============================================================================

-- An XPath constraint cannot reference a constant. `[TotalAmount >
-- @XpathTest.LargeOrderAmount]` parses, and `mxcli check` refuses it
-- (MDL-XPATH03): mxbuild rejects a constant in a retrieve, a data source, an
-- access rule and a user task's targeting with CE0161 (ako/mxcli#1685).
--
-- retrieve $Large from XpathTest.Order
-- where [TotalAmount > @XpathTest.LargeOrderAmount]; -- refused
--
-- In a flow, assign the constant to a variable and compare with the variable.

create constant XpathTest.LargeOrderAmount
( Type: decimal, DefaultValue: 1000 );

/**
* Constant in a constraint, through a variable
*/
create microflow XpathTest.Retrieve_ConstantThroughVariable ()
returns boolean
begin
declare $Threshold decimal = @XpathTest.LargeOrderAmount;
retrieve $Large from XpathTest.Order
where [TotalAmount > $Threshold];
return true;
end;

-- ============================================================================
-- MARK: Nested Predicates
-- ============================================================================
Expand Down
5 changes: 5 additions & 0 deletions mdl/executor/validate_program.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ func ValidateProgram(prog *ast.Program, projectPath string) []linter.Violation {
// answer first; the project, when there is one, for the rest.
violations = append(violations, ValidateDatasourceXPathComparands(prog, project)...)

// A constant in any XPath constraint (MDL-XPATH03): mxbuild rejects
// `[Total > @Mod.C]` with CE0161 whether or not the constant exists, so this
// needs no project (ako/mxcli#1685).
violations = append(violations, validateXPathConstants(prog)...)

// An association between a persistable and a non-persistable entity must
// start in the non-persistable one and have owner Default — CE0001
// (MDL-ASSOC01, ako/mxcli#819). The script's declarations answer first.
Expand Down
19 changes: 13 additions & 6 deletions mdl/executor/validate_user_role_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ func userRoleNames(ctx *ExecContext, prog *ast.Program) (map[string]bool, []stri
// depth: a database retrieve's, a data source's, an access rule's, a user
// task's targeting (as created and as altered) and an offline sync's.
func walkXPathConstraints(v any, fn func(xpath string)) {
walkXPathSlots(v, func(_, xpath string) { fn(xpath) })
}

// walkXPathSlots is walkXPathConstraints that also names the slot each
// constraint sits in ("retrieve", "data source", "access rule", "user task
// targeting", "offline sync"), for a message that says where to look.
func walkXPathSlots(v any, fn func(slot, xpath string)) {
var walk func(rv reflect.Value)
walk = func(rv reflect.Value) {
switch rv.Kind() {
Expand All @@ -134,25 +141,25 @@ func walkXPathConstraints(v any, fn func(xpath string)) {
switch n := rv.Interface().(type) {
case *ast.RetrieveStmt:
if n.StartVariable == "" && n.Where != nil {
fn(expressionToXPath(n.Where))
fn("retrieve", expressionToXPath(n.Where))
}
return
case *ast.DataSourceV3:
fn(n.Where)
fn("data source", n.Where)
return
case *ast.GrantEntityAccessStmt:
fn(n.XPathConstraint)
fn("access rule", n.XPathConstraint)
return
case *ast.WorkflowTargetingNode:
fn(n.XPath)
fn("user task targeting", n.XPath)
return
case *ast.SetActivityPropertyOp:
if n.Property == "targeting_xpath" {
fn(n.Value)
fn("user task targeting", n.Value)
}
return
case *ast.NavSyncDef:
fn(n.Constraint)
fn("offline sync", n.Constraint)
return
}
walk(rv.Elem())
Expand Down
Loading
Loading