Skip to content

M2M: Handle intermediate objects created during transform#4849

Draft
aziemchawdhary-gs wants to merge 26 commits into
finos:masterfrom
aziemchawdhary-gs:sourceTreeIntermediate
Draft

M2M: Handle intermediate objects created during transform#4849
aziemchawdhary-gs wants to merge 26 commits into
finos:masterfrom
aziemchawdhary-gs:sourceTreeIntermediate

Conversation

@aziemchawdhary-gs

Copy link
Copy Markdown
Contributor

No description provided.

aziemchawdhary-gs and others added 24 commits June 15, 2026 12:59
Repros infinite recursion in meta::pure::functions::meta::allNestedProperties
when class graph contains a bidirectional non-association reference.

Observed failure:
[ERROR]   PureTestBuilder$PureTestCase.testAllNestedPropertiesTerminatesOnCycle » StackOverflow
  at core_pure_corefunctions_metaExtension.Root_meta_pure_functions_meta_allNestedProperties_Class_1__AbstractProperty_MANY_(core_pure_corefunctions_metaExtension.java:491)
  (recursive frames via CompiledSupport.mapToManyOverMany -> lambda -> allNestedProperties)
Threads a Map<String, Class<Any>> visited set through the recursion,
keyed on elementToPath() for stable identity. Public arity-1 entry
seeds an empty map; the private overload checks and short-circuits
on already-visited classes.

Tests run: 1006, Failures: 0, Errors: 0.
Repros the buildPropertyPathUptoOwner bug where the navigation map is
keyed by Type but $prop.owner can be an Association. The fixture uses
a CompanyEmployee source with an intermediate that exposes a Person,
then accesses Person.address (defined on association Person_Address).
buildPropertyPathUptoOwner walks back from .street; when it reaches
the address node, ownerClass(address) = Person (not source CompanyEmployee),
the buggy branch fires, and $t->get($prop.owner) looks up an Association
in a Type-keyed map -> empty -> ->toOne() fails.

Observed failure:
  testIntermediateOverAssociation(org.finos.legend.pure.m3.execution.test.PureTestBuilder$PureTestCase) <<< ERROR!
  Execution error at (resource:/core/pure/graphFetch/graphExtension.pure line:745 column:72), "Cannot cast a collection of size 0 to multiplicity [1]"
  at Root_meta_pure_graphFetch_buildPropertyPathUptoOwner_PropertyPathTree_1__Class_1__Map_1__PropertyPathTree_1_
- Look up the navigation map by Class (via ownerClass()) instead of
  $prop.owner, which is PackageableElement and may be an Association
  for association-owned properties. The map is keyed by Type, so an
  Association lookup silently returned empty and the subsequent
  ->toOne() crashed with "Cannot cast a collection of size 0 to
  multiplicity [1]" (the failure mode demonstrated by Task 3's test
  testIntermediateOverAssociation).
- Add a depth guard (max 64) with a clear assertion message so an
  unreachable owner produces a diagnosable error instead of a stack
  overflow.
- Drop the dangling $prop.owner comment and the dead newclz locals.

Tests run: 1007, Failures: 0, Errors: 0.
…igation

The Order class has two properties typed Customer (primaryCustomer and
secondaryCustomer). The mapping wraps primaryCustomer through an
intermediate while accessing secondaryCustomer directly. The current
childPropertiesMap is a Map<Type, AbstractProperty> keyed by Customer
that overwrites on collision, so when buildPropertyPathUptoOwner needs
to navigate back to Order via the intermediate, it picks whichever of
{primaryCustomer, secondaryCustomer} happened to win the map insertion
- which is not in general the intended primaryCustomer.

Observed failure (testCollidingReturnTypes):
  expected: 'Order(amount, primaryCustomer(name), secondaryCustomer(name))'
  actual:   'Order(amount, secondaryCustomer(name))'
The primaryCustomer branch is dropped because Customer's slot in
childPropertiesMap was overwritten by secondaryCustomer.
childPropertiesMap is now Map<Type, List<AbstractProperty<Any>>> built
via groupBy on the property return type, so Order with two
Customer-typed properties (primaryCustomer + secondaryCustomer) keeps
both entries instead of one overwriting the other.

buildPropertyPathUptoOwner now consults a deterministic picker
(pickIntermediateProperty) that prefers an exact return-type match and
breaks ties by lexicographic property name, so the navigation back to
owner is reproducible across runs/Pure compiler versions.

findSubTreeWithOwnerOrPropertyTreesFromOwner's signature is updated to
take the List-valued map; behaviour is unchanged (still only checks
isNotEmpty).

Used the orElse(list([])).values workaround for Map::get to defensively
handle missing keys, as suggested in the task plan.

Fixes the failing test testCollidingReturnTypes added in Task 5.

Tests run: 1008, Failures: 0, Errors: 0.
meta::pure::functions::collection::groupBy already returns
Map<K, List<X>>[1] natively. The keyValues -> map -> newMap pipeline
introduced in Task 6 was a no-op rewrap. Direct groupBy result is the
expected Map<Type, List<AbstractProperty<Any>>>[1].

