@@ -71,38 +71,50 @@ def parse(self, source: str | Path) -> SemanticGraph:
7171 source_path = Path (source )
7272 pending_views : list [dict ] = []
7373 pending_hierarchies : dict [str , list [dict ]] = {}
74+ pending_extends : dict [str , str ] = {} # child_name -> parent_name
7475
7576 if source_path .is_dir ():
7677 # Parse all YAML files in directory
7778 for yaml_file in source_path .rglob ("*.yml" ):
78- self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies )
79+ self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies , pending_extends )
7980 for yaml_file in source_path .rglob ("*.yaml" ):
80- self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies )
81+ self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies , pending_extends )
8182 else :
8283 # Parse single file
83- self ._parse_file (source_path , graph , pending_views , pending_hierarchies )
84+ self ._parse_file (source_path , graph , pending_views , pending_hierarchies , pending_extends )
8485
8586 # Resolve extends (inheritance) after all cubes are parsed
8687 from sidemantic .core .inheritance import resolve_model_inheritance
8788
8889 if any (m .extends for m in graph .models .values ()):
8990 graph .models = resolve_model_inheritance (graph .models )
9091
91- # Apply hierarchies after inheritance so inherited dimensions are available
92- for model_name , hierarchy_defs in pending_hierarchies .items ():
93- model = graph .models .get (model_name )
94- if not model :
95- continue
96- for h_def in hierarchy_defs :
92+ # Apply hierarchies after inheritance so inherited dimensions are available.
93+ # Also propagate parent hierarchies to child cubes via extends_map.
94+ def _apply_hierarchies (model : Model , h_defs : list [dict ]) -> None :
95+ for h_def in h_defs :
9796 levels = h_def .get ("levels" , [])
9897 for i in range (1 , len (levels )):
9998 child_name = levels [i ]
10099 parent_name = levels [i - 1 ]
101100 if "." not in parent_name and "." not in child_name :
102101 child_dim = model .get_dimension (child_name )
103- if child_dim :
102+ if child_dim and not child_dim . parent :
104103 child_dim .parent = parent_name
105104
105+ # Apply explicit hierarchies
106+ for model_name , hierarchy_defs in pending_hierarchies .items ():
107+ model = graph .models .get (model_name )
108+ if model :
109+ _apply_hierarchies (model , hierarchy_defs )
110+
111+ # Propagate parent hierarchies to child cubes that inherited dimensions
112+ for child_name , parent_name in pending_extends .items ():
113+ if parent_name in pending_hierarchies :
114+ child_model = graph .models .get (child_name )
115+ if child_model :
116+ _apply_hierarchies (child_model , pending_hierarchies [parent_name ])
117+
106118 # Parse views after all cubes are loaded and inheritance resolved
107119 for view_def in pending_views :
108120 model = self ._parse_view (view_def , graph )
@@ -117,6 +129,7 @@ def _parse_file(
117129 graph : SemanticGraph ,
118130 pending_views : list [dict ],
119131 pending_hierarchies : dict [str , list [dict ]],
132+ pending_extends : dict [str , str ],
120133 ) -> None :
121134 """Parse a single Cube YAML file.
122135
@@ -125,6 +138,7 @@ def _parse_file(
125138 graph: Semantic graph to add models to
126139 pending_views: List to accumulate view definitions for deferred parsing
127140 pending_hierarchies: Dict to accumulate hierarchy definitions per cube for deferred application
141+ pending_extends: Dict to track extends relationships (child -> parent)
128142 """
129143 with open (file_path ) as f :
130144 data = yaml .safe_load (f )
@@ -143,6 +157,10 @@ def _parse_file(
143157 h_defs = cube_def .get ("hierarchies" )
144158 if h_defs :
145159 pending_hierarchies [model .name ] = h_defs
160+ # Track extends for hierarchy propagation
161+ ext = cube_def .get ("extends" )
162+ if ext :
163+ pending_extends [model .name ] = ext
146164
147165 # Collect views for deferred parsing (need all cubes loaded first)
148166 for view_def in data .get ("views" ) or []:
@@ -670,6 +688,9 @@ def _parse_view(self, view_def: dict, graph: SemanticGraph) -> Model | None:
670688 cube_alias = cube_spec .get ("alias" )
671689 prefix_str = f"{ cube_alias or target_name } _" if prefix else ""
672690
691+ # Build alias map for renaming dependent references
692+ alias_map : dict [str , str ] = {}
693+
673694 if includes == "*" :
674695 dims = [d for d in target .dimensions if d .name not in excludes ]
675696 mets = [m for m in target .metrics if m .name not in excludes ]
@@ -686,6 +707,8 @@ def _parse_view(self, view_def: dict, graph: SemanticGraph) -> Model | None:
686707 elif isinstance (inc , dict ):
687708 orig = inc .get ("name" , "" )
688709 alias = inc .get ("alias" , orig )
710+ if alias != orig :
711+ alias_map [orig ] = alias
689712 d = target .get_dimension (orig )
690713 if d :
691714 dims .append (d .model_copy (update = {"name" : alias }))
@@ -695,6 +718,19 @@ def _parse_view(self, view_def: dict, graph: SemanticGraph) -> Model | None:
695718 else :
696719 continue
697720
721+ # Apply alias map to dependent references (parent, drill_fields)
722+ if alias_map :
723+ dims = [
724+ d .model_copy (update = {"parent" : alias_map [d .parent ]}) if d .parent and d .parent in alias_map else d
725+ for d in dims
726+ ]
727+ mets = [
728+ m .model_copy (update = {"drill_fields" : [alias_map .get (f , f ) for f in m .drill_fields ]})
729+ if m .drill_fields and any (f in alias_map for f in m .drill_fields )
730+ else m
731+ for m in mets
732+ ]
733+
698734 if prefix_str :
699735 # Prefix names and update dependent references (parent, drill_fields)
700736 prefixed_dims = []
0 commit comments