Skip to content

Commit a9a0c03

Browse files
akoclaude
andcommitted
fix(describe): read view-entity OQL from source document on modelsdk (11.x)
DESCRIBE of a view entity on the default (modelsdk) engine omitted the `as (… OQL …)` clause — the describe→exec round-trip silently dropped the query. On Mendix 11.0+ a view entity's OQL is stored ONLY in its separate DomainModels$ViewEntitySourceDocument; the inline OqlViewEntitySource.Oql field was removed (the writer already gates this with `if !pv.IsAtLeast(11,0)`). The modelsdk read only read the now-empty inline field via src.Oql() and never followed SourceDocumentRef, so OqlQuery came back empty and the describer skipped the clause. Legacy was correct — loadViewEntityOqlQueries already joins the source docs. Fix: populateViewEntityOql lists the ViewEntitySourceDocument units, maps moduleName.docName → Oql(), and fills OqlQuery for any view entity that has an empty OqlQuery + a SourceDocumentRef. Wired into ListDomainModels and GetDomainModel, guarded by a cheap pre-check so non-view projects pay nothing. Verified: DESCRIBE now emits the OQL and re-execs (create or modify) cleanly, on both the fixture and the chart example's views. Test: TestGetDomainModel_ViewEntityOqlFromSourceDocument (round-trips via GetDomainModel and ListDomainModels). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1be3ff8 commit a9a0c03

3 files changed

Lines changed: 153 additions & 1 deletion

File tree

