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-visitor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@
{"area": "mdl/visitor", "date": "2026-09-24", "symptom": "`datagrid dg (DataSource: Mod.Car, …)` — the bare-entity shorthand — passed `check` (no project), `exec --no-check` printed \"Created page\", `describe page` showed `datagrid dg (onClick: …)` with the source gone, and mxbuild 11.14.0 reported CE0488 \"No entity configured for the data source of this widgets container\" + CE1571 + two column-attribute errors on that one grid. With `-p`, the reference pass refused it instead, but with the misleading \"Attribute 'Name' is bound but there is no enclosing data container providing entity context\" on a grid whose source the script did name.", "cause": "Every dataSourceExprV3 alternative starts with a keyword (DATABASE/MICROFLOW/…) or a VARIABLE, so `DataSource: Mod.Car` matched none and fell through to the generic `keyword COLON propertyValueV3` branch at the end of widgetPropertyV3 (DATASOURCE is in `keyword`). The visitor stored the string \"Mod.Car\"; GetDataSource() only type-asserts *ast.DataSourceV3, so nothing downstream saw it.", "file": "`mdl/visitor/visitor_page_v3.go` (bareEntityDataSource, bareEntityWidgets)", "insight": "Resolve the shorthand in the VISITOR, keyed on widget type (datagrid/listview/gallery/dataview), not as a grammar alternative. The first cut added `DATASOURCE COLON qualifiedName` to widgetPropertyV3 and broke the Barcode Scanner: a pluggable widget's generic keys are its own .mpk keys, and its `datasource: Module.Entity.Code` binds an ATTRIBUTE (Image's `datasource` is an enum) — only the widget type says what the key means. Grep `\"propertyKey\": \"datasource\"` in modelsdk/widgets/definitions before giving a common word a meaning. The tell for the class is a `map[string]any` property whose readers type-assert: a value of the wrong Go type is invisible rather than wrong. Side effects worth knowing: a data view with the shorthand (maint2-editable-never-create-page.mdl had one, unbound and unnoticed) is now refused as MDL-WIDGET09 instead of written unbound; `-p` reference checking on main already refused the grid case but blamed the column ('no enclosing data container'), a downstream symptom reading as user error; ALTER `set DataSource = M.E` was never silent (refuses 'must be a datasource expression'). Control: pre-fix binary + `exec --no-check` on a fresh 11.14.0 app reproduced the issue's four mxbuild errors verbatim; fixed binary, same script, 0 errors.", "refs": ["ako/mxcli#576", "ako/mxcli#552"], "ce": ["CE0488", "CE1571"]}
{"area":"mdl-visitor","date":"2026-09-24","refs":["#653"],"symptom":"describe entity on a view entity, exec'd back, was never idempotent: \"Each cycle reports `Modified view entity` and stores the query with every line after the first indented two spaces further.\" Re-exec of the SAME described file reported Unchanged, so it looked stable until you described again","cause":"The two directions did not mirror: describe (cmd_entities_describe.go, and cmd_diff_mdl.go) prefixes two spaces to every stored OQL line; the visitor stored extractOriginalText(oqlCtx), which starts at the query's first token, so line 1 lost its indentation and lines 2…n kept all of it — +2 per cycle","file":"`mdl/visitor/visitor_entity.go` (dedentOQL, leadingLineWhitespace); tests `mdl/visitor/visitor_view_entity_oql_indent_test.go`, `mdl/executor/view_entity_oql_roundtrip_test.go`; bug-test `mdl-examples/bug-tests/653-view-entity-oql-indent-drift.mdl`","insight":"**Any verbatim-source capture that starts at the first token is asymmetric**: line 1 is dedented for free, the continuation lines are not. Fix it on the way IN (exec), not by making describe emit less: strip the common leading-whitespace prefix of the non-blank lines, counting line 1 at its column when only whitespace precedes it (read it from the input stream, start.GetStart()-GetColumn()). Compare prefixes byte-wise, not by width, so a Studio Pro query indented with tabs comes back byte-identical under describe's two spaces. A round-trip test must run describe → exec at least twice AND start from stored text mxcli did not write (flat, tabs, blank lines, comments): one pass from a script is exactly how this went unnoticed. Control: stubbing dedentOQL to return raw fails every round-trip case with lines 2…n two spaces deeper; a real 11.12.1 project with the old binary printed Modified ×3 with growing indent, the fixed one Unchanged ×3. Separate, not fixed here: a comment AFTER the query's last token is outside the captured span and is dropped on exec"}
{"area": "mdl/visitor", "date": "2026-09-25", "symptom": "A page action's microflow argument `Flag: true and false` was stored as the expression \"trueandfalse\", `Mode: if true then 'a' else 'b'` as \"iftruethen'a'else'b'\", and a REST call parameter `$OrderId = if $x then $id else 'none'` as \"if$xthen$idelse'none'\" (measured by decoding the units on a copy of ako/TestApp). `check -p --references` passed; describe printed the fused text back", "cause": "Four sites stored an expression as text via ANTLR's ctx.GetText(), which concatenates tokens without the hidden-channel whitespace: microflowArgV3 values (page/nanoflow call arguments), contentparams values, send-rest-request WITH parameters, and a dynamic `execute database query`. Literals, `+` and a lone $currentObject are single tokens or need no spacing, so every common case looked correct", "file": "`mdl/visitor/visitor_helpers.go` (`expressionSourceText`), `mdl/visitor/visitor_page_v3.go` (`buildMicroflowArgV3`, `buildParamAssignmentV3`), `mdl/visitor/visitor_microflow_actions.go` (dynamic query, send rest params)", "insight": "**GetText() on an expression context is always a bug** — grep `Expression().*GetText()` / `expr.GetText()` in mdl/visitor; each hit either builds the AST or must use expressionSourceText (whitespace kept, MDL comments stripped). The earlier comment-leak fix moved the six microflow sites to extractExpressionText and missed these four because they lived in page/REST code, not microflow statements: fix a text-extraction defect by searching for the call, not the feature. The tell that hid it: tests used `$currentObject` and `'a' + 'b'`, both immune; a probe with `and` / `if…then` exposed all four at once. Found while writing PROPOSAL_first_class_expressions.md §5.1. Tests `visitor_expression_source_text_test.go`", "refs": ["mendixlabs/mxcli#750"]}
{"area": "mdl/visitor", "date": "2026-09-26", "refs": ["ako/mxcli#706"], "rules": ["MDL-IDX01", "MDL-ENUMDOC01"], "symptom": "Five forms parsed and `check` printed \"Syntax OK\", then the model lost or changed them: `throw 'x';` vanished from the microflow; `Amount: float` / `currency` became String(unlimited) (Void as a microflow type) and `date` became DateTime; `create association X (from A to B, type: referenceset, storage: table)` was stored as a Reference with column storage; a `/** … */` on an enumeration value and an index name (`index Idx on (…)`, `create index Idx on E (…)`) were read and written nowhere", "cause": "Three different gaps behind one symptom: throwStatement had a grammar rule and no listener (a whole statement); buildDataType/buildMicroflowDataType end in a default return (String / Void) that the FLOAT_TYPE and CURRENCY_TYPE tokens fell into, and DATE_TYPE mapped to an ast.TypeDate the writers alias to DateTime; ExitCreateAssociationStatement read only ctx.AssociationOptions(), while the parenthesised alternative puts its options directly under the statement as `(COMMA associationOption)*`. Enumeration-value docs and index names have no home in the metamodel (EnumerationValue and Index have no such property)", "file": "`mdl/visitor/visitor_silent_drops.go` (ExitThrowStatement, EnterDataType, EnterNonListDataType, rejectParenthesisedAssociation), `mdl/executor/validate_unstored_text.go` (MDL-IDX01, MDL-ENUMDOC01); tests `mdl/visitor/silent_drops_706_test.go`, `mdl/executor/validate_unstored_text_test.go`", "insight": "**Split by what is lost: refuse when the MODEL would be wrong, warn when only WORDS are lost.** throw/types/association write a different model, so they are visitor errors with the working spelling in the message; doc comments on values and index names leave a correct model, and were used in 56 places in our own examples, so they are check warnings. **Refuse from the parse tree, not by deleting the grammar alternative**: `float`, `currency`, `date` and `throw` are also keywords-as-identifiers, so removing `| FLOAT_TYPE` from dataType would let `Amount: float` re-parse as an enumeration reference named float — the same silence again. A listener on EnterDataType covers every place a type is written (attribute, parameter, return, declare, constant, service rules) in one method; checking only the direct child token keeps `M.Currency` and `enumeration(M.Float)` legal (the control). To find these: any `if … ; return ast.DataType{Kind: …}` fall-through default in a builder is a silent-substitution site, and a grammar alternative whose sub-rules differ from its sibling's (`associationOption` vs `associationOptions`) needs its own read. Sweep: run old vs new `mxcli check` over `git ls-files '*.mdl'` and diff exit codes — it found every example that relied on the dropped forms (5 files)"}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ alter entity Module.Customer

