--- name: xpath-constraints description: "XPath constraint syntax for MDL — retrieve WHERE clauses, page data sources, and row-level entity access, including association paths and functions. Use when writing or debugging any XPath in a project." --- # XPath Constraints in MDL This skill provides reference for writing XPath constraint expressions in MDL RETRIEVE statements, page data sources, and security rules. ## When to Use This Skill - Writing `retrieve ... where [xpath]` statements in microflows - Writing `database from entity where [xpath]` in page data sources - Writing `grant ... where 'xpath'` for row-level entity access - Debugging XPath parsing or serialization issues ## XPath vs Mendix Expressions **Critical distinction**: XPath constraints (inside `[...]`) use different syntax from Mendix expressions (in SET, IF, DECLARE, etc.): | Feature | XPath `[...]` | Mendix Expression | |---------|---------------|-------------------| | Path separator | `/` (always path traversal) | `/` (also division) | | Boolean ops | lowercase: `and`, `or`, `not()` | `and`, `or`, `not` | | Negation | `not(expr)` function | `not expr` | | Empty check | `= empty`, `!= empty` | `= empty` | | 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` | > **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 > value first, then compare against the variable: > ```mdl > declare $Next integer = $Game/MoveSeq + 1; > retrieve $M from Mod.Move where [Seq = $Next] first; > ``` > `mxcli check` explains this and shows the workaround when it sees `+`/`*`/`div`/ > `mod` inside a constraint. > **A negative literal is fine, though.** A leading `-` on a number is a value, > not arithmetic, and needs no quoting: > ```mdl > retrieve $L from Mod.T where [Amount > -7]; > retrieve $L from Mod.T where [Amount <= -12.5 and Code != 'X']; > ``` > **Date arithmetic is not available in XPath.** `addDays()`, `addMonths()` and > friends are *Mendix expression* functions — using one in a constraint fails the > build with `CE0161` regardless of its arguments. For relative dates use the > date tokens (`[%CurrentDateTime%]`, `[%BeginOfCurrentDay%]`, …), or compute the > cut-off in a variable first and compare against that: > ```mdl > declare $Cutoff datetime = addDays([%CurrentDateTime%], -7); > retrieve $L from Mod.T where [DueDate > $Cutoff]; > ``` ## Syntax Reference ### Simple Comparisons ```mdl retrieve $Orders from Module.Order where [State = 'Completed']; retrieve $Active from Module.Customer where [IsActive = true]; retrieve $Recent from Module.Order where [OrderDate != empty]; retrieve $HighValue from Module.Order where [TotalAmount >= $MinAmount]; ``` Operators: `=`, `!=`, `<`, `>`, `<=`, `>=` > **Inline vs quoted form.** The inline bracket form `where [State = 'Completed']` > is preferred. The quoted form — `where '[State = ''Completed'']'`, with internal > single quotes doubled (`''`) — is also accepted for `retrieve` and datasource > `where` clauses, and now stores the identical constraint (it un-escapes the `''` > and strips the outer quotes). Don't double-bracket: write either `[...]` or > `'[...]'`, not both. ### Boolean Logic ```mdl -- AND where [State = 'Completed' and IsPaid = true] -- OR where [State = 'Pending' or State = 'Processing'] -- Grouped where [State = 'Completed' and ($IgnorePaid or IsPaid = true)] -- NOT where [not(IsPaid)] where [not(contains(Name, 'demo'))] ``` ## How a Constraint Is Laid Out on Disk MDL keeps a constraint on one line; how it is **stored** is decided by mxcli, not by the whitespace you type. A constraint is rebuilt from its parse tree on every write, so there is no original formatting to keep — instead the layout is derived from the expression: - **80 columns or fewer** → stored exactly as written, on one line. This is the common case, and it means adding this changed nothing about existing projects. - **Longer** → broken at its top-level `and`/`or` joints, one clause per line, the operator leading each continuation line. Where `and` and `or` meet, the `and` runs get explicit parentheses — Mendix binds `and` tighter, and a filter is being broken up precisely because it had stopped being obvious at a glance. - **Nothing to break on** (one long comparison, one long association path) → left whole and over width. Cutting it anywhere else would not be valid XPath. ``` -- authored (one line, 141 characters) where [Archived = false and Status = 'Open' and Priority = 'High' and Category = 'Electrical' and Severity > 3 and ReportedOn > '[%BeginOfCurrentDay%]'] -- stored, and what Studio Pro's XPath editor shows [ Archived = false and Status = 'Open' and Priority = 'High' and Category = 'Electrical' and Severity > 3 and ReportedOn > '[%BeginOfCurrentDay%]' ] ``` `DESCRIBE` puts it back on one line, so a description reads the way it always has and re-executing it re-derives the same stored text — the unit is reported `Unchanged`. A constraint mxcli cannot parse is stored exactly as given rather than reformatted. This applies to page datasources, `retrieve … where` in microflows, and entity access rules alike. ### Association Path Traversal Bare association paths (without `$variable` prefix) navigate through the domain model: ```mdl -- Single-hop: filter by associated object where [Module.Order_Customer = $Customer] -- Multi-hop: traverse through associations where [Module.Order_Customer/Module.Customer/Name = $CustomerName] -- Existence check: has an associated object where [Module.Order_Customer/Module.Customer] -- Negated existence: has NO associated object where [not(Module.Order_Customer/Module.Customer)] ``` **Rule**: Always use the fully qualified association name (`Module.AssociationName`). > **A bare association name is now caught before the build (MDL-XPATH01).** > `[Order_Customer = $currentUser]` used to pass `mxcli check --references`, get > written by `exec`, and only fail at the build with *"Error(s) in XPath > constraint"* (**CE0161**) — which is the expensive shape, because `exec` cannot > roll back and stops with the model half-updated. `check` now names the > association and the qualified spelling to use instead. It fires only when the > bare name is not an attribute of the constrained entity **and** is a known > association, so attributes stay bare and XPath functions are never touched. > **`= empty` / `!= empty` do not work on associations (CE0161 / MDL047).** > `empty` tests *attribute* nullability only. To test whether an object *has no* > associated object, use negated existence: `[not(Module.Order_Customer/Module.Customer)]` > — **not** `[Module.Order_Customer = empty]`; to test that it *has* one, the path > itself: `[Module.Order_Customer/Module.Customer]` — **not** `!= empty`. > `mxcli check` flags both forms as **MDL047** before the build does. ### Variable Paths ```mdl -- Compare attribute via variable path where [Module.Assoc/Module.Entity/Name = $Variable/Name] -- Variable on right side where [Name = $currentObject/SearchString] ``` ### Nested Predicates Filter intermediate path steps with inline `[predicate]`: ```mdl -- Only lines of completed orders where [Module.OrderLine_Order/Module.Order[State = 'Completed']] -- Nested predicate with further traversal where [Module.OrderLine_Order/Module.Order[State = 'Active']/Module.Order_Category/Module.Category/Name = $CategoryName] -- reversed() path modifier (traverse association in reverse direction) where [System.grantableRoles[reversed()]/System.UserRole/System.UserRoles = '[%CurrentUser%]'] ``` ### Functions ```mdl -- String search where [contains(Name, $SearchStr)] where [starts-with(Name, $Prefix)] where [not(contains(Name, 'demo'))] -- Boolean functions where [IsActive = true()] where [Displayed = false()] ``` Supported functions: `contains()`, `starts-with()`, `not()`, `true()`, `false()` The expression functions `startsWith()` / `endsWith()` are not XPath: in a constraint they are CE0161, and `check` reports them as **MDL091**. So is an operator inside a function argument — `starts-with(Name, 'MS-' + $Key)` is CE0161 although `Name = 'X-' + $Key` is fine: compute the value into a variable first (`declare $P String = 'MS-' + $Key;`, then `starts-with(Name, $P)`). A member the entity does not have is a reference error in `check -p`, and so is a system member written the way `describe` prints the attribute: XPath spells it `createdDate`, `changedDate`, `owner`, `changedBy` — `[CreatedDate > …]` is CE0161. ### Tokens Mendix tokens provide runtime values. In an XPath constraint a token used as a value is stored quoted as `'[%Token%]'` (Studio Pro requires this, or it reports CE0161). mxcli quotes it for you whether you write the bare or quoted form: ```mdl -- Both store identically as '[%CurrentDateTime%]' and pass mx check where [OrderDate < [%CurrentDateTime%]] where [OrderDate < '[%CurrentDateTime%]'] where [System.owner = '[%CurrentUser%]'] ``` > **Tokens are typed.** `[%CurrentUser%]` is a **User** reference — compare it only > to an association to System.User (e.g. `System.owner`), never to a String/other > attribute (`[Title = '[%CurrentUser%]']` is a type error → CE0161). > `[%CurrentDateTime%]` compares to DateTime attributes, etc. > **`System.owner` / `System.changedBy` must be enabled** on the entity before you > can reference them in XPath, or Studio Pro reports CE0161. Enable with > `alter entity Module.Entity add attribute owner: autoowner;` (mxcli's > `check --references` flags this). Same for `changedBy`/`changedDate`/`createdDate`. Common tokens: `[%CurrentUser%]`, `[%CurrentDateTime%]`, `[%CurrentObject%]`, `[%UserRole_RoleName%]`, `[%DayLength%]` ### ID Pseudo-Attribute The `id` pseudo-attribute compares object identity (GUID): ```mdl where [id = $currentUser] where [id != $existingObject] where [id = '[%CurrentUser%]'] ``` ## Usage Contexts ### RETRIEVE in Microflows ```mdl retrieve $Results from Module.Entity where [IsActive = true and State = 'Ready'] sort by Name asc limit 100; ``` The expression inside `[...]` is parsed as XPath and stored in BSON as the `XpathConstraint` field. ### Page Data Sources ```mdl datagrid dg ( datasource: database from Module.Entity where [State != 'Cancelled'] sort by Name asc ) { column (attribute: Name, caption: 'Name') } ``` Multiple bracket constraints can be chained. Consecutive brackets without an operator are treated as AND (standard Mendix XPath): ```mdl -- Consecutive brackets (implicit AND) — standard Mendix XPath syntax datasource: database from Module.Entity where [IsActive = true][Stock > 0] -- Explicit AND: same result datasource: database from Module.Entity where [IsActive = true] and [Stock > 0] -- Mix with OR: combines into single bracket datasource: database from Module.Entity where [IsActive = true] or [Stock > 10] ``` ### GRANT Entity Access (Security) Security rules take the XPath in brackets, like every other XPath, so quotes inside it are written once: ```mdl mdl 1; grant read *, write * on entity Module.Entity to Module.Role where [System.owner = '[%CurrentUser%]']; ``` Sibling groups (`where [a][b]`) are kept as one constraint. The old quoted form `grant Module.Role on Module.Entity (...) where '[...]'` still parses, warns MDL-DEPR030, and `mxcli fmt --upgrade` rewrites it. ## Enumeration Attributes **Critical**: XPath constraints are translated to database SQL WHERE clauses at runtime. The database stores enum values as plain strings (the value key), not qualified names. This means: - `[Status = 'Open']` — always valid: direct string literal match - `[Status = Module.OrderStatus.Open]` — also valid: mxcli converts to `'Open'` in BSON automatically Both forms are accepted by mxcli in the write direction. `DESCRIBE MICROFLOW` always shows the qualified name form for readability, even though BSON stores `'Open'`. **Do NOT use qualified names in expression context (IF, SET, DECLARE) for comparisons** — those contexts use a different form. See `write-microflows` "Enumeration Comparisons" section. ```mdl -- Preferred (mxcli converts to 'Open' in BSON): retrieve $OpenOrders from Module.Order where [Status = Module.OrderStatus.Open]; -- Also accepted (stored as-is): retrieve $OpenOrders from Module.Order where [Status = 'Open']; -- NOT equal retrieve $Active from Module.Order where [Status != Module.OrderStatus.Cancelled]; -- OR across multiple enum values retrieve $InProgress from Module.Order where [Status = Module.OrderStatus.Open or Status = Module.OrderStatus.Processing]; -- Enum combined with other predicates retrieve $Results from Module.Order where [Status = Module.OrderStatus.Completed and TotalAmount >= $MinAmount]; ``` ### Troubleshooting silent empty results with enums If a RETRIEVE returns empty unexpectedly when filtering by an enum attribute: 1. Check the **value key** (not caption) — the key is what's stored in the DB column. Check with `DESCRIBE ENUMERATION Module.EnumName`. 2. Keys are **case-sensitive**: `'open'` ≠ `'Open'`. 3. Confirm the attribute type is actually an enumeration and not a string — `DESCRIBE ENTITY Module.EntityName`. ## Common Patterns ### Parameterized Search ```mdl mdl 1; create microflow Module.Search ($query: string, $ActiveOnly: boolean) returns boolean begin retrieve $Results from Module.Customer where [($ActiveOnly = false or IsActive = true) and (contains(Name, $query) or contains(Email, $query))]; return true; end; ``` ### Date Range Filter ```mdl retrieve $Orders from Module.Order where [OrderDate >= $StartDate and OrderDate <= $EndDate]; ``` ### Optional Filters (empty = skip) ```mdl retrieve $Orders from Module.Order where [($Category = empty or Module.Order_Category = $Category) and ($State = empty or State = $State)]; ``` ### Owner-Based Security ```mdl -- In microflow retrieve $MyItems from Module.Item where [System.owner = '[%CurrentUser%]']; -- In security rule grant read * on entity Module.Item to Module.User where [System.owner = '[%CurrentUser%]']; ``` ## Validation Always validate XPath syntax before execution: ```bash # Syntax check (no project needed) ./bin/mxcli check script.mdl # with reference validation (needs project) ./bin/mxcli check script.mdl -p app.mpr --references ``` ## Troubleshooting | Issue | Cause | Fix | |-------|-------|-----| | `mismatched input` on keyword | Attribute name is a reserved word | This is handled — `xpathWord` accepts any keyword as identifier | | Token not quoted in BSON | Token in Mendix expression context | Use `[...]` bracket syntax for XPath, not bare expression | | `CE0111` path error | Missing module prefix on association | Use `Module.AssociationName`, not just `AssociationName` | | `CE0161` XPath constraint error | Qualified name used for non-enum or wrong format | Use string literal `'Value'` or qualified name `Module.Enum.Value`; mxcli converts automatically | | `not` parsed as keyword | Using `not` (uppercase) in XPath | XPath uses lowercase `not()` as a function | | Retrieve returns empty for enum filter | String literal value key mismatch | Key is case-sensitive; verify with `DESCRIBE ENUMERATION Module.Name` |