Tests run: 1008, Failures: 0, Errors: 0.
testSourceAccessViaSupertypeProperty exercises a direct Dog->DogTarget
mapping where Dog.species is inherited from Animal (supertype). The
PropertyPathNode arm of findSubTreeWithOwnerOrPropertyTreesFromOwner
already handles this case, so this test passes today - kept as a
regression guard.

testIntermediateOverSubtype is the failing case: a KennelIntermediate
slot is typed at Animal (the supertype), but the mapping's source is
Dog (the subtype). When the algorithm encounters an Animal-typed Class
node in the property tree and tries to match it against owner Dog, the
current check $clz->isStrictSubType($ownerClass) (Animal->isStrictSubType(Dog))
is false, so the supertype-rooted subtree is not recognised and the
walk-up fails.

Observed failure: testIntermediateOverSubtype - expected
'Dog\n(\n  breed\n  species\n)' but got 'Dog\n(\n  breed\n)' - the
species node is missing because the Animal-rooted property subtree was
not recognised as belonging to source Dog.
…e directions

The Class arm and PropertyPathNode arm previously only accepted
X->isStrictSubType(ownerClass) - i.e. the tree class is a subtype of
owner. The pre-branch behaviour also accepted the supertype direction
(ownerClass->isStrictSubType(X)). This commit reinstates that direction
on both arms so a tree whose root class is a supertype of owner is
still recognised as a subtree-of-owner.

Note: testIntermediateOverSubtype still fails after this change - it
relies on isTreeRootOwner accepting the supertype direction as well,
which is fixed by Task 10.
      supertype-typed slot is not handled

The fixture is kept as a placeholder for follow-up investigation.
The supertype-direction case requires more careful changes to
buildPropertyPathUptoOwner's recursion than Task 10's planned
isTreeRootOwner tweak alone provides: making isTreeRootOwner return
false on empty treeOwner regresses three other tests that rely on
the silent legacy-fallback. The right design needs to thread a
supertype-aware terminator through buildPropertyPathUptoOwner that
preserves the navigation rather than early-returning the leaf
sub-tree.
…rees found

If findSubTreeWithOwnerOrPropertyTreesFromOwner returns empty, building
a fresh ^PropertyPathTree with empty children silently produces an empty
source tree and loses information. Add an inner check: when no
owner-belonging subtrees are found, fall back to the legacy
propertyTreeToGraphFetchTree($owner) path instead.

Tests run: 1009, Failures: 0, Errors: 0.
Tests run: 1009, Failures: 0, Errors: 0.
- testIntermediateWithSubtypeTarget (Task 13): M2M subtype mapping
  where TgtCircle's source goes through an intermediate. Currently
  the subType branch's children are not enriched into the source
  tree; marked test.ToFix.
- testUnionWithIntermediateMember (Task 14): union mapping where one
  member uses an intermediate, the other doesn't. Operation set
  handling in calculateSourceTree is a documented gap
  (see TODO in testOnSourceRoot.pure); marked test.ToFix.
- testTwoHopIntermediate (Task 15): two-level intermediate chain;
  exercises buildPropertyPathUptoOwner recursion. Passes.
- testUnreachableOwnerGivesClearError (Task 16): negative path -
  intermediate references a foreign class with no route back to
  the source. The unreachable-owner assertion isn't currently
  reached in this configuration; marked test.ToFix.

Test_Pure_Core: 1010 tests run (baseline 1009 + 1 newly-passing test;
3 marked test.ToFix following the existing testIntermediateOverSubtype
precedent in the same file).
Mappings whose root class is an Operation (e.g. *Tgt: Operation { union(a, b) })
previously crashed calculateSourceTree with "Match failure: OperationSetImplementation"
because the top-level dispatch only had a PureInstanceSetImplementation arm.

Resolve the operation to its constituent PureInstanceSetImplementations, compute a
source tree per branch, and combine using the first branch as the base with the
remaining branches attached as subTypeTrees. Drops the test.ToFix marker on
testUnionWithIntermediateMember.
For a mapping whose transform routes through an intermediate class that
has no property path back to the source class (e.g. wrap-and-leave-foreign
pattern), calculateSourceTree previously took the isTreeRootOwner permissive
fallback and silently returned a degenerate source tree.

Add an owner-reachability precondition at the top of
enrichSourceTreeNodeForProperty: when the property tree references classes
the owner can neither share hierarchy with nor reach via
findSubTreeWithOwnerOrPropertyTreesFromOwner, raise the same diagnostic
buildPropertyPathUptoOwner would have raised. Gated on
$setImplementation.srcClass == $owner so the check is inert during
operation-set fold dispatch into sibling subtrees (where srcClass != owner
is the legitimate cross-branch shape).

Drops the test.ToFix marker on testUnreachableOwnerGivesClearError. Also
hoists childPropertiesMap / propertyTreesBelongingToOwner out of the
!isTreeRootOwner branch to share with the precondition.
…class

