Summary
Source resolution (resolveContentSources →
packages/content/src/content/ResolveContentSources.ts) resolves the concrete entity endpoints
of each relationship-path step from the data, but leaves each step's relationshipName as the
declared (possibly base/abstract) relationship class. As a result, ResolvedPath steps can
carry non-concrete relationship classes, unlike their concrete sourceClassName/targetClassName.
Current behavior
resolveDeclarationPaths runs a data-driven query and maps each result row into a ResolvedPath.
It overwrites only the entity endpoints with the concrete classes found in the data, and spreads
the rest of the declared step verbatim:
path: declaration.path.map((step, i) => ({
...step, // relationshipName carried over as declared
sourceClassName: (i === 0 ? target.primaryClass : row[i]), // concrete (from data)
targetClassName: row[i + 1], // concrete (from data)
})),
The resolution queries (originalStrategy, rewriteStrategy, crossJoinStrategy) only project the
concrete class names of the step targets (buildClassNameColumns / buildClassIdColumns over
[s{i}]), never the relationship ([r{i}]). So the concrete relationship class is never read from
the data and never written back onto the step.
Expected behavior
ResolvedPath steps should carry concrete relationship classes discovered in the data, matching
the guarantee already provided for sourceClassName/targetClassName — i.e. "All path classes are
concrete — no base classes" (see ResolvedPath in
packages/content/src/content/ContentTarget.ts). For a polymorphic relationship, this means the
concrete relationship subclass(es) actually present in the scanned data.
Impact
Any consumer that relies on ResolvedPath steps being fully concrete is affected. Concretely, it
breaks the PropertyField.valueClassNames "concrete value-supplier classes" invariant for
relationship-class property fields (see the interim mitigation in createFieldsForStep,
packages/content/src/content/descriptor-building/RelatedFields.ts), which in turn makes
TransformableDescriptor.forkField carving by concrete relationship subclass impossible.
Proposed fix
Resolve the concrete relationship class per step alongside the entity endpoints:
- In each resolution strategy query, also project the concrete relationship class per step, e.g.
ec_classname([r{i}].[ECClassId], 's.c'), and group on [r{i}].[ECClassId] (mirroring how the
target class name/id columns are built today).
- Extend the result-row layout and the
resolveDeclarationPaths map(...) to overwrite each
step's relationshipName with the concrete value from the row, exactly like sourceClassName /
targetClassName.
Affected files
packages/content/src/content/ResolveContentSources.ts (query projection + row mapping)
- Tests under
packages/content/src/test/ covering polymorphic relationship-class resolution
- Optionally tighten the
ResolvedPath doc in packages/content/src/content/ContentTarget.ts
Acceptance criteria
- A resolved path over a polymorphic relationship reports the concrete relationship class(es) found
in the data, not the declared base.
- Existing
sourceClassName / targetClassName resolution behavior is unchanged.
- No regression in the resolution strategy race/short-circuit behavior.
Summary
Source resolution (
resolveContentSources→packages/content/src/content/ResolveContentSources.ts) resolves the concrete entity endpointsof each relationship-path step from the data, but leaves each step's
relationshipNameas thedeclared (possibly base/abstract) relationship class. As a result,
ResolvedPathsteps cancarry non-concrete relationship classes, unlike their concrete
sourceClassName/targetClassName.Current behavior
resolveDeclarationPathsruns a data-driven query and maps each result row into aResolvedPath.It overwrites only the entity endpoints with the concrete classes found in the data, and spreads
the rest of the declared step verbatim:
The resolution queries (
originalStrategy,rewriteStrategy,crossJoinStrategy) only project theconcrete class names of the step targets (
buildClassNameColumns/buildClassIdColumnsover[s{i}]), never the relationship ([r{i}]). So the concrete relationship class is never read fromthe data and never written back onto the step.
Expected behavior
ResolvedPathsteps should carry concrete relationship classes discovered in the data, matchingthe guarantee already provided for
sourceClassName/targetClassName— i.e. "All path classes areconcrete — no base classes" (see
ResolvedPathinpackages/content/src/content/ContentTarget.ts). For a polymorphic relationship, this means theconcrete relationship subclass(es) actually present in the scanned data.
Impact
Any consumer that relies on
ResolvedPathsteps being fully concrete is affected. Concretely, itbreaks the
PropertyField.valueClassNames"concrete value-supplier classes" invariant forrelationship-class property fields (see the interim mitigation in
createFieldsForStep,packages/content/src/content/descriptor-building/RelatedFields.ts), which in turn makesTransformableDescriptor.forkFieldcarving by concrete relationship subclass impossible.Proposed fix
Resolve the concrete relationship class per step alongside the entity endpoints:
ec_classname([r{i}].[ECClassId], 's.c'), and group on[r{i}].[ECClassId](mirroring how thetarget class name/id columns are built today).
resolveDeclarationPathsmap(...)to overwrite eachstep's
relationshipNamewith the concrete value from the row, exactly likesourceClassName/targetClassName.Affected files
packages/content/src/content/ResolveContentSources.ts(query projection + row mapping)packages/content/src/test/covering polymorphic relationship-class resolutionResolvedPathdoc inpackages/content/src/content/ContentTarget.tsAcceptance criteria
in the data, not the declared base.
sourceClassName/targetClassNameresolution behavior is unchanged.