From 6280a4857e929e1ba004dfe299c9a6e20bf79d41 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 25 Feb 2025 14:18:19 +0200 Subject: [PATCH 1/3] added support for calc tables --- src/sempy_labs/_model_bpa.py | 5 +++++ src/sempy_labs/_model_bpa_rules.py | 4 ++-- src/sempy_labs/tom/_model.py | 34 +++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/sempy_labs/_model_bpa.py b/src/sempy_labs/_model_bpa.py index ac94f719..184848bb 100644 --- a/src/sempy_labs/_model_bpa.py +++ b/src/sempy_labs/_model_bpa.py @@ -280,6 +280,7 @@ def translate_using_spark(rule_file): lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name), ), "Table": (tom.model.Tables, lambda obj: obj.Name), + "Calculated Table": (tom.all_calculated_tables(), lambda obj: obj.Name), "Role": (tom.model.Roles, lambda obj: obj.Name), "Model": (tom.model, lambda obj: obj.Model.Name), "Calculation Item": ( @@ -322,6 +323,10 @@ def translate_using_spark(rule_file): x = [nm(obj) for obj in tom.all_hierarchies() if expr(obj, tom)] elif scope == "Table": x = [nm(obj) for obj in tom.model.Tables if expr(obj, tom)] + elif scope == "Calculated Table": + x = [ + nm(obj) for obj in tom.all_calculated_tables() if expr(obj, tom) + ] elif scope == "Relationship": x = [nm(obj) for obj in tom.model.Relationships if expr(obj, tom)] elif scope == "Role": diff --git a/src/sempy_labs/_model_bpa_rules.py b/src/sempy_labs/_model_bpa_rules.py index f5295fdb..05dd4e6b 100644 --- a/src/sempy_labs/_model_bpa_rules.py +++ b/src/sempy_labs/_model_bpa_rules.py @@ -565,7 +565,7 @@ def model_bpa_rules( ), ( "DAX Expressions", - "Measure", + ["Measure", "Calculated Table"], "Error", "Column references should be fully qualified", lambda obj, tom: any( @@ -576,7 +576,7 @@ def model_bpa_rules( ), ( "DAX Expressions", - "Measure", + ["Measure", "Calculated Table"], "Error", "Measure references should be unqualified", lambda obj, tom: any( diff --git a/src/sempy_labs/tom/_model.py b/src/sempy_labs/tom/_model.py index 2479e644..1d9728db 100644 --- a/src/sempy_labs/tom/_model.py +++ b/src/sempy_labs/tom/_model.py @@ -3281,9 +3281,14 @@ def depends_on(self, object, dependencies: pd.DataFrame): if objType == TOM.ObjectType.Table: objParentName = objName + object_types = ["Table", "Calc Table"] + elif objType == TOM.ObjectType.Column: + object_types = ["Column", "Calc Column"] + else: + object_types = [str(objType)] fil = dependencies[ - (dependencies["Object Type"] == str(objType)) + (dependencies["Object Type"].isin(object_types)) & (dependencies["Table Name"] == objParentName) & (dependencies["Object Name"] == objName) ] @@ -3389,11 +3394,20 @@ def fully_qualified_measures( dependencies["Object Name"] == dependencies["Parent Node"] ] + if object.ObjectType == TOM.ObjectType.Measure: + expr = object.Expression + elif object.ObjectType == TOM.ObjectType.Table: + part = next(p for p in object.Partitions) + if part.SourceType != TOM.PartitionSourceType.Calculated: + return + expr = part.Source.Expression + else: + return + for obj in self.depends_on(object=object, dependencies=dependencies): if obj.ObjectType == TOM.ObjectType.Measure: - if (f"{obj.Parent.Name}[{obj.Name}]" in object.Expression) or ( - format_dax_object_name(obj.Parent.Name, obj.Name) - in object.Expression + if (f"{obj.Parent.Name}[{obj.Name}]" in expr) or ( + format_dax_object_name(obj.Parent.Name, obj.Name) in expr ): yield obj @@ -3427,6 +3441,16 @@ def create_pattern(tableList, b): combined_pattern = "".join(patterns) + re.escape(f"[{b}]") return combined_pattern + if object.ObjectType == TOM.ObjectType.Measure: + expr = object.Expression + elif object.ObjectType == TOM.ObjectType.Table: + part = next(p for p in object.Partitions) + if part.SourceType != TOM.PartitionSourceType.Calculated: + return + expr = part.Source.Expression + else: + return + for obj in self.depends_on(object=object, dependencies=dependencies): if obj.ObjectType == TOM.ObjectType.Column: tableList = [] @@ -3436,7 +3460,7 @@ def create_pattern(tableList, b): if ( re.search( create_pattern(tableList, re.escape(obj.Name)), - object.Expression, + expr, ) is not None ): From ba45d982358c745d1385a95c1b37851874a3da10 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Feb 2025 13:47:48 +0200 Subject: [PATCH 2/3] added logic for calculated column --- src/sempy_labs/_model_bpa.py | 10 ++++++ src/sempy_labs/tom/_model.py | 59 ++++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/sempy_labs/_model_bpa.py b/src/sempy_labs/_model_bpa.py index 184848bb..777d7e55 100644 --- a/src/sempy_labs/_model_bpa.py +++ b/src/sempy_labs/_model_bpa.py @@ -274,6 +274,10 @@ def translate_using_spark(rule_file): tom.all_columns(), lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name), ), + "Calculated Column": ( + tom.all_calculated_columns(), + lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name), + ), "Measure": (tom.all_measures(), lambda obj: obj.Name), "Hierarchy": ( tom.all_hierarchies(), @@ -337,6 +341,12 @@ def translate_using_spark(rule_file): x = [ nm(obj) for obj in tom.all_calculation_items() if expr(obj, tom) ] + elif scope == "Calculated Column": + x = [ + nm(obj) + for obj in tom.all_calculated_columns() + if expr(obj, tom) + ] if len(x) > 0: new_data = { diff --git a/src/sempy_labs/tom/_model.py b/src/sempy_labs/tom/_model.py index 1d9728db..f6c449cc 100644 --- a/src/sempy_labs/tom/_model.py +++ b/src/sempy_labs/tom/_model.py @@ -3370,6 +3370,41 @@ def referenced_by(self, object, dependencies: pd.DataFrame): if t.Name in tbls: yield t + def _get_expression(self, object): + """ + Helper function to get the expression for any given TOM object. + """ + + import Microsoft.AnalysisServices.Tabular as TOM + + valid_objects = [ + TOM.ObjectType.Measure, + TOM.OBjectType.Table, + TOM.ObjectType.Column, + TOM.ObjectType.CalculationItem, + ] + + if object.ObjectType not in valid_objects: + raise ValueError( + f"{icons.red_dot} The 'object' parameter must be one of these types: {valid_objects}." + ) + + if object.ObjectType == TOM.ObjectType.Measure: + expr = object.Expression + elif object.ObjectType == TOM.ObjectType.Table: + part = next(p for p in object.Partitions) + if part.SourceType == TOM.PartitionSourceType.Calculated: + expr = part.Source.Expression + elif object.ObjectType == TOM.ObjectType.Column: + if object.Type == TOM.ColumnType.Calculated: + expr = object.Expression + elif object.ObjectType == TOM.ObjectType.CalculationItem: + expr = object.Expression + else: + return + + return expr + def fully_qualified_measures( self, object: "TOM.Measure", dependencies: pd.DataFrame ): @@ -3394,15 +3429,7 @@ def fully_qualified_measures( dependencies["Object Name"] == dependencies["Parent Node"] ] - if object.ObjectType == TOM.ObjectType.Measure: - expr = object.Expression - elif object.ObjectType == TOM.ObjectType.Table: - part = next(p for p in object.Partitions) - if part.SourceType != TOM.PartitionSourceType.Calculated: - return - expr = part.Source.Expression - else: - return + expr = self._get_expression(object=object) for obj in self.depends_on(object=object, dependencies=dependencies): if obj.ObjectType == TOM.ObjectType.Measure: @@ -3411,7 +3438,7 @@ def fully_qualified_measures( ): yield obj - def unqualified_columns(self, object: "TOM.Column", dependencies: pd.DataFrame): + def unqualified_columns(self, object, dependencies: pd.DataFrame): """ Obtains all unqualified column references for a given object. @@ -3433,6 +3460,8 @@ def unqualified_columns(self, object: "TOM.Column", dependencies: pd.DataFrame): dependencies["Object Name"] == dependencies["Parent Node"] ] + expr = self._get_expression(object=object) + def create_pattern(tableList, b): patterns = [ r"(? Date: Wed, 26 Feb 2025 14:11:04 +0200 Subject: [PATCH 3/3] expanded scope of bpa rules to calc columns, calc tables --- src/sempy_labs/_model_bpa_rules.py | 14 ++++++++++++-- src/sempy_labs/tom/_model.py | 26 ++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/sempy_labs/_model_bpa_rules.py b/src/sempy_labs/_model_bpa_rules.py index 05dd4e6b..f524263b 100644 --- a/src/sempy_labs/_model_bpa_rules.py +++ b/src/sempy_labs/_model_bpa_rules.py @@ -565,7 +565,12 @@ def model_bpa_rules( ), ( "DAX Expressions", - ["Measure", "Calculated Table"], + [ + "Measure", + "Calculated Table", + "Calculated Column", + "Calculation Item", + ], "Error", "Column references should be fully qualified", lambda obj, tom: any( @@ -576,7 +581,12 @@ def model_bpa_rules( ), ( "DAX Expressions", - ["Measure", "Calculated Table"], + [ + "Measure", + "Calculated Table", + "Calculated Column", + "Calculation Item", + ], "Error", "Measure references should be unqualified", lambda obj, tom: any( diff --git a/src/sempy_labs/tom/_model.py b/src/sempy_labs/tom/_model.py index f6c449cc..a2bf6287 100644 --- a/src/sempy_labs/tom/_model.py +++ b/src/sempy_labs/tom/_model.py @@ -3275,22 +3275,28 @@ def depends_on(self, object, dependencies: pd.DataFrame): """ import Microsoft.AnalysisServices.Tabular as TOM - objType = object.ObjectType - objName = object.Name - objParentName = object.Parent.Name + obj_type = object.ObjectType + obj_name = object.Name - if objType == TOM.ObjectType.Table: - objParentName = objName + if object.ObjectType == TOM.ObjectType.CalculationItem: + obj_parent_name = object.Parent.Table.Name + else: + obj_parent_name = object.Parent.Name + + if obj_type == TOM.ObjectType.Table: + obj_parent_name = obj_name object_types = ["Table", "Calc Table"] - elif objType == TOM.ObjectType.Column: + elif obj_type == TOM.ObjectType.Column: object_types = ["Column", "Calc Column"] + elif obj_type == TOM.ObjectType.CalculationItem: + object_types = ["Calculation Item"] else: - object_types = [str(objType)] + object_types = [str(obj_type)] fil = dependencies[ (dependencies["Object Type"].isin(object_types)) - & (dependencies["Table Name"] == objParentName) - & (dependencies["Object Name"] == objName) + & (dependencies["Table Name"] == obj_parent_name) + & (dependencies["Object Name"] == obj_name) ] meas = ( fil[fil["Referenced Object Type"] == "Measure"]["Referenced Object"] @@ -3379,7 +3385,7 @@ def _get_expression(self, object): valid_objects = [ TOM.ObjectType.Measure, - TOM.OBjectType.Table, + TOM.ObjectType.Table, TOM.ObjectType.Column, TOM.ObjectType.CalculationItem, ]