A target tree like `TgtCatalog { shapes { area }, shapes->subType(@TgtCircle) { radius } }`
where the property mapping (`shapes : $src.shapes`) targets only the base class
(TgtShape) was silently dropping the subType subtree: the propertyMappings filter
required the mapping's target to be a (reflexive) subtype of the requested
subType, which excludes the base mapping.

Two changes in enrichSourceTreeNodeForProperty:
- Accept the property mapping when the requested subType is a subtype of the
  mapping's target class (in addition to the existing direction). Mappings like
  per-subset b[b1]/b[b2] still match only their own subset; single-default
  mappings now also flow through to handle the subtype subtree.
- When tgtPgft.subType is set, use it as the lookup class for childSIs so the
  subtype's set implementation (e.g. *TgtCircle) is picked up, not the
  property's natural return type's set impl.

Drops the test.ToFix marker on testIntermediateWithSubtypeTarget.
For a mapping whose source class is a subtype (Dog) of an intermediate slot's
return type (Animal), accessing an inherited property (species) via the
intermediate (`$src->wrap().animal.species`) raised:

  No intermediate-class property of Dog returns Animal - cannot build path
  back to owner.

The two contributing checks both treated owner-direction as the only valid
shape:

- buildPropertyPathUptoOwner's guard accepted propOwner == owner and
  propOwner subtype-of-owner but not owner subtype-of-propOwner. The
  inheritance direction (species defined on Animal, accessed on Dog) hit
  the candidate lookup and asserted with no candidates.
- addPropertyGraphFetchTrees' PropertyPathNode arm only accepted node.class
  == ownerClass or subtype-of-ownerClass, so even if the path reached the
  synthesised tree the inherited leaf was filtered out (the doc's Attempt 1
  symptom: silently produces `Dog { breed }`).

Fixing both directions together: the buildPropertyPathUptoOwner guard now
also accepts owner subtype-of-propOwner (the inheritance case — leave pTree
in place), and addPropertyGraphFetchTrees mirrors the Class-node arm's
bidirectional check for PropertyPathNode so the inherited property attaches
directly under owner. Drops test.ToFix on testIntermediateOverSubtype.
@aziemchawdhary-gs aziemchawdhary-gs requested a review from a team as a code owner June 15, 2026 12:00
@linux-foundation-easycla

linux-foundation-easycla Bot commented Jun 15, 2026

Copy link
Copy Markdown

CLA Not Signed

@aziemchawdhary-gs aziemchawdhary-gs marked this pull request as draft June 15, 2026 12:00
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

Test Results

  1 095 files   -      31    1 095 suites   - 31   3h 6m 48s ⏱️ - 1h 37m 47s
12 078 tests  - 2 486  11 901 ✔️  - 2 484  177 💤  - 2  0 ±0 
34 225 runs   - 6 435  34 048 ✔️  - 6 433  177 💤  - 2  0 ±0 

Results for commit 2904199. ± Comparison against base commit e6f76b0.

♻️ This comment has been updated with latest results.

testUnionSubTypeSourceWithType (propertyUnion.pure) failed with
"No intermediate-class property of _S_PersonC returns _S_Person - cannot
build path back to owner." even though _S_Person is the supertype of
_S_PersonC and should be excluded from the precondition's
non-trivial-tree-classes set.

Cause: Pure's lazy evaluation can hand back distinct Class objects for
the same type when navigated from different roots. The existing
isStrictSubType helper in this file explicitly switches to element-path
comparison for the same reason. The Gap 4 precondition was still using
object-identity `contains`, so the supertype reference encountered in
the property tree did not match anything in the owner's generalisations
and slipped through into nonTrivialTreeClasses.

Compare by elementToPath() for: owner-vs-tree-class equality, the
owner-generalisations contains check, the reverse generalisations check,
and the setImpl.srcClass == owner gate. Adds testUnionSubtypeSourceReads-
InheritedProperty as a focused regression covering the same shape in
Test_Pure_Core.
testNewOperatorInMapping (functionInMapping.pure) failed with
"No intermediate-class property of Firm returns PersonB - cannot build
path back to owner." for the mapping `count : ^PersonB(...).middleNames->size()`.
The transform constructs a literal PersonB and reads its empty middleNames
list — no source data is required, so the expected source tree is `Firm {}`
and the legacy fallback's empty output is correct.

My owner-reachability precondition was relying on
findSubTreeWithOwnerOrPropertyTreesFromOwner, which only matches tree
classes against childPropertiesMap by exact-class equality. PersonB is not
in Firm's childPropertiesMap (Firm.employees returns Person, not PersonB),
so the lookup missed the inheritance reachability and the precondition
fired on a benign literal-construction case.

Align with the followups doc's exact condition: also accept when any of
owner's class-typed-property return types shares hierarchy (in either
direction, via element path) with a non-trivial tree class. PersonB
shares hierarchy with Person (subtype-of) and is therefore considered
reachable — precondition stays inert. Gap 4's genuine unreachable case
(SrcRoot has zero class-typed properties at all) still raises because the
ownerClassTypedReturnTypes set is empty.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants