using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.Data; using System.Globalization; using Microsoft.AnalysisServices.Tabular; using Tom=Microsoft.AnalysisServices.Tabular; namespace BismNormalizer.TabularCompare.TabularMetadata { /// /// Abstraction of a tabular model table with properties and methods for comparison purposes. /// public class Table : TabularObject { private TabularModel _parentTabularModel; private Tom.Table _tomTable; private string _partitionsDefinition; private string _dataSourceName; private RelationshipCollection _relationships = new RelationshipCollection(); private MeasureCollection _measures = new MeasureCollection(); private bool _isCalculationGroup; private ModeType _tableModeType; private CalculationItemCollection _calculationItems = new CalculationItemCollection(); /// /// Initializes a new instance of the Table class using multiple parameters. /// /// TabularModel object that the Table object belongs to. /// Tabular Object Model Table object abtstracted by the Table class. public Table(TabularModel parentTabularModel, Tom.Table tomTable) : base(tomTable, parentTabularModel) { _parentTabularModel = parentTabularModel; _tomTable = tomTable; PopulateProperties(); } /// /// TabularModel object that the Table object belongs to. /// public TabularModel ParentTabularModel => _parentTabularModel; /// /// For tables with M/query partitions, return the partitions definition. /// public string PartitionsDefinition => _partitionsDefinition; /// /// Name of the DataSource object that the Table object belongs to. /// public string DataSourceName => _dataSourceName; /// /// Collection of relationships for the Table object. /// public RelationshipCollection Relationships => _relationships; /// /// Collection of measures for the Table object. /// public MeasureCollection Measures => _measures; /// /// True if the table is a calculation group. /// public bool IsCalculationGroup => _isCalculationGroup; public ModeType TableModeType => _tableModeType; /// /// Collection of calculation items for the Table object. /// public CalculationItemCollection CalculationItems => _calculationItems; /// /// Tabular Object Model Table object abtstracted by the Table class. /// public Tom.Table TomTable => _tomTable; private void PopulateProperties() { base.RemovePropertyFromObjectDefinition("measures"); _isCalculationGroup = (_tomTable.CalculationGroup != null); _partitionsDefinition = ""; _dataSourceName = ""; bool hasMQueryOrPolicyPartition = false; //Associate table with a DataSource if possible. It's not possible if calc table or if M expression refers to a shared expression, or multiple data sources foreach (Partition partition in _tomTable.Partitions) { _tableModeType = partition.Mode; if (partition.SourceType == PartitionSourceType.M) { hasMQueryOrPolicyPartition = true; //Check M dependency tree to see if all partitions refer only to a single DataSource CalcDependencyCollection calcDependencies = _parentTabularModel.MDependencies.DependenciesReferenceFrom(CalcDependencyObjectType.Partition, _tomTable.Name, partition.Name); if (calcDependencies.Count == 1 && calcDependencies[0].ReferencedObjectType == CalcDependencyObjectType.DataSource) { if (_dataSourceName == "") { _dataSourceName = calcDependencies[0].ReferencedObjectName; } else if (_dataSourceName != calcDependencies[0].ReferencedObjectName) { //Partition depends on a different DataSource to another partition in same table, so ensure no DataSource association for the table and stop iterating partitions. _dataSourceName = ""; break; } } else { //Partition has mutiple dependencies, or depends on an expression instead of DataSource, so ensure no DataSource association for the table and stop iterating partitions. _dataSourceName = ""; break; } } //If old partition, find the primary partition (first one) to determine DataSource. Technically it is possible for different partitions in the same table to point to different DataSources, but the Tabular Designer in VS doesn't support it. If set manually in .bim file, the UI still associates with the first partition (e.g. when processing table by itself, or deletinig the DataSource gives a warning message listing associated tables). if (partition.SourceType == PartitionSourceType.Query) { hasMQueryOrPolicyPartition = true; _dataSourceName = ((QueryPartitionSource)partition.Source).DataSource.Name; break; } //Might be a policy partition. if (partition.SourceType == PartitionSourceType.PolicyRange) { hasMQueryOrPolicyPartition = true; break; } } if (hasMQueryOrPolicyPartition || _isCalculationGroup) { _partitionsDefinition = base.RetrievePropertyFromObjectDefinition("partitions"); //Option to hide partitions only applies to M, query and policy partitions (calculated tables hold dax defintitions in their partitions) if (!_parentTabularModel.ComparisonInfo.OptionsInfo.OptionPartitions) { base.RemovePropertyFromObjectDefinition("partitions"); } } //Find table relationships foreach (Tom.Relationship relationship in _tomTable.Model.Relationships) { if (relationship.FromTable.Name == _tomTable.Name && relationship.Type == RelationshipType.SingleColumn) //currently only support single column { _relationships.Add(new Relationship(this, (SingleColumnRelationship)relationship)); } } //Find measures foreach (Tom.Measure measure in _tomTable.Measures) { _measures.Add(new Measure(this, measure, measure.KPI != null)); } //Find calc items if (_isCalculationGroup) { foreach (Tom.CalculationItem calcItem in _tomTable.CalculationGroup.CalculationItems) { _calculationItems.Add(new CalculationItem(this, calcItem)); } } } #region Relationship collection methods /// /// Delete all associated relationships including those from other tables that refer to this table. /// /// Collection of all associated relationships that were deleted. Useful if updating tables as then need to add back. public List DeleteAllAssociatedRelationships() { List relationshipsToDelete = new List(); foreach (Table table in _parentTabularModel.Tables) { List relationshipsToDeleteInternalNames = new List(); foreach (Relationship relationship in table.Relationships) { if (relationship.FromTableName == this.Name || relationship.ToTableName == this.Name) { SingleColumnRelationship relationshipTarget = new SingleColumnRelationship(); relationship.TomRelationship.CopyTo(relationshipTarget); relationshipsToDelete.Add(relationshipTarget); relationshipsToDeleteInternalNames.Add(relationship.InternalName); } } foreach (string relationshipToDeleteInternalName in relationshipsToDeleteInternalNames) { table.DeleteRelationship(relationshipToDeleteInternalName); } } return relationshipsToDelete; } /// /// Find all direct relationships that filter this table. This is all ACTIVE relationships where 1) this is FROM table, or 2) this is TO table with CrossFilteringBehavior=BothDirections /// /// All the associated Relationships. public List FindFilteredRelationships(bool checkSecurityBehavior = false) { //T1[C1]->T2[C2] //FromTableName: T1 *** this.Name //ToTableName: T2 //Considers DIRECT relationships for this table ONLY (1 level). List filteringRelationships = new List(); foreach (Table table in _parentTabularModel.Tables) { foreach (Relationship relationship in table.Relationships) { if (relationship.TomRelationship.IsActive && (relationship.FromTableName == this.Name || //Alex Whittles fix 9/20 removed !checkSecurityBehavior || //(relationship.ToTableName == this.Name && relationship.TomRelationship.CrossFilteringBehavior == CrossFilteringBehavior.BothDirections && (!checkSecurityBehavior || (checkSecurityBehavior && relationship.TomRelationship.SecurityFilteringBehavior == SecurityFilteringBehavior.BothDirections))) (relationship.ToTableName == this.Name && relationship.TomRelationship.CrossFilteringBehavior == CrossFilteringBehavior.BothDirections && (checkSecurityBehavior && relationship.TomRelationship.SecurityFilteringBehavior == SecurityFilteringBehavior.BothDirections)) ) ) { filteringRelationships.Add(relationship); } } } return filteringRelationships; } /// /// Find all direct relationships that filter this table. This is all ACTIVE relationships where 1) this is FROM table, or 2) this is TO table with CrossFilteringBehavior=BothDirections /// /// All the associated Relationships. public List FindFilteringRelationships(bool checkSecurityBehavior = false) { //T1[C1]->T2[C2] //FromTableName: T1 //ToTableName: T2 *** this.Name //Considers DIRECT relationships for this table ONLY (1 level). List filteringRelationships = new List(); foreach (Table table in _parentTabularModel.Tables) { foreach (Relationship relationship in table.Relationships) { if (relationship.TomRelationship.IsActive && (relationship.ToTableName == this.Name || (relationship.FromTableName == this.Name && relationship.TomRelationship.CrossFilteringBehavior == CrossFilteringBehavior.BothDirections && (!checkSecurityBehavior || (checkSecurityBehavior && relationship.TomRelationship.SecurityFilteringBehavior == SecurityFilteringBehavior.BothDirections))) ) ) { filteringRelationships.Add(relationship); } } } return filteringRelationships; } #endregion #region Update Actions // Relationships /// /// Delete relationship associated with the Table object. /// /// Internal name of the relationship to be deleted. public void DeleteRelationship(string internalName) { if (_tomTable.Model.Relationships.Contains(internalName)) { _tomTable.Model.Relationships.Remove(internalName); } // shell model if (_relationships.ContainsInternalName(internalName)) { _relationships.RemoveByInternalName(internalName); } } /// /// Update relationship associated with the Table object. /// /// Relationship object from the source tabular model. /// Tabular Object Model Table representing "to table" in the relationship. /// Name of the relationship to be updated. /// Warning message to return to caller. /// Boolean indicating if update was successful. public bool UpdateRelationship(Relationship relationshipSource, Tom.Table toTomTableSource, string relationshipName, ref string warningMessage) { SingleColumnRelationship tabularRelationshipSource = relationshipSource.TomRelationship; // Check if "to" table exists (don't need to check "from" table as we are in the "from" table) ... if (!_parentTabularModel.Tables.ContainsName(toTomTableSource.Name)) { warningMessage = $"Unable to update Relationship {relationshipName} because (considering changes) parent table not found in target model."; return false; } // does the required child column exist? In this case need to check child column as user might have skipped Update of table meaning columns are out of sync. if (!_tomTable.Columns.ContainsName(tabularRelationshipSource.FromColumn.Name)) { warningMessage = $"Unable to update Relationship {relationshipName} because (considering changes) child column not found in target model."; return false; } // does the required "to" column exist? Tom.Table toTableTarget = _parentTabularModel.Tables.FindByName(tabularRelationshipSource.ToTable.Name).TomTable; if ( (toTableTarget == null) || (!toTableTarget.Columns.ContainsName(tabularRelationshipSource.ToColumn.Name)) ) { warningMessage = $"Unable to update Relationship {relationshipName} because (considering changes) parent column not found in target model."; return false; } // at this point we know we will update the relationship SingleColumnRelationship relationshipTarget = new SingleColumnRelationship(); tabularRelationshipSource.CopyTo(relationshipTarget); //decouple from original table to the current one relationshipTarget.FromColumn = this.TomTable.Columns.Find(relationshipTarget.FromColumn.Name); relationshipTarget.ToColumn = toTableTarget.Columns.Find(relationshipTarget.ToColumn.Name); // Delete the target relationship with same tables/columns if still there. Not using RemoveByInternalName in case internal name is actually different. if (this.Relationships.ContainsName(relationshipSource.Name)) { this.DeleteRelationship(this.Relationships.FindByName(relationshipSource.Name).InternalName); } CreateRelationship(relationshipTarget); return true; } /// /// Create a relationship for the Table object, with validation to ensure referential integrity. /// /// Relationship object from the source tabular model. /// Tabular Object Model Table representing "to table" in the relationship. /// Name of the relationship to be created. /// Warning message to return to caller. /// Boolean indicating if creation was successful. public bool CreateRelationshipWithValidation(Relationship relationshipSource, Tom.Table toTomTableSource, string relationshipName, ref string warningMessage) { SingleColumnRelationship tabularRelationshipSource = relationshipSource.TomRelationship; // Check if "to" table exists (don't need to check "from" table as we are in the "from" table) ... if (!_parentTabularModel.Tables.ContainsName(toTomTableSource.Name)) { warningMessage = $"Unable to create Relationship {relationshipName} because (considering changes) parent table not found in target model."; return false; } // does the required child column exist? In this case need to check child column as user might have skipped Update of table meaning columns are out of sync. if (!_tomTable.Columns.ContainsName(tabularRelationshipSource.FromColumn.Name)) { warningMessage = $"Unable to create Relationship {relationshipName} because (considering changes) child column not found in target model."; return false; } // does the required "to" column exist? Tom.Table toTableTarget = _parentTabularModel.Tables.FindByName(tabularRelationshipSource.ToTable.Name).TomTable; if ( (toTableTarget == null) || (!toTableTarget.Columns.ContainsName(tabularRelationshipSource.ToColumn.Name)) ) { warningMessage = $"Unable to create Relationship {relationshipName} because (considering changes) parent column not found in target model."; return false; } if (this.IsCalculationGroup || _parentTabularModel.Tables.FindByName(tabularRelationshipSource.ToTable.Name).IsCalculationGroup) { warningMessage = $"Unable to create Relationship {relationshipName} because one or more tables is a calculation group."; return false; } // Delete the target relationship with same tables/columns if still there. Not using RemoveByInternalName in case internal name is actually different. if (this.Relationships.ContainsName(relationshipSource.Name)) { warningMessage = $"Unable to create Relationship {relationshipName} because (considering changes) relationship already exists in target model."; return false; } // at this point we know we will update the relationship SingleColumnRelationship relationshipTarget = new SingleColumnRelationship(); tabularRelationshipSource.CopyTo(relationshipTarget); //decouple from original table to the current one relationshipTarget.FromColumn = this.TomTable.Columns.Find(relationshipTarget.FromColumn.Name); relationshipTarget.ToColumn = toTableTarget.Columns.Find(relationshipTarget.ToColumn.Name); CreateRelationship(relationshipTarget); return true; } /// /// Create a relationship for the Table object. /// /// Tabular Object Model SingleColumnRelationship object to be abstracted by the Relationship object being created. public void CreateRelationship(SingleColumnRelationship tomRelationshipTarget) { bool modifiedInternalName = false; string oldInternalName = ""; // check if there is an existing relationship with same internal name if (_parentTabularModel.ContainsRelationshipByInternalName(tomRelationshipTarget.Name)) { modifiedInternalName = true; oldInternalName = tomRelationshipTarget.Name; tomRelationshipTarget.Name = Convert.ToString(Guid.NewGuid()); } _parentTabularModel.TomDatabase.Model.Relationships.Add(tomRelationshipTarget); _relationships.Add(new Relationship(this, tomRelationshipTarget, copiedFromSource: true, modifiedInternalName: modifiedInternalName, oldInternalName: oldInternalName)); } // Measures /// /// Delete measure associated with the Table object. /// /// Name of the measure to be deleted. public void DeleteMeasure(string name) { if (_tomTable.Measures.ContainsName(name)) { _tomTable.Measures.Remove(name); } // shell model if (_measures.ContainsName(name)) { _measures.RemoveByName(name); } } /// /// Create measure associated with the Table object. /// /// Tabular Object Model Measure object from the source tabular model to be abstracted in the target. public void CreateMeasure(Tom.Measure tomMeasureSource) { if (_tomTable.Measures.ContainsName(tomMeasureSource.Name)) { _tomTable.Measures.Remove(tomMeasureSource.Name); } Tom.Measure tomMeasureTarget = new Tom.Measure(); tomMeasureSource.CopyTo(tomMeasureTarget); _tomTable.Measures.Add(tomMeasureTarget); // shell model _measures.Add(new Measure(this, tomMeasureTarget, tomMeasureTarget.KPI != null)); } /// /// Update measure associated with the Table object. /// /// Tabular Object Model Measure object from the source tabular model to be abstracted in the target. public void UpdateMeasure(Tom.Measure tomMeasureSource) { if (_measures.ContainsName(tomMeasureSource.Name)) { DeleteMeasure(tomMeasureSource.Name); } CreateMeasure(tomMeasureSource); } // CalculationItems /// /// Delete calculation item associated with the Table object. /// /// Name of the calculationItem to be deleted. public void DeleteCalculationItem(string name) { if (_tomTable.CalculationGroup.CalculationItems.ContainsName(name)) { _tomTable.CalculationGroup.CalculationItems.Remove(name); } // shell model if (_calculationItems.ContainsName(name)) { _calculationItems.RemoveByName(name); } } /// /// Create calculationItem associated with the Table object. /// /// Tabular Object Model CalculationItem object from the source tabular model to be abstracted in the target. public void CreateCalculationItem(Tom.CalculationItem tomCalculationItemSource) { if (_tomTable.CalculationGroup.CalculationItems.ContainsName(tomCalculationItemSource.Name)) { _tomTable.CalculationGroup.CalculationItems.Remove(tomCalculationItemSource.Name); } Tom.CalculationItem tomCalculationItemTarget = new Tom.CalculationItem(); tomCalculationItemSource.CopyTo(tomCalculationItemTarget); _tomTable.CalculationGroup.CalculationItems.Add(tomCalculationItemTarget); // shell model _calculationItems.Add(new CalculationItem(this, tomCalculationItemTarget)); } /// /// Update calculationItem associated with the Table object. /// /// Tabular Object Model CalculationItem object from the source tabular model to be abstracted in the target. public void UpdateCalculationItem(Tom.CalculationItem tomCalculationItemSource) { if (_calculationItems.ContainsName(tomCalculationItemSource.Name)) { DeleteCalculationItem(tomCalculationItemSource.Name); } CreateCalculationItem(tomCalculationItemSource); } #endregion #region Other public methods /// /// For option when retain storage mode in composite models. /// /// public void ResetStorageMode(ModeType modeType) { foreach (Partition partition in _tomTable.Partitions) { partition.Mode = modeType; } _tableModeType = modeType; } /// /// A Boolean specifying whether the table contains a column with the same name searching without case sensitivity. /// /// The name of the column being searched for. /// True if the object is found, or False if it's not found. public bool ColumnsContainsNameCaseInsensitive(string columnName) { foreach (Column column in _tomTable.Columns) { if (column.Name.ToUpper() == columnName.ToUpper()) { return true; } } return false; } #endregion public override string ToString() => this.GetType().FullName; } }