-- Add an index
alter entity Module.Customer
add index idx_email (Email asc);
add index (Email asc);

-- Reposition entity on domain model canvas
alter entity Module.Customer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ create persistent entity Module.EntityName (
attributename: string(200) not null error 'Attribute name is required',
/** Numeric value */
Amount: decimal,
/** Date field */
CreationDate: date,
/** Date field (there is no date-only type) */
CreationDate: datetime,
/** Boolean flag */
IsActive: boolean not null error 'IsActive flag is required' default true,
/** Enumeration field */
Expand Down
16 changes: 9 additions & 7 deletions .claude/skills/mendix/mdl-entities/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ create persistent entity Module.Customer (
IsActive: boolean default true,

-- Date/Time
BirthDate: date,
BirthDate: datetime, -- there is no date-only type; `date` is refused
-- Use autocreateddate (not datetime) to record when the object was created.
-- 'CreatedDate' as a plain datetime triggers lint error MDL020.
CreatedDate: autocreateddate,
Expand Down Expand Up @@ -77,7 +77,6 @@ create non-persistent entity Module.CustomerSearchParams (
| Decimal | `Name: decimal` | `Amount: decimal` |
| Boolean | `Name: boolean` | `IsActive: boolean` |
| DateTime | `Name: datetime` | `CreatedAt: datetime` |
| Date | `Name: date` | `BirthDate: date` |
| Enumeration | `Name: Module.EnumName` | `status: Module.Status` |
| AutoNumber | `Name: autonumber default 1` | `Code: autonumber default 1` (seed required) |
| Binary | `Name: binary` | `FileData: binary` |
Expand Down Expand Up @@ -259,14 +258,17 @@ create persistent entity Module.Product (
Category: string(50),
Price: decimal
)
index idx_product_code (Code)
index idx_product_category (Category);
index (Code)
index (Category);
/
```

> The `on` keyword is optional and reads SQL-like: `index idx_product_code on (Code)`
> is equivalent to `index idx_product_code (Code)`. Multi-column indexes list the
> columns in order: `index idx_pos on (Row, Col)`.
> A Mendix index has **no name** — its columns, in order and direction, are its
> identity. A name is accepted (`index idx_code on (Code)`) but not stored, so
> `check` warns (MDL-IDX01), `describe` prints the index back as `index (Code)`,
> and `drop index idx_code` cannot find it. Write indexes anonymously; drop one
> by its columns: `alter entity Module.Product drop index (Code)`. Multi-column
> indexes list the columns in order: `index (Row, Col desc)`.

## Complete Domain Model Example

Expand Down
2 changes: 1 addition & 1 deletion cmd/mxcli/syntax/features_domain_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ func init() {
"datetime", "autonumber", "binary", "hashedstring",
"enumeration type", "currency", "float",
},
Syntax: "String(n) Variable-length text up to n characters\nInteger Whole number (-2B to 2B)\nLong Large whole number\nDecimal Precise decimal for currency/calculations\nBoolean True or false\nDateTime Date and time combined\nDate Date only (no time)\nAutoNumber Auto-incrementing integer\nBinary Binary data (files, images)\nHashedString Securely hashed string (passwords)\nEnumeration(Name) Reference to an enumeration\nAutoOwner System.owner (auto-set on create)\nAutoChangedBy System.changedBy (auto-set on commit)\nAutoCreatedDate DateTime (auto-set on create)\nAutoChangedDate DateTime (auto-set on commit)",
Syntax: "String(n) Variable-length text up to n characters\nInteger Whole number (-2B to 2B)\nLong Large whole number\nDecimal Precise decimal for currency/calculations\nBoolean True or false\nDateTime Date and time combined (there is no date-only type)\nAutoNumber Auto-incrementing integer\nBinary Binary data (files, images)\nHashedString Securely hashed string (passwords)\nEnumeration(Name) Reference to an enumeration\nAutoOwner System.owner (auto-set on create)\nAutoChangedBy System.changedBy (auto-set on commit)\nAutoCreatedDate DateTime (auto-set on create)\nAutoChangedDate DateTime (auto-set on commit)\n\nNot types: Date (use DateTime), Float and Currency (use Decimal) - refused.",
Example: "CREATE PERSISTENT ENTITY MyModule.Customer (\n Name: String(100) NOT NULL,\n Age: Integer,\n Balance: Decimal,\n IsActive: Boolean DEFAULT true,\n CreatedAt: DateTime,\n Status: Enumeration(MyModule.Status)\n);",
SeeAlso: []string{"domain-model.entity.attributes"},
})
Expand Down
10 changes: 7 additions & 3 deletions cmd/mxcli/syntax/features_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func init() {

Register(SyntaxFeature{
Path: "microflow.error-handling",
Summary: "Error handling with ON ERROR, THROW, CONTINUE, ROLLBACK",
Summary: "Error handling with ON ERROR, RAISE ERROR, CONTINUE, ROLLBACK",
Keywords: []string{
"error", "error handling", "on error", "continue",
"rollback", "throw", "exception", "try", "catch",
Expand All @@ -165,6 +165,10 @@ func init() {
"-- LOG, SHOW PAGE, CLOSE PAGE, SHOW MESSAGE, VALIDATION FEEDBACK,\n" +
"-- SYNCHRONIZE, DOWNLOAD FILE and the mapping/REST statements.\n" +
"--\n" +
"-- RAISE ERROR ends a handler with an error end event, re-raising the error\n" +
"-- being handled. There is no THROW <expr>: Mendix has no action that raises\n" +
"-- a new error carrying a value, and THROW is refused (it used to be dropped).\n" +
"--\n" +
"-- Two limits, both enforced rather than silently ignored:\n" +
"--\n" +
"-- ON ERROR CONTINUE is rejected by Mendix (CE6035) on CREATE, CHANGE,\n" +
Expand All @@ -180,7 +184,7 @@ func init() {
"-- there in EVERY form, and are refused: a nanoflow activity aborts the\n" +
"-- flow on error by default and has no transaction to roll back.\n" +
"--\n" +
"-- A handler that does NOT end in RETURN/THROW merges back into the main\n" +
"-- A handler that does NOT end in RETURN/RAISE ERROR merges back into the main\n" +
"-- flow, so a variable created after the merge is out of scope on the error\n" +
"-- path (CE0108). End the handler, or expect that.\n" +
"--\n" +
Expand Down Expand Up @@ -285,7 +289,7 @@ func init() {
"-- owns its own object collection and a sequence flow cannot\n" +
"-- leave it, so there is no graph this could build.\n" +
"--\n" +
"-- A path that has already ended (RETURN, THROW, JOIN) does NOT fall\n" +
"-- A path that has already ended (RETURN, RAISE ERROR, JOIN) does NOT fall\n" +
"-- through into a following MERGE — the merge starts a new path.",
Example: "CREATE MICROFLOW M.Post (Payload: String) RETURNS String\n" +
"BEGIN\n" +
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/appendixes/data-type-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ CREATE PERSISTENT ENTITY Demo.AllTypes (
/** Timestamp */
CreatedAt: DateTime,

/** Date only */
BirthDate: Date,
/** Date of birth (Mendix has no date-only type) */
BirthDate: DateTime,

/** File attachment */
Attachment: Binary,
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/language/comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Documentation comments can be placed before:
- Enumeration definitions (becomes enumeration documentation)
- Association definitions (becomes association documentation)

Not before an **enumeration value** or an **index**: Mendix stores no documentation
on a value and no name on an index, so `check` warns that the text is not kept
(`MDL-ENUMDOC01`, `MDL-IDX01`). Use an ordinary `--` comment for a note there.

### Updating Documentation

You can also set documentation on existing entities with `ALTER ENTITY`:
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/language/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ CREATE PERSISTENT ENTITY Demo.AllTypes (
Amount: Decimal DEFAULT 0.00,
IsActive: Boolean DEFAULT TRUE,
CreatedAt: DateTime,
BirthDate: Date,
BirthDate: DateTime,
Attachment: Binary,
Status: Enumeration(Demo.Status) DEFAULT 'Active'
);
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/language/generalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The most common use of generalization is creating an application-specific user e
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
EmployeeNumber: String(20) NOT NULL UNIQUE,
Department: String(100),
HireDate: Date
HireDate: DateTime
);
```

Expand Down
19 changes: 7 additions & 12 deletions docs-site/src/language/primitive-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,15 @@ ModifiedAt: DateTime
ScheduledFor: DateTime
```

DateTime values include both date and time components. For date-only display, use `Date` instead.
DateTime values include both date and time components. To show only the date, format it on the widget; there is no `Date` type.

## Date
## No `Date`, `Float` or `Currency`

Date only (no time component). Internally stored as DateTime in Mendix, but the UI only shows the date portion.

```sql
Date
```

```sql
BirthDate: Date
ExpiryDate: Date
```
Mendix has no date-only attribute type: a date is a `DateTime`, and showing only
its date part is a formatting choice on the widget. `Float` and `Currency` were
removed in Mendix 7 in favour of `Decimal`. MDL refuses all three with the type to
write instead — earlier versions accepted them and silently stored a `DateTime`
(for `date`) or a `String` (for `float` and `currency`).

## AutoNumber

Expand Down
3 changes: 1 addition & 2 deletions docs-site/src/reference/domain-model/create-entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ Each attribute definition specifies a name, a data type, and optional constraint
| Decimal | `Decimal` | Arbitrary-precision decimal |
| Boolean | `Boolean` | `TRUE` or `FALSE`; defaults to `FALSE` when no `DEFAULT` is given |
| DateTime | `DateTime` | Date and time combined |
| Date | `Date` | Date only |
| AutoNumber | `AutoNumber` | Auto-incrementing integer (persistent entities only) |
| Binary | `Binary` | Binary data |
| HashedString | `HashedString` | One-way hashed string (for passwords) |
Expand Down Expand Up @@ -140,7 +139,7 @@ INDEX (Email);
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
EmployeeNumber: String(20) NOT NULL UNIQUE,
Department: String(100),
HireDate: Date
HireDate: DateTime
);
```

Expand Down
Loading
Loading