Skip to content

Commit 1a98683

Browse files
akoclaude
andcommitted
fix(view-entity): catch derived-string CE6770 pre-build, revive OQL type checker
A view entity with a DERIVED string column — cast(x as string), a string-returning CASE, or a string expression — fails MxBuild CE6770 "View Entity is out of sync with the OQL Query" whenever the declared attribute length is anything other than 200. The OQL runs fine, so it looks like an mxcli serialization gap, but it is a Mendix platform rule: Mendix normalizes a derived string column to the default length String(200), while a plain pass-through column inherits its source length. Confirmed on mxbuild 11.6.6: string(30) -> CE6770, string(200) -> 0 errors. mxcli now catches this pre-build as MDL031 with an actionable fix ("change to 'X: String(200)'") instead of letting it fall through to a cryptic build-time CE6770. Two parts: - Infer cast(expr AS string) and string-returning CASE as String(200); normalize any inferred string column to 200 in ValidateOQLTypes; typesStrictlyCompatible now compares string length (not just kind). - Revive the OQL type checker, which was dead: inferTypeStatic / inferTypeFromExpression / inferCaseType uppercased the expression then compared against LOWERCASE literals ("cast(", "count(", "case", "true"...), so every prefix check failed and only DATEPART matched. Sibling of Bug 9b's extractSelectClause case bug. Fixed all prefixes to uppercase. False-positive guard: SUM() over an un-inferable inner (a bare attribute ref -> Unknown) now returns Unknown instead of guessing Decimal, so sum(s.Units) declared integer no longer false-fires (caught on 34-chart-widget-examples.mdl). Tests: TestValidateOQLTypesDerivedString (CAST/CASE length rule), TestValidateOQLTypesNoFalsePositive (still green). Bug-test: mdl-examples/bug-tests/view-entity-derived-string-length.mdl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 64bc653 commit 1a98683

5 files changed

Lines changed: 249 additions & 20 deletions

File tree

‎.claude/skills/fix-issue.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
147147
| A HeatMap `scalecolor` entry's `ColorValue: '#rrggbb'` parses and passes `check` but the colour is **silently dropped on write** — describe shows only `ValuePercentage`, and the runtime scale has no colour. `valuePercentage` persists fine | The schema property is spelled **`colour`** (British), and `scaleColors` had no `ColorValue` MDL alias, so the engine looked up `colour`, didn't find `ColorValue`, and wrote the template default (empty). Identical mechanism to Bug 10a (`columnClass`←`DynamicCellClass`) — a missing `itemPropertyAliases` entry | `mdl/executor/widget_defs.go` (`itemPropertyAliases`, `heatmap.scaleColors.colour`) + `mdl/executor/widget_engine.go` (`WidgetDefGeneratorVersion` bump) | Add `"colour": {"ColorValue"}` to the HeatMap `scaleColors` aliases and **bump `WidgetDefGeneratorVersion`** so cached `.mxcli/widgets/heatmap.def.json` regenerate. DESCRIBE reconstructs it as `Colour:` (the PascalCase schema key — round-trips case-insensitively). Verified: `#rrggbb` now in BSON; describe→exec byte-stable. Test: `TestObjectListItemAliases_HeatMapColour`. Bug 10a class |
148148
| A **PieChart/HeatMap** (charts that bind data at the WIDGET level, no series object-list) fails MxBuild **after `mx update-widgets`** with **CE0642** "Value attribute is required" and (PieChart) **CE4899** "Series name is required" — even though `mxcli check` + `exec` are clean. The datasource persists; the value attribute + series name don't. Only surfaces once CE0463 version-drift is cleared by update-widgets | These widgets expose several attribute-typed top-level properties (`seriesValueAttribute`, `seriesSortAttribute`) plus a required `seriesName` texttemplate. The engine's `resolveMapping` `"Attribute"` case read a single generic `w.GetAttribute()` — ambiguous across multiple attribute props and blind to the friendly MDL names — so `ValueAttribute:` never reached `seriesValueAttribute`; and `GenerateDefJSON` **skips all top-level texttemplate props**, so `seriesName` had no mapping. Item 1b | `mdl/executor/widget_defs.go` (`propertyAliases`, `GenerateDefJSON` attribute-alias + gated texttemplate case) + `mdl/executor/widget_engine.go` (`PropertyMapping.MdlAliases`, `namedPropValue`, `resolveMapping` `"Attribute"`/`"TextTemplate"`, version bump) | Add `PropertyMapping.MdlAliases` (top-level analog of `ItemPropertyMapping.MdlAliases`) + a `propertyAliases` map (piechart/heatmap `seriesValueAttribute`←`ValueAttribute`, piechart `seriesName`←`SeriesName`). `resolveMapping` `"Attribute"` now reads via `namedPropValue` (property key + aliases) and resolves against the widget datasource entity (the DataSource mapping is ordered first), falling back to `w.GetAttribute()` for single-attribute widgets. Emit a top-level texttemplate mapping **only** when an alias is registered (keeps the broad skip). Bump `WidgetDefGeneratorVersion`; the example must supply the required `SeriesName:`. mxbuild-verified: whole chart file = 0 errors after update-widgets. **Diagnose "required property" CE0642/CE4899 by running `mx update-widgets` first (clears CE0463), then check which schema key is missing from the BSON.** Tests: `TestResolveMapping_NamedAttribute`, `TestGenerateDefJSON_PieChartNamedProperties`. Widget-level DESCRIBE of `seriesName`/datasource is a separate gap. Item 1b |
149149
| A view entity whose attribute is named after a Mendix OQL keyword — most often a date-part word like `Quarter`/`Month`/`Year` — passes `mxcli check`/`exec` but fails **MxBuild CE0174** "The 'Quarter' part is incomplete or incorrect. You could use here: … OPEN_QUOTE, or IDENTIFIER". A `Region`/`Period` column is fine | OQL reads the bare word as the keyword, not the attribute. mxcli **cannot escape it**: the OQL grammar has no quoted-identifier form (`s."Quarter"` is a parse error — `qualifiedName`/`atomicExpression` accept only `IDENTIFIER`/`keyword`, not `QUOTED_IDENTIFIER`), and adding one is high-risk (`qualifiedName` is shared by every name in the language). The OQL is a verbatim passthrough (`RawQuery` via `extractOriginalText`), so mxcli isn't mangling it — the name itself is unusable | `mdl/executor/oql_type_inference.go` (`ValidateOQLSyntax`, `oqlReservedWords`, **MDL032**) | **Don't try to fix by quoting** (grammar blast radius) — surface it at check time instead. MDL032 (warning) scans the view OQL for a reserved word in identifier position (after `.` or `as`) and tells the user to rename. Fix for the user: rename the attribute **and its source reference** (`s.Quarter as Quarter` → `s.Period as Period`) — both sides trip CE0174. Reserved set is the date-part words (not exhaustive; SQL words like SELECT are never plausible attribute names). Tests: `TestValidateOQLSyntax_ReservedWord`. (Making quoted OQL identifiers actually work is a deferred grammar change.) |
150+
| A view entity with a **derived string column** — `cast(x as string)`, a string-returning `CASE`, or a string expression — passes `mxcli check`/`exec` but fails **MxBuild CE6770** "View Entity is out of sync with the OQL Query" whenever the declared attribute length is anything other than **200** (e.g. `string(30)`, `string(50)`, unlimited). The OQL itself runs fine, so it looks like a serialization gap but is a **platform rule**: Mendix normalizes a derived string column to the default length String(200); a plain pass-through column (`c.Name as Name`) instead inherits its source length | Two parts. (1) The rule: only *derived* string columns are forced to 200 — a bare attribute ref infers `TypeUnknown` and is skipped, so any concretely-typed String the static inferrer sees is derived → 200. (2) The checker was **dead**: `inferTypeStatic`/`inferTypeFromExpression`/`inferCaseType` uppercased the expr (`upper := ToUpper(...)`) then compared against **lowercase** literals (`HasPrefix(upper, "cast(")`, `"count("`, `"case"`, `== "true"` …), so every prefix check failed and only `DATEPART(` (uppercase literal) ever matched — CAST/CASE/COUNT/SUM/AVG/MIN/MAX/LENGTH inference all returned Unknown. Sibling of Bug 9b's `extractSelectClause` case bug | `mdl/executor/oql_type_inference.go` (`inferTypeStatic` CAST/CASE + case-fixed prefixes, `castTargetType`, `derivedStringLength=200`, `ValidateOQLTypes` string-length normalize, `typesStrictlyCompatible` length compare, `inferCaseType`, `inferTypeFromExpression`) — **MDL031** | Infer `cast(expr AS string)` and string CASE as `String(200)`; in `ValidateOQLTypes` normalize any inferred `TypeString` length to 200 (derived); `typesStrictlyCompatible` compares String **length** (not just kind), so `string(30)` vs `String(200)` is flagged with `Fix: change to 'X: String(200)'`. **Fix the case-comparison bug** (lowercase→UPPERCASE literals) so the checker actually runs. **Guard against false positives**: `SUM(` over an un-inferable inner (a bare attribute ref → Unknown) returns **Unknown**, not a guessed Decimal — otherwise `sum(s.Units)` declared `integer` false-fires (caught on `34-chart-widget-examples.mdl`). mxbuild-confirmed on 11.6.6: `string(30)`→CE6770, `string(200)`→0 errors. Tests: `TestValidateOQLTypesDerivedString`, `TestValidateOQLTypesNoFalsePositive`; bug-test `mdl-examples/bug-tests/view-entity-derived-string-length.mdl` |
150151
| `DESCRIBE` of a view entity on the **default (modelsdk)** engine omits the `as (… OQL …)` clause entirely (legacy renders it) — the describe→exec round-trip silently drops the query, leaving a queryless view. Reproduces on any Mendix **11.x** project | On Mendix 11.0+ the OQL is stored **only** in the separate `DomainModels$ViewEntitySourceDocument`; the inline `OqlViewEntitySource.Oql` field was removed (see the writer's `if !pv.IsAtLeast(11,0)` gate in `serializeOqlViewEntitySource`). The modelsdk read (`entityFromGen`: `out.OqlQuery = src.Oql()`) only read the now-empty inline field and never followed the `SourceDocumentRef`. Legacy was correct — `loadViewEntityOqlQueries` in `sdk/mpr/reader_documents.go` already joins the source docs | `mdl/backend/modelsdk/domainmodel.go` (`populateViewEntityOql`, wired into `ListDomainModels` + `GetDomainModel`) | After building the domain models, list `ViewEntitySourceDocument` units (`mprread.ListUnitsWithContainer[*genDm.ViewEntitySourceDocument]`), map `moduleName.docName → Oql()`, and fill `OqlQuery` for any view entity with an empty `OqlQuery` + a `SourceDocumentRef` (mirrors the legacy join; guarded by a cheap pre-check so non-view projects pay nothing). When a view-entity read differs between engines on 11.x, suspect a field the platform moved out-of-line into a source document. Test: `TestGetDomainModel_ViewEntityOqlFromSourceDocument` (round-trips via both read paths) |
151152
| An `actionbutton`/`linkbutton` `icon:` property is silently dropped on write (flagged `MDL-WIDGET07` "property `icon` … will be silently dropped") — no way to give a button an icon, so the button renders without one. `linkbutton` (Link render mode) itself already worked | The button builder read Caption/Action/ButtonStyle but never the `icon` property, and the `Forms$ActionButton` codec default nulls the `Icon` field. Nothing built the Mendix icon element | `sdk/pages/pages_widgets_action.go` (`Icon` struct + `IconTypeIconCollection`) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildButtonV3` reads `icon`) + `mdl/backend/modelsdk/widget_write.go` (`iconToGen`, wired into the `ActionButton` case) + describe (`cmd_pages_describe_parse.go` `extractIconRef`, `cmd_pages_describe_output.go` emit) + `validate_widgets.go` (`staticWidgetKnownProps` += `Icon`) | Build the modern **icon-collection** icon: `icon: 'Atlas_Core.Atlas_Filled.pencil'` → `Forms$IconCollectionIcon{Image: QN}` (verified against a Studio-Pro button — storage `$Type` is `Forms$`, not `Pages$`; no typed gen struct, so build it via `newElem`/`addStr`). `g.SetIcon(...)` overrides the null-Icon default. DESCRIBE reconstructs `Icon:` from the element's `Image`; add `Icon` to the known-props list so MDL-WIDGET07 stops warning. A non-existent icon name is (correctly) rejected by MxBuild with **CE1613** "selected custom icon … no longer exists". Tests: `TestBuildButtonV3_Icon`, `TestOutputWidgetMDLV3_ButtonIcon`; bug-test `mdl-examples/bug-tests/602-button-icon.mdl`. Issue #602 |
152153
| `DESCRIBE WORKFLOW` on the **default (modelsdk)** engine renders some activities as non-round-trippable comments — `-- [Workflows$StartWorkflowActivity]`, `-- [Workflows$JumpToActivity]`, `-- [Workflows$WaitForTimerActivity]`, `-- [Workflows$WaitForNotificationActivity]` — so `describe → exec` drops those activities and you can't learn their syntax from an existing workflow. User tasks, decisions, splits, call-microflow/workflow round-trip fine (legacy round-trips all of them) | The describe **formatter** (`formatWorkflowActivities`) already has typed cases for jump/wait/start/end, but the modelsdk **reader** never produced them: these activities have no `genWf` struct (written via generic `newElem`), so `workflowActivityFromGen`'s type switch fell through to `GenericWorkflowActivity`, which the formatter renders as `-- [$Type]`. Legacy's `parser_workflow.go` had dedicated cases, so legacy was correct — modelsdk-only gap | `mdl/backend/modelsdk/workflow_read.go` (`workflowActivityFromGen` default → new `workflowSimpleActivityFromGen`) | Recognise the untyped simple activities by `el.TypeName()` and rebuild the semantic struct, reading fields from raw BSON via `genWf.RawFieldString` (`TargetActivity` for jump, `Delay` for the timer) — mirrors the legacy parser. Start/end become typed so the formatter **skips** them (implicit in MDL); jump/wait become executable statements. When an engine emits `-- [$Type]` in describe, the *formatter* usually already handles the type — the gap is the *reader* decoding it to `GenericWorkflowActivity`. Test: `TestWorkflowSimpleActivities_ReconstructedTyped`; round-trip validated describe→drop→exec→`docker check` = 0 workflow errors. Bug 11b (11a = the missing `write-workflows.md` authoring skill) |

‎.claude/skills/mendix/mdl-entities.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,24 @@ as
223223
/
224224
```
225225

226+
**Derived string columns must be `string(200)`.** A plain pass-through column
227+
(`c.Name as CustomerName`) inherits its source attribute's length, so declaring
228+
`string(100)` above is fine. But a **derived** string column — `cast(x as
229+
string)`, a string-returning `CASE`, or a string expression — is normalized by
230+
Mendix to the platform default length **`string(200)`**. Declaring any other
231+
length (`string(30)`, unlimited, …) passes `mxcli check`'s parser but fails the
232+
MxBuild consistency check with **CE6770 "View Entity is out of sync with the OQL
233+
Query."** `mxcli check` catches this pre-build as **MDL031** with a suggested
234+
fix:
235+
236+
```mdl
237+
create view entity Module.TicketLabel (
238+
StatusLabel: string(200) -- derived → must be 200, not string(30)
239+
) as
240+
select cast(t.Status as string) as StatusLabel from Module.Ticket t;
241+
/
242+
```
243+
226244
## Entity with Index
227245

228246
```mdl
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
-- Bug: a view entity with a DERIVED string column (CAST(x AS string), or a
2+
-- string-returning CASE) fails MxBuild CE6770 "View Entity is out of sync with
3+
-- the OQL Query" whenever the declared attribute length is anything other than
4+
-- 200 — even though the OQL itself runs fine and mxcli's own `check`/`exec`
5+
-- accepted it. The confusing part: the OQL is valid, so the failure looks like
6+
-- an mxcli serialization gap, but it is a Mendix PLATFORM rule.
7+
--
8+
-- Root cause (platform behavior, now caught pre-build by mxcli):
9+
-- Mendix normalizes a DERIVED string column to the platform default length
10+
-- (String(200)). A plain pass-through column (e.g. `c.LastName as Name`)
11+
-- inherits its source attribute's length, but any CAST-to-string, a
12+
-- string-returning CASE, or a string expression becomes String(200). The
13+
-- view-entity static sync check (CE6770) then rejects a declaration that says
14+
-- string(30) / string(50) / unlimited.
15+
--
16+
-- Fix in mxcli: the OQL type checker (`ValidateOQLTypes`, MDL031) now infers a
17+
-- derived string column as String(200) and flags a mismatched declared length
18+
-- at `check` time with an actionable suggestion — instead of letting it fall
19+
-- through to a cryptic CE6770 at build time. (The checker was previously dead
20+
-- due to a case-comparison bug — sibling of Bug 9b — which this fix also
21+
-- repairs, so CAST/CASE/aggregate inference actually runs now.)
22+
--
23+
-- Verify:
24+
-- ./bin/mxcli check mdl-examples/bug-tests/view-entity-derived-string-length.mdl
25+
-- → MDL031 on StatusLabelWrong (declared string(30), OQL returns String(200))
26+
-- → no MDL031 on StatusLabelOk (declared string(200))
27+
-- In Studio Pro / mxbuild: the string(200) view builds clean; changing it to
28+
-- string(30) reproduces CE6770.
29+
30+
create module VDeriv;
31+
32+
create enumeration VDeriv.Status (
33+
Open 'Open',
34+
Closed 'Closed'
35+
);
36+
37+
create entity VDeriv.Ticket (
38+
Title: string(100),
39+
State: VDeriv.Status
40+
);
41+
42+
-- WRONG: a derived (CAST-to-string) column declared as string(30) → CE6770.
43+
-- mxcli now flags this at check time as MDL031.
44+
create view entity VDeriv.TicketLabelWrong (
45+
StatusLabelWrong: string(30)
46+
) as
47+
select
48+
cast(t.State as string) as StatusLabelWrong
49+
from VDeriv.Ticket as t;
50+
51+
-- CORRECT: the same derived column declared as string(200) → builds clean.
52+
create view entity VDeriv.TicketLabelOk (
53+
StatusLabelOk: string(200)
54+
) as
55+
select
56+
cast(t.State as string) as StatusLabelOk
57+
from VDeriv.Ticket as t;
58+
59+
-- Also correct: a string-returning CASE is likewise String(200).
60+
create view entity VDeriv.TicketBucketOk (
61+
Bucket: string(200)
62+
) as
63+
select
64+
case when t.State = 'Open' then 'Active' else 'Done' end as Bucket
65+
from VDeriv.Ticket as t;

0 commit comments

Comments
 (0)