@@ -70,23 +70,39 @@ def parse(self, source: str | Path) -> SemanticGraph:
7070 graph = SemanticGraph ()
7171 source_path = Path (source )
7272 pending_views : list [dict ] = []
73+ pending_hierarchies : dict [str , list [dict ]] = {}
7374
7475 if source_path .is_dir ():
7576 # Parse all YAML files in directory
7677 for yaml_file in source_path .rglob ("*.yml" ):
77- self ._parse_file (yaml_file , graph , pending_views )
78+ self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies )
7879 for yaml_file in source_path .rglob ("*.yaml" ):
79- self ._parse_file (yaml_file , graph , pending_views )
80+ self ._parse_file (yaml_file , graph , pending_views , pending_hierarchies )
8081 else :
8182 # Parse single file
82- self ._parse_file (source_path , graph , pending_views )
83+ self ._parse_file (source_path , graph , pending_views , pending_hierarchies )
8384
8485 # Resolve extends (inheritance) after all cubes are parsed
8586 from sidemantic .core .inheritance import resolve_model_inheritance
8687
8788 if any (m .extends for m in graph .models .values ()):
8889 graph .models = resolve_model_inheritance (graph .models )
8990
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 :
97+ levels = h_def .get ("levels" , [])
98+ for i in range (1 , len (levels )):
99+ child_name = levels [i ]
100+ parent_name = levels [i - 1 ]
101+ if "." not in parent_name and "." not in child_name :
102+ child_dim = model .get_dimension (child_name )
103+ if child_dim :
104+ child_dim .parent = parent_name
105+
90106 # Parse views after all cubes are loaded and inheritance resolved
91107 for view_def in pending_views :
92108 model = self ._parse_view (view_def , graph )
@@ -95,13 +111,20 @@ def parse(self, source: str | Path) -> SemanticGraph:
95111
96112 return graph
97113
98- def _parse_file (self , file_path : Path , graph : SemanticGraph , pending_views : list [dict ]) -> None :
114+ def _parse_file (
115+ self ,
116+ file_path : Path ,
117+ graph : SemanticGraph ,
118+ pending_views : list [dict ],
119+ pending_hierarchies : dict [str , list [dict ]],
120+ ) -> None :
99121 """Parse a single Cube YAML file.
100122
101123 Args:
102124 file_path: Path to YAML file
103125 graph: Semantic graph to add models to
104126 pending_views: List to accumulate view definitions for deferred parsing
127+ pending_hierarchies: Dict to accumulate hierarchy definitions per cube for deferred application
105128 """
106129 with open (file_path ) as f :
107130 data = yaml .safe_load (f )
@@ -116,6 +139,10 @@ def _parse_file(self, file_path: Path, graph: SemanticGraph, pending_views: list
116139 model = self ._parse_cube (cube_def )
117140 if model :
118141 graph .add_model (model )
142+ # Collect hierarchies for deferred application (after inheritance)
143+ h_defs = cube_def .get ("hierarchies" )
144+ if h_defs :
145+ pending_hierarchies [model .name ] = h_defs
119146
120147 # Collect views for deferred parsing (need all cubes loaded first)
121148 for view_def in data .get ("views" ) or []:
@@ -183,18 +210,6 @@ def _parse_cube(self, cube_def: dict) -> Model | None:
183210 if dim_def .get ("primary_key" ):
184211 primary_key = dim .name
185212
186- # Parse hierarchies and set Dimension.parent chains
187- for h_def in cube_def .get ("hierarchies" ) or []:
188- levels = h_def .get ("levels" , [])
189- for i in range (1 , len (levels )):
190- child_name = levels [i ]
191- parent_name = levels [i - 1 ]
192- # Skip cross-cube level references (contain dots)
193- if "." not in parent_name and "." not in child_name :
194- child_dim = next ((d for d in dimensions if d .name == child_name ), None )
195- if child_dim :
196- child_dim .parent = parent_name
197-
198213 # Parse measures
199214 measures = []
200215 for measure_def in cube_def .get ("measures" ) or []:
@@ -847,11 +862,10 @@ def _export_cube(self, model: Model, resolved_models: dict[str, Model]) -> dict:
847862 # Time comparison - use Cube's time dimension features
848863 measure_def ["type" ] = "number"
849864 if measure .base_metric :
850- # Add comment explaining this is a time comparison
851- measure_def ["description" ] = (
852- measure .description or ""
853- ) + f" (Time comparison of { measure .base_metric } )"
854- measure_def ["sql" ] = measure .base_metric
865+ # Convert qualified name (cube.metric) to Cube reference (${metric})
866+ base_ref = measure .base_metric .split ("." )[- 1 ] if "." in measure .base_metric else measure .base_metric
867+ measure_def ["sql" ] = f"${{{ base_ref } }}"
868+ measure_def ["description" ] = (measure .description or "" ) + f" (Time comparison of { base_ref } )"
855869 else :
856870 # Regular aggregation measure
857871 type_mapping = {
0 commit comments