Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions TM1py/Services/CellService.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,17 @@ def clear_with_dataframe(self, cube: str, df: "pd.DataFrame", dimension_mapping:
).filter_by_level(0)

elif isinstance(hierarchies, Iterable):
# Build a union across all hierarchies so every hierarchy
# contributes to the MDX crossjoin instead of the last one
# overwriting all previous entries (issue #1068).
combined: Optional["MdxHierarchySet"] = None
for hierarchy in hierarchies:
mdx_selections[dimension] = MdxHierarchySet.tm1_subset_all(
hset = MdxHierarchySet.tm1_subset_all(
dimension=dimension, hierarchy=hierarchy
).filter_by_level(0)
combined = hset if combined is None else combined.union(hset)
if combined is not None:
mdx_selections[dimension] = combined

else:
raise ValueError(f"Unexpected value type for key '{dimension}' in dimension_mapping")
Expand Down Expand Up @@ -3123,19 +3130,20 @@ def execute_view_dataframe_pivot(

@require_pandas
def execute_mdx_dataframe_pivot(
self, mdx: str, dropna: bool = False, fill_value: bool = None, sandbox_name: str = None
self, mdx: str, dropna: bool = False, fill_value: bool = None, sandbox_name: str = None, **kwargs
) -> "pd.DataFrame":
"""Execute MDX Query to get a pandas pivot data frame in the shape as specified in the Query

:param mdx:
:param dropna:
:param fill_value:
:param sandbox_name: str
:param kwargs: Additional keyword arguments forwarded to extract_cellset_dataframe_pivot
:return:
"""
cellset_id = self.create_cellset(mdx=mdx, sandbox_name=sandbox_name)
return self.extract_cellset_dataframe_pivot(
cellset_id=cellset_id, dropna=dropna, fill_value=fill_value, sandbox_name=sandbox_name
cellset_id=cellset_id, dropna=dropna, fill_value=fill_value, sandbox_name=sandbox_name, **kwargs
)

def execute_mdx_cellcount(self, mdx: str, sandbox_name: str = None, **kwargs) -> int:
Expand Down
142 changes: 142 additions & 0 deletions Tests/test_cellservice_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Unit tests for CellService logic that can run without a live TM1 server.

Covers:
- clear_with_dataframe: multiple hierarchies in dimension_mapping (issue #1068)
- execute_mdx_dataframe_pivot: **kwargs forwarding (issue #1113)
"""

import unittest
from unittest.mock import MagicMock, patch, call

import pandas as pd

from TM1py.Services.CellService import CellService


def _make_cell_service():
"""Return a CellService with the REST connection mocked out."""
rest = MagicMock()
rest.is_data_admin = True
rest.is_ops_admin = True
rest.is_admin = True
rest.version = "11.8.000"
svc = CellService.__new__(CellService)
svc._rest = rest
svc._tm1_rest = rest
return svc


class TestClearWithDataframeMultipleHierarchies(unittest.TestCase):
"""Regression test for #1068.

When dimension_mapping maps a single dimension name to a list of hierarchies,
the generated MDX must include *all* hierarchies, not just the last one.
"""

def _run(self, dimension_mapping, cube="MyCube", dim_names=None):
"""Run clear_with_dataframe with mocked dependencies and return the MDX string."""
svc = _make_cell_service()

if dim_names is None:
dim_names = list(dimension_mapping.keys())

# Mock cube_service.get_dimension_names to return the cube's dimensions
mock_cube_svc = MagicMock()
mock_cube_svc.get_dimension_names.return_value = dim_names
svc.get_cube_service = MagicMock(return_value=mock_cube_svc)

# Capture the MDX passed to clear_with_mdx instead of running it
captured = {}
svc.clear_with_mdx = lambda cube, mdx, **kw: captured.update({"mdx": mdx})

df = pd.DataFrame({d: ["elem1"] for d in dim_names if d not in dimension_mapping})

svc.clear_with_dataframe(cube=cube, df=df, dimension_mapping=dimension_mapping)
return captured.get("mdx", "")

def test_single_hierarchy_still_works(self):
"""Sanity-check: a str mapping produces valid MDX."""
mdx = self._run(
dim_names=["Year", "Month", "MyDim"],
dimension_mapping={"MyDim": "MyHierarchy"},
)
self.assertIn("myhierarchy", mdx.lower())

def test_two_hierarchies_both_appear(self):
"""Both hierarchies must appear in the MDX (not just the last one)."""
mdx = self._run(
dim_names=["Year", "Month", "MyDim"],
dimension_mapping={"MyDim": ["Hierarchy1", "Hierarchy2"]},
)
mdx_lower = mdx.lower()
self.assertIn("hierarchy1", mdx_lower, "First hierarchy missing from MDX")
self.assertIn("hierarchy2", mdx_lower, "Second hierarchy missing from MDX")
# Both should appear in a UNION, not just the last one overwriting
self.assertIn("union", mdx_lower, "Expected UNION of hierarchies in MDX")

def test_three_hierarchies_all_appear(self):
"""All three hierarchies must appear."""
mdx = self._run(
dim_names=["Year", "Month", "MyDim"],
dimension_mapping={"MyDim": ["H1", "H2", "H3"]},
)
mdx_lower = mdx.lower()
for h in ("h1", "h2", "h3"):
self.assertIn(h, mdx_lower, f"{h} missing from MDX")


class TestExecuteMdxDataframePivotKwargs(unittest.TestCase):
"""Regression test for #1113.

execute_mdx_dataframe_pivot must forward **kwargs to extract_cellset_dataframe_pivot.
"""

def test_kwargs_forwarded(self):
svc = _make_cell_service()

svc.create_cellset = MagicMock(return_value="cellset-id-123")
svc.extract_cellset_dataframe_pivot = MagicMock(return_value=pd.DataFrame())

svc.execute_mdx_dataframe_pivot(
mdx="SELECT ... FROM [Cube]",
dropna=True,
fill_value=0,
sandbox_name="MySandbox",
use_compact_json=True,
)

svc.extract_cellset_dataframe_pivot.assert_called_once_with(
cellset_id="cellset-id-123",
dropna=True,
fill_value=0,
sandbox_name="MySandbox",
use_compact_json=True,
)

def test_extra_kwargs_forwarded(self):
"""Arbitrary extra kwargs like cell_properties are forwarded."""
svc = _make_cell_service()
svc.create_cellset = MagicMock(return_value="cid")
svc.extract_cellset_dataframe_pivot = MagicMock(return_value=pd.DataFrame())

svc.execute_mdx_dataframe_pivot(
mdx="SELECT FROM [Cube]",
cell_properties=["FormattedValue"],
)

_, kwargs = svc.extract_cellset_dataframe_pivot.call_args
self.assertIn("cell_properties", kwargs)
self.assertEqual(kwargs["cell_properties"], ["FormattedValue"])

def test_no_kwargs_still_works(self):
"""Calling without kwargs must not raise."""
svc = _make_cell_service()
svc.create_cellset = MagicMock(return_value="cid")
svc.extract_cellset_dataframe_pivot = MagicMock(return_value=pd.DataFrame())

result = svc.execute_mdx_dataframe_pivot(mdx="SELECT FROM [Cube]")
self.assertIsInstance(result, pd.DataFrame)


if __name__ == "__main__":
unittest.main()