.claude/skills/fix-issue.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
138138
| A `dynamictext` contentparam (or DataGrid2 column) navigating an association still fails mxbuild **CE0402 "No value specified"** — the *actually unsolved* Bug 3 case. Reproduces only when the widget's entity context is a **specialization** of the entity that declares the association: a grid over `SpecialExpense extends Expense` with `contentparams: [{1} = Expense_Employee/Name]` (the `Expense_Employee` association is on the base `Expense`). An exact-endpoint context (grid over `Expense`) works. `describe` shows `[{1} = <unbound>]`; the binding is dropped. **Very common in real apps** (entities inherit from a base, associations on the base) — which is why the earlier fixes looked incomplete | `resolveAssociationAttributePath` → `associationDestination` matched the context against the association's FROM/TO **by exact string equality**. A subclass context equalled neither endpoint, so it returned `ok=false` and the caller fell back to the flat, unresolvable `Assoc/Attr` path → no valid AttributeRef → CE0402. Affects both engines (shared builder) and both the contentparam and Bug 7 column-attribute paths | `mdl/executor/cmd_pages_builder_v3.go` (`associationDestination`, new `entityIsOrDescendsFrom` + `entityGeneralizations`) | Match the endpoint the context **is or descends from**: walk the generalization chain (`Entity.GeneralizationRef`, qualified parent name) so an association declared on a base entity resolves from a subclass context. Diagnose association-binding drops by checking whether the widget's entity context exactly equals the association endpoint — inheritance is the usual culprit. mxbuild-verified 0 errors across grid-over-base, listview, and grid-over-subclass. Tests: `TestResolveAssociationAttributePath_InheritedContext`; bug-test `mdl-examples/bug-tests/bug3-contentparam-inherited-association.mdl`. Bug 3 (inheritance) |
139139
| A documented object-list widget child keyword parse-errors: `line lRevenue (...)` inside a LineChart/TimeSeries/BubbleChart, or `scalecolor scLow (...)` inside a HeatMap → `mismatched input 'line' expecting '}'`. `series` (etc.) works | The executor side was already complete — `deriveObjectListKeyword` maps `lines`→`LINE` and `scaleColors`→`SCALECOLOR`, and `buildObjectListItem` routes any object-list child generically via the widget's `.def.json`. The **grammar** just lacked the tokens: `LINE`/`SCALECOLOR` weren't lexer tokens nor `widgetTypeV3` alternatives | `mdl/grammar/MDLLexer.g4` (token) + `mdl/grammar/domains/MDLPage.g4` (`widgetTypeV3`) + `mdl/grammar/domains/MDLSettings.g4` (`keyword` rule) | Add the token, the `widgetTypeV3` alternative (alongside `SERIES`/`MARKER`), **and** the `keyword` rule entry (so a very common word like `line` stays usable as an identifier via `identifierOrKeyword`); `make grammar`. No executor/def changes needed — `deriveObjectListKeyword` already produces the singular-uppercase keyword and the def.json carries the mapping. Verify child data lands in BSON (`Lines`/`Series` array); DESCRIBE round-trip of chart object-lists is a separate pre-existing gap (SERIES has it too). Confirmed via `mdl-examples/doctype-tests/34-chart-widget-examples.test.mdl` (exec 0 errors; `line`/`scalecolor` persist) |
140140
| 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.) |
141+
| `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) |
141142

142143
---
143144

mdl/backend/modelsdk/domainmodel.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,60 @@ func (b *Backend) ListDomainModels() ([]*domainmodel.DomainModel, error) {
3232
for _, u := range units {
3333
out = append(out, domainModelFromGen(u.Element, u.ContainerID))
3434
}
35+
b.populateViewEntityOql(out)
3536
// The System module is virtual (not stored in the project); inject its domain
3637
// model so platform entities (System.WorkflowUserTask, User, …) resolve.
3738
out = append(out, buildSystemDomainModel())
3839
return out, nil
3940
}
4041

42+
// populateViewEntityOql fills OqlQuery for view entities from their separate
43+
// DomainModels$ViewEntitySourceDocument. On Mendix 11.0+ the OQL is stored ONLY
44+
// in that document — the writer's version gate drops the inline
45+
// OqlViewEntitySource.Oml field (serializeOqlViewEntitySource), so entityFromGen
46+
// leaves OqlQuery empty and DESCRIBE VIEW ENTITY would omit the `as (…)` clause.
47+
// Follow the SourceDocumentRef here. Mirrors the legacy reader's
48+
// loadViewEntityOqlQueries. No-op when nothing needs it (pre-11 inline OQL, or no
49+
// view entities).
50+
func (b *Backend) populateViewEntityOql(dms []*domainmodel.DomainModel) {
51+
need := false
52+
for _, dm := range dms {
53+
for _, e := range dm.Entities {
54+
if e.SourceDocumentRef != "" && e.OqlQuery == "" {
55+
need = true
56+
}
57+
}
58+
}
59+
if !need {
60+
return
61+
}
62+
units, err := mprread.ListUnitsWithContainer[*genDm.ViewEntitySourceDocument](b.reader)
63+
if err != nil {
64+
return
65+
}
66+
oqlByQN := make(map[string]string, len(units))
67+
for _, u := range units {
68+
name := u.Element.Name()
69+
if name == "" {
70+
continue
71+
}
72+
mi, _ := b.reader.GetModule(string(u.ContainerID))
73+
if mi == nil {
74+
continue
75+
}
76+
oqlByQN[mi.Name+"."+name] = u.Element.Oql()
77+
}
78+
for _, dm := range dms {
79+
for _, e := range dm.Entities {
80+
if e.OqlQuery == "" && e.SourceDocumentRef != "" {
81+
if oql, ok := oqlByQN[e.SourceDocumentRef]; ok {
82+
e.OqlQuery = oql
83+
}
84+
}
85+
}
86+
}
87+
}
88+
4189
// GetDomainModel returns the domain model whose container is moduleID.
4290
func (b *Backend) GetDomainModel(moduleID model.ID) (*domainmodel.DomainModel, error) {
4391
units, err := mprread.ListUnitsWithContainer[*genDm.DomainModel](b.reader)
@@ -46,7 +94,9 @@ func (b *Backend) GetDomainModel(moduleID model.ID) (*domainmodel.DomainModel, e
4694
}
4795
for _, u := range units {
4896
if u.ContainerID == moduleID {
49-
return domainModelFromGen(u.Element, u.ContainerID), nil
97+
dm := domainModelFromGen(u.Element, u.ContainerID)
98+
b.populateViewEntityOql([]*domainmodel.DomainModel{dm})
99+
return dm, nil
50100
}
51101
}
52102
// The System module is virtual — its domain model is not stored in the project,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package modelsdkbackend
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/sdk/domainmodel"
9+
)
10+
11+
// On Mendix 11.0+ a view entity's OQL query is stored ONLY in its separate
12+
// DomainModels$ViewEntitySourceDocument — the inline OqlViewEntitySource.Oql
13+
// field was removed. The modelsdk read must follow the SourceDocumentRef and
14+
// repopulate OqlQuery; otherwise DESCRIBE VIEW ENTITY drops the `as (…)` clause
15+
// and the describe→exec round-trip loses the query. (The fixture is 11.6.6.)
16+
func TestGetDomainModel_ViewEntityOqlFromSourceDocument(t *testing.T) {
17+
proj := copyFixture(t)
18+
const oql = "select s.Name as Name from MyFirstModule.Account as s"
19+
20+
// Author a view entity: source document + entity referencing it.
21+
b := New()
22+
if err := b.Connect(proj); err != nil {
23+
t.Fatalf("connect: %v", err)
24+
}
25+
mod, err := b.GetModuleByName("MyFirstModule")
26+
if err != nil || mod == nil {
27+
t.Fatalf("GetModuleByName: %v %v", mod, err)
28+
}
29+
dm, err := b.GetDomainModel(mod.ID)
30+
if err != nil {
31+
t.Fatalf("GetDomainModel: %v", err)
32+
}
33+
if _, err := b.CreateViewEntitySourceDocument(mod.ID, "MyFirstModule", "VOql", oql, ""); err != nil {
34+
t.Fatalf("CreateViewEntitySourceDocument: %v", err)
35+
}
36+
ent := &domainmodel.Entity{
37+
Name: "VOql",
38+
Persistable: true,
39+
Source: "DomainModels$OqlViewEntitySource",
40+
SourceDocumentRef: "MyFirstModule.VOql",
41+
OqlQuery: oql,
42+
Attributes: []*domainmodel.Attribute{{
43+
Name: "Name",
44+
Type: &domainmodel.StringAttributeType{Length: 200},
45+
Value: &domainmodel.AttributeValue{ViewReference: "Name"},
46+
}},
47+
}
48+
if err := b.CreateEntity(dm.ID, ent); err != nil {
49+
t.Fatalf("CreateEntity: %v", err)
50+
}
51+
if err := b.Disconnect(); err != nil {
52+
t.Fatalf("disconnect: %v", err)
53+
}
54+
55+
// Reopen (fresh read, no write-side state) and confirm OqlQuery round-trips
56+
// via both GetDomainModel and ListDomainModels.
57+
readBack := func(t *testing.T, get func(*Backend) (*domainmodel.DomainModel, error)) {
58+
t.Helper()
59+
b2 := New()
60+
if err := b2.Connect(proj); err != nil {
61+
t.Fatalf("reconnect: %v", err)
62+
}
63+
defer b2.Disconnect()
64+
dm2, err := get(b2)
65+
if err != nil {
66+
t.Fatalf("read: %v", err)
67+
}
68+
var got string
69+
found := false
70+
for _, e := range dm2.Entities {
71+
if e.Name == "VOql" {
72+
found, got = true, e.OqlQuery
73+
}
74+
}
75+
if !found {
76+
t.Fatal("view entity VOql not found on read-back")
77+
}
78+
if got != oql {
79+
t.Errorf("OqlQuery = %q, want %q (read must follow SourceDocumentRef on 11.x)", got, oql)
80+
}
81+
}
82+
83+
t.Run("GetDomainModel", func(t *testing.T) {
84+
readBack(t, func(b *Backend) (*domainmodel.DomainModel, error) { return b.GetDomainModel(mod.ID) })
85+
})
86+
t.Run("ListDomainModels", func(t *testing.T) {
87+
readBack(t, func(b *Backend) (*domainmodel.DomainModel, error) {
88+
dms, err := b.ListDomainModels()
89+
if err != nil {
90+
return nil, err
91+
}
92+
for _, d := range dms {
93+
if d.ContainerID == mod.ID {
94+
return d, nil
95+
}
96+
}
97+
t.Fatal("domain model not found")
98+
return nil, nil
99+
})
100+
})
101+
}

0 commit comments

Comments
 (0)