Skip to content

Commit 88e1b02

Browse files
committed
Defer hierarchy application and fix time_shift export roundtrip
Apply hierarchy parent chains after inheritance resolution so child cubes that reference parent-inherited dimensions get correct parent links. Previously hierarchies were applied during _parse_cube before inheritance, so inherited dimensions were invisible. Fix time_comparison export to emit ${metric} Cube references instead of qualified cube.metric names, preserving roundtrip fidelity.
1 parent a6735e6 commit 88e1b02

1 file changed

Lines changed: 35 additions & 21 deletions

File tree

sidemantic/adapters/cube.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)