From 640e91478f7b1c74b3163fb0d3f289379a9209b3 Mon Sep 17 00:00:00 2001 From: stagknee <241821830+stagknee@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:15:39 -0500 Subject: [PATCH] fix(describe): name the entity an association data source reaches as its context (mendixlabs/mxcli#1571) `describe page` printed the association as the context entity of a data container bound over an association: dataview dvInner (DataSource: $currentObject/Shop.Order_Customer) { -- Context: $currentObject (Shop.Order_Customer) <- the association dataSourceEntityContext used the datasource reference as the entity for every kind but a flow. The reference means a different thing per kind: the entity for a database source, the flow for a microflow/nanoflow source (fixed in 6e59e0e3e), the association for an association source. The page was always correct; only the comment, and the association-name spelling that depends on the context, was wrong. The entity an association source reaches is stored on its last hop (EntityRef.Steps[last].DestinationEntity), which is already right from either end of the association, for a reference set, and for a specialization of the association's end, so it is read instead of re-derived from the domain model. associationSourcePath saw each step's destination but kept only the association names; rawDataSource.Destination now carries the last one, set where both association sources are built (Forms$AssociationSource / data view and the pluggable XPath source). A source with no stored destination keeps the reference, as the flow fix does. The `DataSource:` text is unchanged. Tests: owner-to-child reference, the reverse direction, reference set with its selection variable, specialization, two hops, a page-parameter start, the pluggable source, the no-destination fallback, and database/flow controls. Against upstream/main the association cases fail with the reported symptom. Verified on a Mendix 11.12.5 project: both directions describe the entity, describe -> exec reports "Unchanged page", mx check 0 errors. Co-Authored-By: Claude Sonnet 5.5 --- ...17-describe-context-names-association.json | 1 + CHANGELOG.md | 4 + .../1571-describe-context-association.mdl | 107 +++++++++ mdl/executor/cmd_pages_describe.go | 25 +++ .../cmd_pages_describe_assoc_context_test.go | 204 ++++++++++++++++++ mdl/executor/cmd_pages_describe_datasource.go | 4 +- .../cmd_pages_describe_flowcontext.go | 17 +- .../cmd_pages_describe_flowcontext_test.go | 6 +- 8 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-executor/2026-10-17-describe-context-names-association.json create mode 100644 mdl-examples/bug-tests/1571-describe-context-association.mdl create mode 100644 mdl/executor/cmd_pages_describe_assoc_context_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-describe-context-names-association.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-describe-context-names-association.json new file mode 100644 index 0000000000..0b3e5b17e1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-describe-context-names-association.json @@ -0,0 +1 @@ +{"area": "mdl/executor", "date": "2026-10-17", "symptom": "`describe page` prints `-- Context: $currentObject (Shop.Order_Customer)` — the ASSOCIATION — under a data view or list view bound over an association (`DataSource: $currentObject/Shop.Order_Customer`), where the entity it reaches (`Shop.Customer`) belongs; the page itself is correct (`mx check` clean) and only the comment, and consumers that qualify names against the context, were wrong (mendixlabs/mxcli#1571)", "cause": "`dataSourceEntityContext` returned `rawDataSource.Reference` for every kind but a flow. The Reference means a different thing per source kind: the entity for a database source, the flow for a microflow/nanoflow source (the 6e59e0e3e fix), the association for an association source, the page parameter's name for a parameter source. `associationSourcePath` read each step's DestinationEntity but kept only the association names, so a single-hop source carried nothing but the association", "file": "`mdl/executor/cmd_pages_describe_flowcontext.go` (`dataSourceEntityContext`), `mdl/executor/cmd_pages_describe.go` (`rawDataSource.Destination`, `associationSourceDestination`), `mdl/executor/cmd_pages_describe_datasource.go` (the two sites that build an association source)", "insight": "Deriving the context entity from a datasource reference needs a per-kind rule, and each kind with a non-entity reference is its own instance of this bug: ask what `Reference` holds before using it as an entity. For an association source the answer is stored, not inferred: Forms$AssociationSource, a DataViewSource and a pluggable XPath source all keep EntityRef.Steps[last].DestinationEntity, which is already right in both directions (owner to child and back), for a reference set, and for a destination that specializes the association's end — so read it rather than working out parent/child from the domain model (ALTER PAGE's `entityFromEntityRef` does the same, FINDINGS #55). A step with no stored destination keeps the old output. Not fixed here, same function: a `parameter` source's Reference is the page parameter's NAME, so a data view over `$Order` prints `(Order)`, not `(Shop.Order)`; the entity is stored in the same DataViewSource's EntityRef. The check-time `childContext` already treats an association source as unresolved on purpose"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0798c1db7b..acf2a4e667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- **`describe page` names the entity an association data source reaches, not the association** (mendixlabs/mxcli#1571) — under a data view, list view, grid or gallery bound over an association, the context comment read `-- Context: $currentObject (Shop.Order_Customer)`: the association, where `(Shop.Customer)` belongs. The page was right (`mx check` clean); the comment misled anyone reading `describe` to learn what a nested widget binds, and the same context decides how an association name inside the container is spelled. The entity is the destination stored on the source's last hop, so it is correct from either end of the association, for a reference set, and for a specialization of the association's end. The `DataSource:` text is unchanged. A source with no stored destination prints what it did before. + ## [0.27.0] - 2026-10-15 Headline: **`exec` is all or nothing: when a statement fails at write time, the whole run is rolled back, so a script never leaves a project half-changed.** Alongside it: a wave of `describe` → `check` → `exec` fixes from round-trip runs over real apps (the Siemens FSM template, the GenAI showcase, Marketplace modules), Mendix 9 projects can take microflows, nanoflows and pluggable widgets again, `check -p` resolves many more of the references `exec` and the build would refuse, the classic `filemanager` widget and role-based `Visible: roles (…)` are in MDL, and the companion draws a module's domain model. The MongoDB driver is upgraded for the October 2026 BSON advisories. diff --git a/mdl-examples/bug-tests/1571-describe-context-association.mdl b/mdl-examples/bug-tests/1571-describe-context-association.mdl new file mode 100644 index 0000000000..fec2aa8fb6 --- /dev/null +++ b/mdl-examples/bug-tests/1571-describe-context-association.mdl @@ -0,0 +1,107 @@ +mdl 1; +-- ============================================================================ +-- Bug (mendixlabs/mxcli#1571) — `describe page` named the ASSOCIATION as the +-- context entity of a data container bound over an association: +-- +-- dataview dvCustomer (DataSource: $currentObject/CtxAssoc.Order_Customer) { +-- -- Context: $currentObject (CtxAssoc.Order_Customer) <- the association +-- +-- The datasource reference was used verbatim as the context entity. That is +-- right for a database source (the reference is the entity) and wrong for an +-- association source, whose reference is the association's qualified name — the +-- same mistake the microflow/nanoflow case had (795b-flow-datasource-context- +-- entity.mdl). The page itself was always correct (`mx check` clean); only the +-- comment, and anything resolving names against the context, was wrong. +-- +-- The entity the association reaches is stored on the last hop of the source +-- (EntityRef.Steps[last].DestinationEntity), so describe reads it: the other +-- end for a reference, the element entity for a reference set, and the owner when +-- the page is read from the child's side. +-- +-- Describe output is not asserted by CI for a script (Go tests cover it: +-- mdl/executor/cmd_pages_describe_assoc_context_test.go); this script builds the +-- shapes so the output can be read by hand. +-- +-- Manual verification (needs a project with Atlas_Core): +-- +-- mxcli exec 1571-describe-context-association.mdl -p app.mpr +-- mxcli -p app.mpr -c "describe page CtxAssoc.Probe" +-- mxcli -p app.mpr -c "describe page CtxAssoc.ProbeFromCustomer" +-- +-- Expect, under dvCustomer: -- Context: $currentObject (CtxAssoc.Customer) +-- under lvLines: -- Context: $currentObject (CtxAssoc.OrderLine), $lvLines (selection) +-- under lvOrders (Probe- +-- FromCustomer): -- Context: $currentObject (CtxAssoc.Order), $lvOrders (selection) +-- Before the fix each named the association (CtxAssoc.Order_Customer, +-- CtxAssoc.Order_Line, CtxAssoc.Order_Customer). The `DataSource:` text is +-- unchanged, and describe -> exec still reports "Unchanged page". +-- ============================================================================ + +create module CtxAssoc; +create module role CtxAssoc.User; + +@position(100, 100) +create persistent entity CtxAssoc.Customer ( + Name: string(200) +); + +@position(400, 100) +create persistent entity CtxAssoc.Order ( + Number: string(200) +); + +@position(700, 100) +create persistent entity CtxAssoc.OrderLine ( + Description: string(200) +); + +-- A reference: Order -> Customer. +create association CtxAssoc.Order_Customer from CtxAssoc.Order to CtxAssoc.Customer; + +-- A reference set: Order -> OrderLine. +create association CtxAssoc.Order_Line from CtxAssoc.Order to CtxAssoc.OrderLine type ReferenceSet; + +create or modify page CtxAssoc.Probe ( + Title: 'Probe', + Layout: Atlas_Core.Atlas_Default, + Params: ( $Order: CtxAssoc.Order ) +) { + layoutgrid lg { + row { + column (DesktopWidth: AutoFill) { + dataview dvOrder (DataSource: $Order) { + -- Over a reference: the context is the Customer, not the association. + dataview dvCustomer (DataSource: $currentObject/CtxAssoc.Order_Customer) { + textbox tbName (Label: 'Name', Attribute: Name) + } + -- Over a reference set: the context is one OrderLine. + listview lvLines (DataSource: $currentObject/CtxAssoc.Order_Line) { + textbox tbDescription (Label: 'Description', Attribute: Description, Editable: Never) + } + } + } + } + } +}; + +-- The same reference read from the other side: the context is the Order. +create or modify page CtxAssoc.ProbeFromCustomer ( + Title: 'Probe', + Layout: Atlas_Core.Atlas_Default, + Params: ( $Customer: CtxAssoc.Customer ) +) { + layoutgrid lg { + row { + column (DesktopWidth: AutoFill) { + dataview dvCustomer (DataSource: $Customer) { + listview lvOrders (DataSource: $currentObject/CtxAssoc.Order_Customer) { + textbox tbNumber (Label: 'Number', Attribute: Number, Editable: Never) + } + } + } + } + } +}; + +describe page CtxAssoc.Probe; +describe page CtxAssoc.ProbeFromCustomer; diff --git a/mdl/executor/cmd_pages_describe.go b/mdl/executor/cmd_pages_describe.go index a6a3738ce1..baa43119b1 100644 --- a/mdl/executor/cmd_pages_describe.go +++ b/mdl/executor/cmd_pages_describe.go @@ -612,6 +612,11 @@ type rawDataSource struct { // are rendered, since both re-parse against the source's own entity. SearchAttributes []string ContextVariable string // association source: context variable name (empty → $currentObject) + // Destination is the entity an association source's last hop arrives at, as + // stored (EntityRef.Steps[last].DestinationEntity). Reference is the + // association there, so this is the entity the source puts in scope. Empty + // when the model does not store it. + Destination string // EntitySteps is set on a database source reached over associations from a // context object: a Forms$ListViewXPathSource whose EntityRef is an // IndirectEntityRef. Each hop keeps its stored DestinationEntity, which may @@ -645,6 +650,26 @@ type rawDataSourceArg struct { Value string } +// associationSourceDestination returns the entity the last hop of an association +// source arrives at, as stored — "" when there is no hop or none is stored. +// associationSourcePath keeps only the association of a single hop, so this +// reads the same steps for the entity. +func associationSourceDestination(ds map[string]any) string { + entityRef, ok := ds["EntityRef"].(map[string]any) + if !ok || entityRef == nil { + return "" + } + steps := getBsonArrayElements(entityRef["Steps"]) + if len(steps) == 0 { + return "" + } + last, ok := steps[len(steps)-1].(map[string]any) + if !ok { + return "" + } + return extractString(last["DestinationEntity"]) +} + // associationSourcePath reconstructs the association navigation of a // Forms$AssociationSource (EntityRef = IndirectEntityRef of association steps) // into its path ("Module.Assoc" or "Module.Assoc/…") plus the context variable diff --git a/mdl/executor/cmd_pages_describe_assoc_context_test.go b/mdl/executor/cmd_pages_describe_assoc_context_test.go new file mode 100644 index 0000000000..9525f207b9 --- /dev/null +++ b/mdl/executor/cmd_pages_describe_assoc_context_test.go @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 + +// A data container bound over an association reported the *association* as its +// context entity — `-- Context: $currentObject (Shop.Order_Customer)` instead of +// `(Shop.Customer)` — because the datasource reference was used verbatim. That is +// right for a database source, where the reference is the entity, and wrong for +// a microflow source (its flow) and for an association source (its association). +// The flow case was fixed first; these tests cover the association case. +// +// Storage carries the answer: every step of an association source's EntityRef +// stores the DestinationEntity it arrives at, in either direction. +package executor + +import ( + "strings" + "testing" +) + +func assocStep(assoc, dest string) map[string]any { + return map[string]any{"$Type": "DomainModels$EntityRefStep", "Association": assoc, "DestinationEntity": dest} +} + +func indirectRef(steps ...map[string]any) map[string]any { + items := make([]any, len(steps)) + for i, s := range steps { + items[i] = s + } + return map[string]any{"$Type": "DomainModels$IndirectEntityRef", "Steps": items} +} + +func pageVariable(param string) map[string]any { + return map[string]any{"$Type": "Forms$PageVariable", "PageParameter": param} +} + +// assocDataSource is a Forms$AssociationSource over steps from $currentObject. +func assocDataSource(steps ...map[string]any) map[string]any { + return map[string]any{"$Type": "Forms$AssociationSource", "EntityRef": indirectRef(steps...)} +} + +// outerOrderView wraps inner in a data view over page parameter $Order, the way +// the field report nests it. +func outerOrderView(inner map[string]any) map[string]any { + return map[string]any{ + "$Type": "Forms$DataView", + "Name": "dvOuter", + "DataSource": map[string]any{ + "$Type": "Forms$DataViewSource", + "EntityRef": map[string]any{"$Type": "DomainModels$DirectEntityRef", "Entity": "Shop.Order"}, + "SourceVariable": pageVariable("Order"), + }, + "Widgets": []any{int32(2), inner}, + } +} + +// A context comment is written only above a container's body, so each nested +// container carries one child. +func containerChild() map[string]any { + return map[string]any{"$Type": "Forms$TextBox", "Name": "tbInner"} +} + +func nestedDataView(ds map[string]any) map[string]any { + return map[string]any{"$Type": "Forms$DataView", "Name": "dvInner", "DataSource": ds, "Widgets": []any{int32(2), containerChild()}} +} + +func nestedListView(ds map[string]any) map[string]any { + return map[string]any{"$Type": "Forms$ListView", "Name": "lvInner", "DataSource": ds, "Widgets": []any{int32(2), containerChild()}} +} + +// A data view over a reference reaches the association's other end, and a list +// view over a reference set reaches its elements — in both directions. +func TestDescribe_AssociationSourceContextIsTheEntityReached(t *testing.T) { + tests := []struct { + name string + inner map[string]any + wantLine string + wantExpr string + notWanted string + }{ + { + name: "reference, owner to child", + inner: nestedDataView(map[string]any{"$Type": "Forms$DataViewSource", "EntityRef": indirectRef(assocStep("Shop.Order_Customer", "Shop.Customer")), "SourceVariable": pageVariable("")}), + wantLine: "-- Context: $currentObject (Shop.Customer)\n", + wantExpr: "DataSource: $currentObject/Shop.Order_Customer", + notWanted: "(Shop.Order_Customer)", + }, + { + name: "reference seen from the child's side", + inner: nestedListView(assocDataSource(assocStep("Shop.Order_Customer", "Shop.Order"))), + wantLine: "-- Context: $currentObject (Shop.Order), $lvInner (selection)\n", + wantExpr: "DataSource: $currentObject/Shop.Order_Customer", + notWanted: "(Shop.Order_Customer)", + }, + { + name: "reference set", + inner: nestedListView(assocDataSource(assocStep("Shop.Order_Line", "Shop.OrderLine"))), + wantLine: "-- Context: $currentObject (Shop.OrderLine), $lvInner (selection)\n", + wantExpr: "DataSource: $currentObject/Shop.Order_Line", + notWanted: "(Shop.Order_Line)", + }, + { + // The stored destination can be a specialization of the association's + // own end, and that — not the end — is the entity in scope. + name: "stored destination is a specialization of the end", + inner: nestedListView(assocDataSource(assocStep("Shop.Order_Line", "Shop.SpecialLine"))), + wantLine: "-- Context: $currentObject (Shop.SpecialLine), $lvInner (selection)\n", + wantExpr: "DataSource: $currentObject/Shop.Order_Line", + notWanted: "(Shop.Order_Line)", + }, + { + name: "two hops: the last destination", + inner: nestedListView(assocDataSource( + assocStep("Shop.Order_Customer", "Shop.Customer"), + assocStep("Shop.Address_Customer", "Shop.Address"))), + wantLine: "-- Context: $currentObject (Shop.Address), $lvInner (selection)\n", + wantExpr: "DataSource: $currentObject/Shop.Order_Customer/Shop.Customer/Shop.Address_Customer/Shop.Address", + notWanted: "(Shop.Order_Customer", + }, + { + name: "over a page parameter instead of the context object", + inner: nestedDataView(map[string]any{ + "$Type": "Forms$DataViewSource", + "EntityRef": indirectRef(assocStep("Shop.Order_Customer", "Shop.Customer")), + "SourceVariable": pageVariable("Order"), + }), + wantLine: "-- Context: $currentObject (Shop.Customer)\n", + wantExpr: "DataSource: $Order/Shop.Order_Customer", + notWanted: "(Shop.Order_Customer)", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := describeWidget(t, outerOrderView(tc.inner)) + if !strings.Contains(got, tc.wantLine) { + t.Errorf("describe output lacks %q:\n%s", tc.wantLine, got) + } + if strings.Contains(got, tc.notWanted) { + t.Errorf("describe output still names the association as the context %q:\n%s", tc.notWanted, got) + } + // Only the context comment changes: the datasource text is what + // exec reads back. + if !strings.Contains(got, tc.wantExpr) { + t.Errorf("describe output lacks the unchanged datasource %q:\n%s", tc.wantExpr, got) + } + }) + } +} + +// The pluggable Gallery / Data grid 2 / chart datasource reads through the same +// parser (#941): an XPath source with no entity and the association hops in +// EntityRef. +func TestParseDataSource_PluggableAssociationSourceContextIsTheEntityReached(t *testing.T) { + ds := parseDataSource(map[string]any{ + "$Type": "CustomWidgets$CustomWidgetXPathSource", + "EntityRef": indirectRef(assocStep("Shop.Order_Line", "Shop.OrderLine")), + }) + if ds == nil || ds.Type != "association" { + t.Fatalf("parseDataSource = %+v, want an association source", ds) + } + if got := dataSourceEntityContext(nil, ds); got != "Shop.OrderLine" { + t.Errorf("dataSourceEntityContext = %q, want Shop.OrderLine", got) + } + if got := dataSourceExpr(nil, ds); got != "$currentObject/Shop.Order_Line" { + t.Errorf("dataSourceExpr = %q, want the unchanged $currentObject/Shop.Order_Line", got) + } +} + +// A source whose destination is not stored keeps the reference: no worse than +// before, as the flow fix does for a flow it cannot resolve. +func TestDataSourceEntityContext_AssociationWithoutStoredDestinationKeepsTheReference(t *testing.T) { + ds := parseDataSource(assocDataSource(assocStep("Shop.Order_Customer", ""))) + if ds == nil || ds.Type != "association" { + t.Fatalf("parseDataSource = %+v, want an association source", ds) + } + if got := dataSourceEntityContext(nil, ds); got != "Shop.Order_Customer" { + t.Errorf("dataSourceEntityContext = %q, want the reference kept", got) + } + + // Two hops of which one has no stored destination: the path prints the + // associations alone, and so does the fallback. + ds = parseDataSource(assocDataSource(assocStep("Shop.A_B", "Shop.B"), assocStep("Shop.B_C", ""))) + if got := dataSourceEntityContext(nil, ds); got != "Shop.A_B/Shop.B_C" { + t.Errorf("dataSourceEntityContext = %q, want the reference kept", got) + } +} + +// Controls: a database source keeps naming its own entity, and so does a +// datasource reached over associations that is still a database retrieve. +func TestDescribe_DatabaseSourceContextUnchanged(t *testing.T) { + got := describeWidget(t, outerOrderView(nestedListView(map[string]any{ + "$Type": "Forms$ListViewXPathSource", + "EntityRef": map[string]any{"$Type": "DomainModels$DirectEntityRef", "Entity": "Shop.OrderLine"}, + }))) + if want := "-- Context: $currentObject (Shop.OrderLine), $lvInner (selection)\n"; !strings.Contains(got, want) { + t.Errorf("describe output lacks %q:\n%s", want, got) + } + + got = describeWidget(t, outerOrderView(nestedListView(map[string]any{ + "$Type": "Forms$ListViewXPathSource", + "EntityRef": indirectRef(assocStep("Shop.Order_Line", "Shop.OrderLine")), + }))) + if want := "-- Context: $currentObject (Shop.OrderLine), $lvInner (selection)\n"; !strings.Contains(got, want) { + t.Errorf("describe output lacks %q:\n%s", want, got) + } +} diff --git a/mdl/executor/cmd_pages_describe_datasource.go b/mdl/executor/cmd_pages_describe_datasource.go index a71fdd7a48..567d93030b 100644 --- a/mdl/executor/cmd_pages_describe_datasource.go +++ b/mdl/executor/cmd_pages_describe_datasource.go @@ -104,7 +104,7 @@ func parseDataSource(ds map[string]any) *rawDataSource { } case dsType == dsTypeAssociation: if path, ctxVar := associationSourcePath(ds); path != "" { - return &rawDataSource{Type: "association", Reference: path, ContextVariable: ctxVar} + return &rawDataSource{Type: "association", Reference: path, ContextVariable: ctxVar, Destination: associationSourceDestination(ds)} } case dsType == dsTypeCustomWidgetDatabase: if got := parseCustomWidgetDatabaseSource(ds); got != nil { @@ -329,7 +329,7 @@ func entityPathExpr(ds *rawDataSource) string { // or straight from a page or snippet parameter. func parseContextSource(ds map[string]any) *rawDataSource { if path, ctxVar := associationSourcePath(ds); path != "" { - return &rawDataSource{Type: "association", Reference: path, ContextVariable: ctxVar} + return &rawDataSource{Type: "association", Reference: path, ContextVariable: ctxVar, Destination: associationSourceDestination(ds)} } if sv, ok := ds["SourceVariable"].(map[string]any); ok && sv != nil { if param := extractString(sv["PageParameter"]); param != "" { diff --git a/mdl/executor/cmd_pages_describe_flowcontext.go b/mdl/executor/cmd_pages_describe_flowcontext.go index 4b71321b07..6ddd7a6ca3 100644 --- a/mdl/executor/cmd_pages_describe_flowcontext.go +++ b/mdl/executor/cmd_pages_describe_flowcontext.go @@ -11,14 +11,21 @@ import ( // dataSourceEntityContext returns the entity a datasource puts in scope for the // widgets inside it. // -// For a database or association source the datasource reference *is* the entity. -// For a microflow or nanoflow source it is the flow's own qualified name, so using -// it directly named the flow as the context entity — `-- Context: $currentObject +// For a database source the datasource reference *is* the entity. For a +// microflow or nanoflow source it is the flow's own qualified name, so using it +// directly named the flow as the context entity — `-- Context: $currentObject // (Module.GetOrders)` instead of `(Module.Order)` — and any consumer that resolves // attributes against the context was looking up the wrong element. The flow's // return type is resolved instead, falling back to the reference when the flow // cannot be found or does not return an object/list, which is what the caller // used before. +// +// For an association source the reference is the association's own qualified +// name (`Module.Order_Customer`), the same mistake with a different element. The +// entity it reaches is the destination stored on its last hop, which already +// accounts for the direction travelled (owner to child or back, reference or +// reference set) and for a specialization of the association's end; it is read +// rather than re-derived. A source that stores none keeps the reference. func dataSourceEntityContext(ctx *ExecContext, ds *rawDataSource) string { if ds == nil || ds.Reference == "" { return "" @@ -28,6 +35,10 @@ func dataSourceEntityContext(ctx *ExecContext, ds *rawDataSource) string { if entity := flowReturnEntity(ctx, ds.Type, ds.Reference); entity != "" { return entity } + case "association": + if ds.Destination != "" { + return ds.Destination + } } return ds.Reference } diff --git a/mdl/executor/cmd_pages_describe_flowcontext_test.go b/mdl/executor/cmd_pages_describe_flowcontext_test.go index 9fcd9fba98..a651712608 100644 --- a/mdl/executor/cmd_pages_describe_flowcontext_test.go +++ b/mdl/executor/cmd_pages_describe_flowcontext_test.go @@ -37,8 +37,10 @@ func TestDataTypeEntity(t *testing.T) { } } -// TestDataSourceEntityContext_NonFlowSources: a database or association source -// already names its entity, so it must pass straight through. +// TestDataSourceEntityContext_NonFlowSources: a database source already names its +// entity, so it passes straight through. An association source with no stored +// destination (cmd_pages_describe_assoc_context_test.go covers one that has it) +// and a parameter source keep their reference too. func TestDataSourceEntityContext_NonFlowSources(t *testing.T) { for _, dsType := range []string{"database", "association", "parameter"} { t.Run(dsType, func(t *testing.T) {