From 769310cce3d78125ce5694a5d969744196ed55f4 Mon Sep 17 00:00:00 2001 From: Christian Wade Date: Fri, 30 Jun 2017 19:03:33 -0700 Subject: [PATCH] checkin bism norm --- .../BismNormalizer.CommandLine/App.config | 14 + .../BismNormalizer.CommandLine.csproj | 69 + .../BismNormalizer.CommandLine/Program.cs | 241 + .../Properties/AssemblyInfo.cs | 36 + .../packages.config | 4 + .../BismNormalizer.IconSetup/App.config | 6 + .../BismNormalizer.IconSetup.csproj | 67 + .../BismNormalizer.IconSetup/Program.cs | 101 + .../Properties/AssemblyInfo.cs | 36 + .../BismNormalizer.IconSetup/app.manifest | 58 + .../BismNormalizer.Tests.csproj | 129 + .../BismNormalizerTests.cs | 68 + .../BismNormalizer.Tests/CreateDbsRunBSMN.bat | 13 + .../BismNormalizer.Tests/DeleteDbs.bat | 1 + .../Properties/AssemblyInfo.cs | 36 + .../BismNormalizer.Tests/Test1103.bsmn | 25 + .../BismNormalizer.Tests/Test1103_Source.xmla | 5717 +++++++++++++++++ .../BismNormalizer.Tests/Test1103_Target.xmla | 2548 ++++++++ .../BismNormalizer.Tests/Test1200.bsmn | 26 + .../BismNormalizer.Tests/Test1200_Source.xmla | 2021 ++++++ .../BismNormalizer.Tests/Test1200_Target.xmla | 2013 ++++++ .../BismNormalizer.Tests/TestPrep.ps1 | 46 + BismNormalizer/BismNormalizer.sln | 40 + .../BismNormalizer/BismNormalizer.csproj | 524 ++ .../BismNormalizer/BismNormalizer.obproj.map | 4130 ++++++++++++ .../BismNormalizer/BismNormalizer.shfbproj | 89 + .../BismNormalizer/BismNormalizer.targets | 186 + .../BismNormalizer/BismNormalizer.vsct | 127 + .../BismNormalizer/BismNormalizerPackage.cs | 536 ++ BismNormalizer/BismNormalizer/DemoHarness.cs | 37 + .../BismNormalizer/EditorFactory.cs | 190 + BismNormalizer/BismNormalizer/EditorPane.cs | 1150 ++++ BismNormalizer/BismNormalizer/EditorPane.resx | 153 + .../Microsoft.Office.Interop.Excel.fakes | Bin 0 -> 276 bytes .../BismNormalizer/GlobalSuppressions.cs | 13 + BismNormalizer/BismNormalizer/Guids.cs | 16 + BismNormalizer/BismNormalizer/Key.snk | Bin 0 -> 596 bytes .../BismNormalizer/NativeMethods.cs | 28 + BismNormalizer/BismNormalizer/PkgCmdID.cs | 16 + .../BismNormalizer/Properties/AssemblyInfo.cs | 33 + .../BismNormalizer/Resources.Designer.cs | 151 + BismNormalizer/BismNormalizer/Resources.resx | 148 + BismNormalizer/BismNormalizer/Resources/1.png | Bin 0 -> 3147 bytes BismNormalizer/BismNormalizer/Resources/2.png | Bin 0 -> 3469 bytes BismNormalizer/BismNormalizer/Resources/3.png | Bin 0 -> 2874 bytes BismNormalizer/BismNormalizer/Resources/4.png | Bin 0 -> 2964 bytes BismNormalizer/BismNormalizer/Resources/5.png | Bin 0 -> 3078 bytes BismNormalizer/BismNormalizer/Resources/6.png | Bin 0 -> 3165 bytes BismNormalizer/BismNormalizer/Resources/7.png | Bin 0 -> 3178 bytes BismNormalizer/BismNormalizer/Resources/8.png | Bin 0 -> 3384 bytes .../BismNormalizer/Resources/Action.png | Bin 0 -> 2852 bytes .../BismNormalizer/Resources/BismMeasure.png | Bin 0 -> 2935 bytes .../Resources/BismNormalizerLogo.png | Bin 0 -> 3848 bytes .../Resources/BismNormalizerLogoText.png | Bin 0 -> 10620 bytes .../BismNormalizer/Resources/ButtonSwitch.png | Bin 0 -> 2954 bytes .../BismNormalizer/Resources/Check.png | Bin 0 -> 3346 bytes .../Resources/CompareBismModels_Small.png | Bin 0 -> 3147 bytes .../BismNormalizer/Resources/Connection.png | Bin 0 -> 2979 bytes .../BismNormalizer/Resources/CreateAction.png | Bin 0 -> 2845 bytes .../Resources/CreateActionGrey.png | Bin 0 -> 1035 bytes .../BismNormalizer/Resources/Culture.png | Bin 0 -> 3028 bytes .../Resources/DeleteAction. BACKUP.png | Bin 0 -> 3170 bytes .../BismNormalizer/Resources/DeleteAction.png | Bin 0 -> 3170 bytes .../Resources/DeleteActionGrey.png | Bin 0 -> 1190 bytes .../BismNormalizer/Resources/Error.png | Bin 0 -> 3021 bytes .../Resources/Expression Old.png | Bin 0 -> 2935 bytes .../BismNormalizer/Resources/Expression.png | Bin 0 -> 1524 bytes .../BismNormalizer/Resources/File.ico | Bin 0 -> 1150 bytes .../BismNormalizer/Resources/Images.png | Bin 0 -> 4056 bytes .../Resources/Informational.png | Bin 0 -> 3296 bytes .../BismNormalizer/Resources/KPI.png | Bin 0 -> 2933 bytes .../BismNormalizer/Resources/LicenseTerms.txt | 50 + .../BismNormalizer/Resources/LogoSmall.png | Bin 0 -> 4390 bytes .../BismNormalizer/Resources/Minus.png | Bin 0 -> 2878 bytes .../BismNormalizer/Resources/Package.ico | Bin 0 -> 1150 bytes .../BismNormalizer/Resources/Perspective.png | Bin 0 -> 3169 bytes .../BismNormalizer/Resources/Plus.png | Bin 0 -> 2929 bytes .../BismNormalizer/Resources/Processing.png | Bin 0 -> 2880 bytes .../BismNormalizer/Resources/Progress.gif | Bin 0 -> 8787 bytes .../Resources/ProgressCheck.png | Bin 0 -> 3300 bytes .../Resources/ProgressError.png | Bin 0 -> 4062 bytes .../Resources/ProgressWarning.png | Bin 0 -> 4306 bytes .../Resources/Relationship. BACKUPpng | Bin 0 -> 3081 bytes .../BismNormalizer/Resources/Relationship.png | Bin 0 -> 3081 bytes .../BismNormalizer/Resources/Role.png | Bin 0 -> 3348 bytes .../BismNormalizer/Resources/SkipAction.png | Bin 0 -> 3155 bytes .../Resources/SkipActionGrey.png | Bin 0 -> 1147 bytes .../BismNormalizer/Resources/Table BACKUP.png | Bin 0 -> 2866 bytes .../BismNormalizer/Resources/Table.png | Bin 0 -> 2866 bytes .../BismNormalizer/Resources/UpdateAction.png | Bin 0 -> 2880 bytes .../BismNormalizer/Resources/Warning.png | Bin 0 -> 3290 bytes .../Resources/WarningToolWindow.png | Bin 0 -> 3197 bytes .../BismNormalizer/Settings.Designer.cs | 290 + BismNormalizer/BismNormalizer/Settings.cs | 27 + .../BismNormalizer/Settings.settings | 72 + .../BismNormalizer/Settings1.Designer.cs | 209 + .../TabularCompare/BlobKeyEventArgs.cs | 35 + .../TabularCompare/ComparisonFactory.cs | 91 + .../TabularCompare/ComparisonInfo.cs | 238 + .../TabularCompare/ConnectionInfo.cs | 513 ++ .../TabularCompare/Core/Comparison.cs | 656 ++ .../TabularCompare/Core/ComparisonObject.cs | 126 + .../DatabaseDeploymentEventArgs.cs | 15 + .../DeploymentCompleteEventArgs.cs | 31 + .../DeploymentMessageEventArgs.cs | 38 + .../BismNormalizer/TabularCompare/Enums.cs | 43 + .../MultidimensionalMetadata/Action.cs | 148 + .../ActionCollection.cs | 75 + .../MultidimensionalMetadata/Comparison.cs | 1295 ++++ .../ComparisonObject.cs | 188 + .../MultidimensionalMetadata/DataSource.cs | 99 + .../DataSourceCollection.cs | 100 + .../ITabularObject.cs | 39 + .../MultidimensionalMetadata/Kpi.cs | 217 + .../MultidimensionalMetadata/KpiCollection.cs | 117 + .../MultidimensionalMetadata/Measure.cs | 321 + .../MeasureCollection.cs | 117 + .../MultidimensionalMetadata/Perspective.cs | 400 ++ .../PerspectiveCollection.cs | 100 + .../MultidimensionalMetadata/Relationship.cs | 129 + .../RelationshipCollection.cs | 118 + .../MultidimensionalMetadata/Role.cs | 143 + .../RoleCollection.cs | 100 + .../MultidimensionalMetadata/Table.cs | 782 +++ .../TableCollection.cs | 118 + .../MultidimensionalMetadata/TabularModel.cs | 2935 +++++++++ .../TabularCompare/OptionsInfo.cs | 180 + .../TabularCompare/PartitionRowCounter.cs | 36 + .../TabularCompare/PasswordPromptEventArgs.cs | 40 + .../TabularCompare/ProcessingTable.cs | 89 + .../ProcessingTableCollection.cs | 79 + .../TabularCompare/SkipSelection.cs | 69 + .../TabularCompare/SkipSelectionCollection.cs | 31 + .../TabularCompare/TabularCompare.dgml | 922 +++ .../TabularMetadata/CalcDependency.cs | 103 + .../CalcDependencyCollection.cs | 113 + .../TabularMetadata/Comparison.cs | 1540 +++++ .../TabularMetadata/ComparisonObject.cs | 216 + .../TabularCompare/TabularMetadata/Culture.cs | 87 + .../TabularMetadata/CultureCollection.cs | 83 + .../TabularMetadata/DataSource.cs | 39 + .../TabularMetadata/DataSourceCollection.cs | 66 + .../TabularMetadata/Expression.cs | 36 + .../TabularMetadata/ExpressionCollection.cs | 83 + .../TabularCompare/TabularMetadata/Measure.cs | 125 + .../TabularMetadata/MeasureCollection.cs | 81 + .../TabularMetadata/Perspective.cs | 131 + .../TabularMetadata/PerspectiveCollection.cs | 83 + .../TabularMetadata/Relationship.cs | 95 + .../TabularMetadata/RelationshipChain.cs | 80 + .../TabularMetadata/RelationshipCollection.cs | 100 + .../TabularMetadata/RelationshipLink.cs | 71 + .../TabularCompare/TabularMetadata/Role.cs | 36 + .../TabularMetadata/RoleCollection.cs | 83 + .../TabularCompare/TabularMetadata/Table.cs | 410 ++ .../TabularMetadata/TableCollection.cs | 107 + .../TabularMetadataClassDiagram.cd | 192 + .../TabularMetadata/TabularModel.cs | 1869 ++++++ .../TabularMetadata/TabularObject.cs | 137 + .../UI/BlobCredentials.Designer.cs | 183 + .../TabularCompare/UI/BlobCredentials.cs | 84 + .../TabularCompare/UI/BlobCredentials.resx | 120 + .../UI/ComparisonControl.Designer.cs | 592 ++ .../TabularCompare/UI/ComparisonControl.cs | 1154 ++++ .../TabularCompare/UI/ComparisonControl.resx | 554 ++ .../TabularCompare/UI/Connections.Designer.cs | 400 ++ .../TabularCompare/UI/Connections.cs | 447 ++ .../TabularCompare/UI/Connections.resx | 123 + .../TabularCompare/UI/Deployment.Designer.cs | 225 + .../TabularCompare/UI/Deployment.cs | 270 + .../TabularCompare/UI/Deployment.resx | 185 + .../TabularCompare/UI/DiffMatchPatch.cs | 2633 ++++++++ .../TabularCompare/UI/EditorTextBox.cs | 60 + .../UI/EditorTextBox.designer.cs | 46 + .../TabularCompare/UI/EditorTextBox.resx | 123 + .../TabularCompare/UI/HighDpiUtils.cs | 198 + .../UI/ImpersonationCredentials.Designer.cs | 207 + .../UI/ImpersonationCredentials.cs | 171 + .../UI/ImpersonationCredentials.resx | 120 + .../TabularCompare/UI/Options.Designer.cs | 278 + .../TabularCompare/UI/Options.cs | 114 + .../TabularCompare/UI/Options.resx | 120 + .../UI/ProcessingErrorMessage.Designer.cs | 82 + .../UI/ProcessingErrorMessage.cs | 60 + .../UI/ProcessingErrorMessage.resx | 120 + .../UI/SynchronizedScrollRichTextBox.cs | 40 + .../TabularCompare/UI/TreeGridCell.cs | 313 + .../TabularCompare/UI/TreeGridEvents.cs | 64 + .../TabularCompare/UI/TreeGridNode.cs | 507 ++ .../UI/TreeGridNodeCollection.cs | 216 + .../TabularCompare/UI/TreeGridView.cs | 594 ++ .../UI/TreeGridViewComparison.cs | 935 +++ .../UI/TreeGridViewValidationOutput.cs | 306 + .../UI/ValidationOutput.Designer.cs | 93 + .../TabularCompare/UI/ValidationOutput.cs | 90 + .../TabularCompare/UI/ValidationOutput.resx | 120 + .../UI/ValidationOutputButton.Designer.cs | 106 + .../UI/ValidationOutputButton.cs | 119 + .../UI/ValidationOutputButton.resx | 177 + .../UI/ValidationOutputButtons.Designer.cs | 47 + .../UI/ValidationOutputButtons.cs | 149 + .../UI/ValidationOutputButtons.resx | 120 + .../ValidationMessageEventArgs.cs | 38 + .../Templates/BismNormalizer.vsdir | 1 + .../Templates/TabularCompare.bsmn | 0 .../BismNormalizer/VSPackage.Designer.cs | 174 + BismNormalizer/BismNormalizer/VSPackage.resx | 158 + BismNormalizer/BismNormalizer/WarningList.cs | 40 + BismNormalizer/BismNormalizer/app.config | 99 + BismNormalizer/BismNormalizer/packages.config | 20 + .../source.extension.vsixmanifest | 26 + .../BismNormalizer/template-report.xml | 8 + BismNormalizer/LICENSE | 21 + BismNormalizer/README.md | 36 + .../Sample/BismNormalizerSample.sln | 25 + BismNormalizer/Sample/SourceModel/Model.bim | 1962 ++++++ .../Sample/SourceModel/SourceModel.smproj | 43 + BismNormalizer/Sample/TargetModel/Model.bim | 1727 +++++ .../Sample/TargetModel/TargetModel.smproj | 43 + 219 files changed, 55315 insertions(+) create mode 100644 BismNormalizer/BismNormalizer.CommandLine/App.config create mode 100644 BismNormalizer/BismNormalizer.CommandLine/BismNormalizer.CommandLine.csproj create mode 100644 BismNormalizer/BismNormalizer.CommandLine/Program.cs create mode 100644 BismNormalizer/BismNormalizer.CommandLine/Properties/AssemblyInfo.cs create mode 100644 BismNormalizer/BismNormalizer.CommandLine/packages.config create mode 100644 BismNormalizer/BismNormalizer.IconSetup/App.config create mode 100644 BismNormalizer/BismNormalizer.IconSetup/BismNormalizer.IconSetup.csproj create mode 100644 BismNormalizer/BismNormalizer.IconSetup/Program.cs create mode 100644 BismNormalizer/BismNormalizer.IconSetup/Properties/AssemblyInfo.cs create mode 100644 BismNormalizer/BismNormalizer.IconSetup/app.manifest create mode 100644 BismNormalizer/BismNormalizer.Tests/BismNormalizer.Tests.csproj create mode 100644 BismNormalizer/BismNormalizer.Tests/BismNormalizerTests.cs create mode 100644 BismNormalizer/BismNormalizer.Tests/CreateDbsRunBSMN.bat create mode 100644 BismNormalizer/BismNormalizer.Tests/DeleteDbs.bat create mode 100644 BismNormalizer/BismNormalizer.Tests/Properties/AssemblyInfo.cs create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1103.bsmn create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1103_Source.xmla create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1103_Target.xmla create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1200.bsmn create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1200_Source.xmla create mode 100644 BismNormalizer/BismNormalizer.Tests/Test1200_Target.xmla create mode 100644 BismNormalizer/BismNormalizer.Tests/TestPrep.ps1 create mode 100644 BismNormalizer/BismNormalizer.sln create mode 100644 BismNormalizer/BismNormalizer/BismNormalizer.csproj create mode 100644 BismNormalizer/BismNormalizer/BismNormalizer.obproj.map create mode 100644 BismNormalizer/BismNormalizer/BismNormalizer.shfbproj create mode 100644 BismNormalizer/BismNormalizer/BismNormalizer.targets create mode 100644 BismNormalizer/BismNormalizer/BismNormalizer.vsct create mode 100644 BismNormalizer/BismNormalizer/BismNormalizerPackage.cs create mode 100644 BismNormalizer/BismNormalizer/DemoHarness.cs create mode 100644 BismNormalizer/BismNormalizer/EditorFactory.cs create mode 100644 BismNormalizer/BismNormalizer/EditorPane.cs create mode 100644 BismNormalizer/BismNormalizer/EditorPane.resx create mode 100644 BismNormalizer/BismNormalizer/Fakes/Microsoft.Office.Interop.Excel.fakes create mode 100644 BismNormalizer/BismNormalizer/GlobalSuppressions.cs create mode 100644 BismNormalizer/BismNormalizer/Guids.cs create mode 100644 BismNormalizer/BismNormalizer/Key.snk create mode 100644 BismNormalizer/BismNormalizer/NativeMethods.cs create mode 100644 BismNormalizer/BismNormalizer/PkgCmdID.cs create mode 100644 BismNormalizer/BismNormalizer/Properties/AssemblyInfo.cs create mode 100644 BismNormalizer/BismNormalizer/Resources.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/Resources.resx create mode 100644 BismNormalizer/BismNormalizer/Resources/1.png create mode 100644 BismNormalizer/BismNormalizer/Resources/2.png create mode 100644 BismNormalizer/BismNormalizer/Resources/3.png create mode 100644 BismNormalizer/BismNormalizer/Resources/4.png create mode 100644 BismNormalizer/BismNormalizer/Resources/5.png create mode 100644 BismNormalizer/BismNormalizer/Resources/6.png create mode 100644 BismNormalizer/BismNormalizer/Resources/7.png create mode 100644 BismNormalizer/BismNormalizer/Resources/8.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Action.png create mode 100644 BismNormalizer/BismNormalizer/Resources/BismMeasure.png create mode 100644 BismNormalizer/BismNormalizer/Resources/BismNormalizerLogo.png create mode 100644 BismNormalizer/BismNormalizer/Resources/BismNormalizerLogoText.png create mode 100644 BismNormalizer/BismNormalizer/Resources/ButtonSwitch.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Check.png create mode 100644 BismNormalizer/BismNormalizer/Resources/CompareBismModels_Small.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Connection.png create mode 100644 BismNormalizer/BismNormalizer/Resources/CreateAction.png create mode 100644 BismNormalizer/BismNormalizer/Resources/CreateActionGrey.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Culture.png create mode 100644 BismNormalizer/BismNormalizer/Resources/DeleteAction. BACKUP.png create mode 100644 BismNormalizer/BismNormalizer/Resources/DeleteAction.png create mode 100644 BismNormalizer/BismNormalizer/Resources/DeleteActionGrey.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Error.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Expression Old.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Expression.png create mode 100644 BismNormalizer/BismNormalizer/Resources/File.ico create mode 100644 BismNormalizer/BismNormalizer/Resources/Images.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Informational.png create mode 100644 BismNormalizer/BismNormalizer/Resources/KPI.png create mode 100644 BismNormalizer/BismNormalizer/Resources/LicenseTerms.txt create mode 100644 BismNormalizer/BismNormalizer/Resources/LogoSmall.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Minus.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Package.ico create mode 100644 BismNormalizer/BismNormalizer/Resources/Perspective.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Plus.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Processing.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Progress.gif create mode 100644 BismNormalizer/BismNormalizer/Resources/ProgressCheck.png create mode 100644 BismNormalizer/BismNormalizer/Resources/ProgressError.png create mode 100644 BismNormalizer/BismNormalizer/Resources/ProgressWarning.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Relationship. BACKUPpng create mode 100644 BismNormalizer/BismNormalizer/Resources/Relationship.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Role.png create mode 100644 BismNormalizer/BismNormalizer/Resources/SkipAction.png create mode 100644 BismNormalizer/BismNormalizer/Resources/SkipActionGrey.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Table BACKUP.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Table.png create mode 100644 BismNormalizer/BismNormalizer/Resources/UpdateAction.png create mode 100644 BismNormalizer/BismNormalizer/Resources/Warning.png create mode 100644 BismNormalizer/BismNormalizer/Resources/WarningToolWindow.png create mode 100644 BismNormalizer/BismNormalizer/Settings.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/Settings.cs create mode 100644 BismNormalizer/BismNormalizer/Settings.settings create mode 100644 BismNormalizer/BismNormalizer/Settings1.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/BlobKeyEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ComparisonFactory.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ComparisonInfo.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ConnectionInfo.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/Core/Comparison.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/Core/ComparisonObject.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/DatabaseDeploymentEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/DeploymentCompleteEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/DeploymentMessageEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/Enums.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Action.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ActionCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Comparison.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ComparisonObject.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSource.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSourceCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ITabularObject.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Kpi.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/KpiCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Measure.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/MeasureCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Perspective.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/PerspectiveCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Relationship.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RelationshipCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Role.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RoleCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Table.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TableCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TabularModel.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/OptionsInfo.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/PartitionRowCounter.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/PasswordPromptEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ProcessingTable.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ProcessingTableCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/SkipSelection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/SkipSelectionCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularCompare.dgml create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependency.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependencyCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Comparison.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ComparisonObject.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Culture.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CultureCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSource.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSourceCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Expression.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ExpressionCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Measure.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/MeasureCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Perspective.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/PerspectiveCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Relationship.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipChain.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipLink.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Role.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RoleCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Table.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TableCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularMetadataClassDiagram.cd create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularModel.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularObject.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Connections.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Connections.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Connections.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Deployment.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Deployment.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Deployment.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/DiffMatchPatch.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/EditorTextBox.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/EditorTextBox.designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/EditorTextBox.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/HighDpiUtils.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ImpersonationCredentials.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ImpersonationCredentials.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ImpersonationCredentials.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Options.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Options.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/Options.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ProcessingErrorMessage.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ProcessingErrorMessage.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ProcessingErrorMessage.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/SynchronizedScrollRichTextBox.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridCell.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridEvents.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridNode.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridNodeCollection.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridView.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridViewComparison.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/TreeGridViewValidationOutput.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutput.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutput.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutput.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButton.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButton.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButton.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButtons.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButtons.cs create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/UI/ValidationOutputButtons.resx create mode 100644 BismNormalizer/BismNormalizer/TabularCompare/ValidationMessageEventArgs.cs create mode 100644 BismNormalizer/BismNormalizer/Templates/BismNormalizer.vsdir create mode 100644 BismNormalizer/BismNormalizer/Templates/TabularCompare.bsmn create mode 100644 BismNormalizer/BismNormalizer/VSPackage.Designer.cs create mode 100644 BismNormalizer/BismNormalizer/VSPackage.resx create mode 100644 BismNormalizer/BismNormalizer/WarningList.cs create mode 100644 BismNormalizer/BismNormalizer/app.config create mode 100644 BismNormalizer/BismNormalizer/packages.config create mode 100644 BismNormalizer/BismNormalizer/source.extension.vsixmanifest create mode 100644 BismNormalizer/BismNormalizer/template-report.xml create mode 100644 BismNormalizer/LICENSE create mode 100644 BismNormalizer/README.md create mode 100644 BismNormalizer/Sample/BismNormalizerSample.sln create mode 100644 BismNormalizer/Sample/SourceModel/Model.bim create mode 100644 BismNormalizer/Sample/SourceModel/SourceModel.smproj create mode 100644 BismNormalizer/Sample/TargetModel/Model.bim create mode 100644 BismNormalizer/Sample/TargetModel/TargetModel.smproj diff --git a/BismNormalizer/BismNormalizer.CommandLine/App.config b/BismNormalizer/BismNormalizer.CommandLine/App.config new file mode 100644 index 0000000..959051b --- /dev/null +++ b/BismNormalizer/BismNormalizer.CommandLine/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/BismNormalizer/BismNormalizer.CommandLine/BismNormalizer.CommandLine.csproj b/BismNormalizer/BismNormalizer.CommandLine/BismNormalizer.CommandLine.csproj new file mode 100644 index 0000000..9efe0ab --- /dev/null +++ b/BismNormalizer/BismNormalizer.CommandLine/BismNormalizer.CommandLine.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {4C77E665-FA37-4793-8950-69AABD3DC626} + Exe + Properties + BismNormalizer.CommandLine + BismNormalizer + v4.6.1 + 512 + true + SAK + SAK + SAK + SAK + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\BismNormalizer\bin\Release\BismNormalizer.dll + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.CommandLine/Program.cs b/BismNormalizer/BismNormalizer.CommandLine/Program.cs new file mode 100644 index 0000000..6421a01 --- /dev/null +++ b/BismNormalizer/BismNormalizer.CommandLine/Program.cs @@ -0,0 +1,241 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Xml.Serialization; +using BismNormalizer.TabularCompare; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.CommandLine +{ + class Program + { + private const int ERROR_SUCCESS = 0; + private const int ERROR_FILE_NOT_FOUND = 2; + private const int ERROR_BAD_ARGUMENTS = 160; + private const int ERROR_GENERIC_NOT_MAPPED = 1360; + private const int ERROR_NULL_REF_POINTER = 1780; + + static int Main(string[] args) + { + string bsmnFile = null; + string logFile = null; + string scriptFile = null; + List skipOptions = null; + StreamWriter writer = null; + Comparison _comparison = null; + + try + { + #region Argument validation / help message + + if (!(args?.Length > 0)) + { + Console.WriteLine("No arguments received. Exiting."); + return ERROR_BAD_ARGUMENTS; + } + else if (args[0].ToLower() == "help" || args[0].ToLower() == "?" || args[0].ToLower() == "/?" || args[0].ToLower() == "/h" || args[0].ToLower() == "/help" || args[0].ToLower() == "-help" || args[0].ToLower() == "-h") + { + Console.WriteLine(""); + Console.WriteLine(" BISM Normalizer Command-Line Utility"); + Console.WriteLine(""); + Console.WriteLine(" Executes BISM Normalizer in command-line mode, based on content of BSMN file"); + Console.WriteLine(""); + Console.WriteLine(" USAGE:"); + Console.WriteLine(""); + Console.WriteLine(" BismNormalizer.exe BsmnFile [/Log:LogFile] [/Script:ScriptFile] [/Skip:{MissingInSource | MissingInTarget | DifferentDefinitions}]"); + Console.WriteLine(""); + Console.WriteLine(" BsmnFile : Full path to the .bsmn file."); + Console.WriteLine(""); + Console.WriteLine(" /Log:LogFile : All messages are output to the LogFile."); + Console.WriteLine(""); + Console.WriteLine(" /Script:ScriptFile : Does not perform actual update to target database; instead, a deployment script is generated and stored to the ScriptFile."); + Console.WriteLine(""); + Console.WriteLine(" /Skip:{MissingInSource | MissingInTarget | DifferentDefinitions} : Skip all objects that are missing in source/missing in target/with different definitions. Can pass a comma separated list of multiple skip options; e.g. 'MissingInSource,MissingInTarget,DifferentDefinitions'."); + Console.WriteLine(""); + + return ERROR_SUCCESS; + } + + bsmnFile = args[0]; + + const string logPrefix = "/log:"; + const string scriptPrefix = "/script:"; + const string skipPrefix = "/skip:"; + + for (int i = 1; i < args.Length; i++) + { + if (args[i].Length >= logPrefix.Length && args[i].Substring(0, logPrefix.Length).ToLower() == logPrefix) + { + logFile = args[i].Substring(logPrefix.Length, args[i].Length - logPrefix.Length).Replace("\"", ""); + } + else if (args[i].Length >= scriptPrefix.Length && args[i].Substring(0, scriptPrefix.Length).ToLower() == scriptPrefix) + { + scriptFile = args[i].Substring(scriptPrefix.Length, args[i].Length - scriptPrefix.Length).Replace("\"", ""); + } + else if (args[i].Length >= skipPrefix.Length && args[i].Substring(0, skipPrefix.Length).ToLower() == skipPrefix) + { + skipOptions = new List(args[i].Substring(skipPrefix.Length, args[i].Length - skipPrefix.Length).Split(',')); + foreach (string skipOption in skipOptions) + { + if (!(skipOption == ComparisonObjectStatus.MissingInSource.ToString() || skipOption == ComparisonObjectStatus.MissingInTarget.ToString() || skipOption == ComparisonObjectStatus.DifferentDefinitions.ToString())) + { + Console.WriteLine($"Argument '{args[i]}' is invalid. Valid skip options are '{ComparisonObjectStatus.MissingInSource.ToString()}', '{ComparisonObjectStatus.MissingInTarget.ToString()}' or '{ComparisonObjectStatus.DifferentDefinitions.ToString()}'"); + return ERROR_BAD_ARGUMENTS; + } + } + } + else + { + Console.WriteLine($"'{args[i]}' is not a valid argument."); + return ERROR_BAD_ARGUMENTS; + } + } + + if (logFile != null) + { + // Attempt to open output file. + writer = new StreamWriter(logFile); + // Redirect output from the console to the file. + Console.SetOut(writer); + } + + #endregion + + if (!File.Exists(bsmnFile)) + { + throw new FileNotFoundException($"File not found {bsmnFile}"); + } + Console.WriteLine($"About to deserialize {bsmnFile}"); + ComparisonInfo comparisonInfo = ComparisonInfo.DeserializeBsmnFile(bsmnFile); + + Console.WriteLine(); + if (comparisonInfo.ConnectionInfoSource.UseProject) + { + Console.WriteLine($"Source Project File: {comparisonInfo.ConnectionInfoSource.ProjectFile}"); + } + else + { + Console.WriteLine($"Source Database: {comparisonInfo.ConnectionInfoSource.ServerName};{comparisonInfo.ConnectionInfoSource.DatabaseName}"); + } + + if (comparisonInfo.ConnectionInfoTarget.UseProject) + { + Console.WriteLine($"Target Project: {comparisonInfo.ConnectionInfoTarget.ProjectName}"); + } + else + { + Console.WriteLine($"Target Database: {comparisonInfo.ConnectionInfoTarget.ServerName};{comparisonInfo.ConnectionInfoTarget.DatabaseName}"); + } + + Console.WriteLine(); + Console.WriteLine("--Comparing ..."); + _comparison = ComparisonFactory.CreateComparison(comparisonInfo); + _comparison.ValidationMessage += HandleValidationMessage; + _comparison.Connect(); + _comparison.CompareTabularModels(); + + if (skipOptions != null) + { + foreach (string skipOption in skipOptions) + { + SetSkipOptions(skipOption, _comparison.ComparisonObjects); + } + } + Console.WriteLine("--Done"); + Console.WriteLine(); + + Console.WriteLine("--Validating ..."); + _comparison.ValidateSelection(); + Console.WriteLine("--Done"); + Console.WriteLine(); + + if (scriptFile != null) + { + Console.WriteLine("--Generating script ..."); + //Generate script + File.WriteAllText(scriptFile, _comparison.ScriptDatabase()); + Console.WriteLine($"Generated script '{scriptFile}'"); + } + else + { + Console.WriteLine("--Updating ..."); + //Update target database/project + _comparison.Update(); + if (comparisonInfo.ConnectionInfoTarget.UseProject) + { + Console.WriteLine($"Applied changes to project {comparisonInfo.ConnectionInfoTarget.ProjectName}."); + } + else + { + Console.WriteLine($"Deployed changes to database {comparisonInfo.ConnectionInfoTarget.DatabaseName}."); + Console.WriteLine("Passwords have not been set for impersonation accounts (setting passwords is not supported in command-line mode). Ensure the passwords are set before processing."); + if (comparisonInfo.OptionsInfo.OptionProcessingOption != ProcessingOption.DoNotProcess) + { + Console.WriteLine("No processing has been done (processing is not supported in command-line mode)."); + } + } + } + Console.WriteLine("--Done"); + } + catch (FileNotFoundException exc) + { + Console.WriteLine("The following exception occurred:"); + Console.WriteLine(exc.ToString()); + + return ERROR_FILE_NOT_FOUND; + } + catch (ArgumentNullException exc) + { + Console.WriteLine("The following exception occurred. Try re-saving the BSMN file from Visual Studio using latest version of BISM Normalizer to ensure all necessary properties are deserialized and stored in the file."); + Console.WriteLine(); + Console.WriteLine(exc.ToString()); + + return ERROR_NULL_REF_POINTER; + } + catch (Exception exc) + { + Console.WriteLine("The following exception occurred:"); + Console.WriteLine(exc.ToString()); + + return ERROR_GENERIC_NOT_MAPPED; + } + finally + { + if (writer != null) + { + writer.Close(); + } + if (_comparison != null) + { + _comparison.Disconnect(); + } + } + + return ERROR_SUCCESS; + } + + private static void SetSkipOptions(string skipOption, List comparisonObjects) + { + foreach (ComparisonObject comparisonObj in comparisonObjects) + { + if ( ((skipOption == ComparisonObjectStatus.MissingInSource.ToString() && comparisonObj.Status == ComparisonObjectStatus.MissingInSource) || + (skipOption == ComparisonObjectStatus.MissingInTarget.ToString() && comparisonObj.Status == ComparisonObjectStatus.MissingInTarget) || + (skipOption == ComparisonObjectStatus.DifferentDefinitions.ToString() && comparisonObj.Status == ComparisonObjectStatus.DifferentDefinitions) + ) && comparisonObj.MergeAction != MergeAction.Skip + ) + { + comparisonObj.MergeAction = MergeAction.Skip; + string objectName = (string.IsNullOrEmpty(comparisonObj.SourceObjectName) ? comparisonObj.TargetObjectName : comparisonObj.SourceObjectName).TrimStart(); + Console.WriteLine($"Skip due to /Skip argument {skipOption} on {comparisonObj.ComparisonObjectType.ToString()} object {objectName}"); + } + + SetSkipOptions(skipOption, comparisonObj.ChildComparisonObjects); + } + } + + private static void HandleValidationMessage(object sender, ValidationMessageEventArgs e) + { + Console.WriteLine($"{e.ValidationMessageStatus.ToString()} Message for {e.ValidationMessageType.ToString()}: {e.Message}"); + } + } +} diff --git a/BismNormalizer/BismNormalizer.CommandLine/Properties/AssemblyInfo.cs b/BismNormalizer/BismNormalizer.CommandLine/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..525c184 --- /dev/null +++ b/BismNormalizer/BismNormalizer.CommandLine/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Bism Normalizer Command Line")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bism Normalizer")] +[assembly: AssemblyProduct("BismNormalizerCommandLine")] +[assembly: AssemblyCopyright("Bism Normalizer")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4c77e665-fa37-4793-8950-69aabd3dc626")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("4.0.0.11")] +[assembly: AssemblyFileVersion("4.0.0.11")] diff --git a/BismNormalizer/BismNormalizer.CommandLine/packages.config b/BismNormalizer/BismNormalizer.CommandLine/packages.config new file mode 100644 index 0000000..9762150 --- /dev/null +++ b/BismNormalizer/BismNormalizer.CommandLine/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.IconSetup/App.config b/BismNormalizer/BismNormalizer.IconSetup/App.config new file mode 100644 index 0000000..bae5d6d --- /dev/null +++ b/BismNormalizer/BismNormalizer.IconSetup/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/BismNormalizer/BismNormalizer.IconSetup/BismNormalizer.IconSetup.csproj b/BismNormalizer/BismNormalizer.IconSetup/BismNormalizer.IconSetup.csproj new file mode 100644 index 0000000..57a7a3d --- /dev/null +++ b/BismNormalizer/BismNormalizer.IconSetup/BismNormalizer.IconSetup.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {849693FC-AD82-4323-8A96-D6A0F6D97566} + Exe + Properties + BismNormalizer.IconSetup + BismNormalizer.IconSetup + v4.6.1 + 512 + SAK + SAK + SAK + SAK + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + app.manifest + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.IconSetup/Program.cs b/BismNormalizer/BismNormalizer.IconSetup/Program.cs new file mode 100644 index 0000000..b7cb04e --- /dev/null +++ b/BismNormalizer/BismNormalizer.IconSetup/Program.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Microsoft.Win32; + +namespace BismNormalizer.IconSetup +{ + class Program + { + [System.Runtime.InteropServices.DllImport("shell32.dll")] + private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); + + const string _extension = ".bsmn"; + const string _progId = "BismNormalizer.BismNormalizerPackage"; + + static void Main(string[] args) + { + try + { + string exeFullName = System.Reflection.Assembly.GetExecutingAssembly().Location; + + //Copy icon to program files + string iconFullName = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\BISM Normalizer"; + CreateDirIfNecessary(iconFullName); + iconFullName += "\\Icon"; + CreateDirIfNecessary(iconFullName); + iconFullName += "\\Package.ico"; + if (!File.Exists(iconFullName)) + { + File.Copy(exeFullName.Replace("BismNormalizer.IconSetup.exe", "Resources\\Package.ico"), iconFullName); + } + + //---------------------- + //Find VS install path and create dos command + string vsVersion = "14.0"; + if (exeFullName.Contains("VisualStudio")) + { + int endVsChar = exeFullName.LastIndexOf("VisualStudio") + 13; + if (exeFullName.Length > endVsChar + 4) + { + string candidateVsVersion = exeFullName.Substring(endVsChar, 4); + decimal vsVersionDecimal; + if (decimal.TryParse(candidateVsVersion, out vsVersionDecimal)) + { + vsVersion = candidateVsVersion; + } + } + } + string command = "\"" + (String)Registry.GetValue(String.Format("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\" + vsVersion), "InstallDir", "") + "devenv.exe\" \"%1\""; + //Console.WriteLine("VS Install Path: " + vsInstallPath); + + RegistryKey fileReg = Registry.ClassesRoot.CreateSubKey(".bsmn"); + fileReg.SetValue("", _progId); + fileReg.CreateSubKey("DefaultIcon").SetValue("", iconFullName); + fileReg.CreateSubKey("PerceivedType").SetValue("", "Text"); + + RegistryKey appReg = Registry.ClassesRoot.CreateSubKey(_progId); + appReg.SetValue("", "BISM Normalizer file"); + appReg.CreateSubKey("DefaultIcon").SetValue("", iconFullName); + + RegistryKey shell = appReg.CreateSubKey("Shell"); + shell.CreateSubKey("open").CreateSubKey("command").SetValue("", command); + shell.CreateSubKey("edit").CreateSubKey("command").SetValue("", command); + + fileReg.Close(); + appReg.Close(); + shell.Close(); + + RegistryKey appAssoc = Registry.CurrentUser.CreateSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.bsmn"); + appAssoc.CreateSubKey("UserChoice").SetValue("Progid", _progId, RegistryValueKind.String); + appAssoc.Close(); + + SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero); + + Console.WriteLine("Set up of icon complete. Re-open Visual Studio to see the icon in Solution Explorer. Can also open .bsmn files from Windows Explorer."); + } + catch (Exception exc) + { + Console.WriteLine("Exception occurred:"); + Console.WriteLine(exc.Message); + } + finally + { + Console.WriteLine(); + Console.WriteLine("Press any key to exit"); + Console.ReadKey(); + } + } + + private static void CreateDirIfNecessary(string targetDir) + { + if (!Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + } + } +} diff --git a/BismNormalizer/BismNormalizer.IconSetup/Properties/AssemblyInfo.cs b/BismNormalizer/BismNormalizer.IconSetup/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dbaf1ba --- /dev/null +++ b/BismNormalizer/BismNormalizer.IconSetup/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Bism Normalizer IconSetup")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bism Normalizer")] +[assembly: AssemblyProduct("BismNormalizerIconSetup")] +[assembly: AssemblyCopyright("Bism Normalizer")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d7cfb625-abcb-4ea4-899e-7d16196a6776")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("4.0.0.11")] +[assembly: AssemblyFileVersion("4.0.0.11")] diff --git a/BismNormalizer/BismNormalizer.IconSetup/app.manifest b/BismNormalizer/BismNormalizer.IconSetup/app.manifest new file mode 100644 index 0000000..ff3d8ae --- /dev/null +++ b/BismNormalizer/BismNormalizer.IconSetup/app.manifest @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BismNormalizer/BismNormalizer.Tests/BismNormalizer.Tests.csproj b/BismNormalizer/BismNormalizer.Tests/BismNormalizer.Tests.csproj new file mode 100644 index 0000000..7d4b541 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/BismNormalizer.Tests.csproj @@ -0,0 +1,129 @@ + + + + Debug + AnyCPU + {3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4} + Library + Properties + BismNormalizer.Tests + BismNormalizer.Tests + v4.6.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + SAK + SAK + SAK + SAK + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + C:\Program Files (x86)\Microsoft SQL Server\130\SDK\Assemblies\Microsoft.AnalysisServices.DLL + + + False + C:\Program Files (x86)\Microsoft SQL Server\130\SDK\Assemblies\Microsoft.AnalysisServices.Core.DLL + + + False + C:\Program Files (x86)\Microsoft SQL Server\130\SDK\Assemblies\Microsoft.AnalysisServices.Tabular.DLL + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.Tests/BismNormalizerTests.cs b/BismNormalizer/BismNormalizer.Tests/BismNormalizerTests.cs new file mode 100644 index 0000000..edd1939 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/BismNormalizerTests.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics; +using Amo=Microsoft.AnalysisServices; +using Tom=Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.Tests +{ + [TestClass] + public class BismNormalizerTests + { + [ClassInitialize] + public static void InitializeDbs(TestContext testContext) + { + ExecBatFile("CreateDbsRunBSMN.bat"); + } + + [ClassCleanup] + public static void CleanupDbs() + { + ExecBatFile("DeleteDbs.bat"); + } + + [TestMethod] + public void TableCount1103() + { + using (Amo.Server server = new Amo.Server()) + { + server.Connect("localhost"); + + Amo.Database db = server.Databases.FindByName("Test1103_Target"); + Assert.IsNotNull(db); + + Assert.AreEqual(3, db.Cubes[0].Dimensions.Count); + server.Disconnect(); + } + } + + [TestMethod] + public void TableCount1200() + { + using (Tom.Server server = new Tom.Server()) + { + server.Connect("localhost"); + + Tom.Database db = server.Databases.FindByName("Test1200_Target"); + Assert.IsNotNull(db); + + Assert.AreEqual(6, db.Model.Tables.Count); + server.Disconnect(); + } + } + + private static void ExecBatFile(string batFileName) + { + Assert.IsTrue(File.Exists(batFileName)); + Process proc = new Process(); + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.FileName = batFileName; + proc.Start(); + Console.WriteLine(proc.StandardOutput.ReadToEnd()); + proc.WaitForExit(); + Assert.AreEqual(proc.ExitCode, 0); + } + } +} diff --git a/BismNormalizer/BismNormalizer.Tests/CreateDbsRunBSMN.bat b/BismNormalizer/BismNormalizer.Tests/CreateDbsRunBSMN.bat new file mode 100644 index 0000000..27fec94 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/CreateDbsRunBSMN.bat @@ -0,0 +1,13 @@ +Powershell.exe -Command Set-ExecutionPolicy Unrestricted + +Powershell.exe .\TestPrep.ps1 + +echo About to run Test1103.bsmn ... +.\..\..\..\BismNormalizer.CommandLine\bin\Release\BismNormalizer.exe Test1103.bsmn + +if %ERRORLEVEL% gtr 0 exit /b %ERRORLEVEL% + +echo About to run Test1200.bsmn ... +.\..\..\..\BismNormalizer.CommandLine\bin\Release\BismNormalizer.exe Test1200.bsmn + +exit /b %ERRORLEVEL% diff --git a/BismNormalizer/BismNormalizer.Tests/DeleteDbs.bat b/BismNormalizer/BismNormalizer.Tests/DeleteDbs.bat new file mode 100644 index 0000000..4ef7bcd --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/DeleteDbs.bat @@ -0,0 +1 @@ +Powershell.exe .\TestPrep.ps1 -DeleteOnly diff --git a/BismNormalizer/BismNormalizer.Tests/Properties/AssemblyInfo.cs b/BismNormalizer/BismNormalizer.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6d988be --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BismNormalizer.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft Corporation")] +[assembly: AssemblyProduct("BismNormalizer.Tests")] +[assembly: AssemblyCopyright("Copyright © Microsoft Corporation 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3ae5d06f-a054-4ad1-83a9-9f0b8b1fc0b4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("3.1.0.23")] +[assembly: AssemblyFileVersion("3.1.0.23")] diff --git a/BismNormalizer/BismNormalizer.Tests/Test1103.bsmn b/BismNormalizer/BismNormalizer.Tests/Test1103.bsmn new file mode 100644 index 0000000..9b199d4 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1103.bsmn @@ -0,0 +1,25 @@ + + + + false + localhost + Test1103_Source + + + false + localhost + Test1103_Target + + + true + true + true + true + true + false + true + Default + false + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.Tests/Test1103_Source.xmla b/BismNormalizer/BismNormalizer.Tests/Test1103_Source.xmla new file mode 100644 index 0000000..d16b8de --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1103_Source.xmla @@ -0,0 +1,5717 @@ + + + + Test1103_Source + Test1103_Source + + + ClientCompatibilityLevel + 300 + + + 1103 + InMemory + 1033 + + Default + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + DataMarketWebBaseUrl + + + DataMarketWebQueryUrl + + + + Sandbox + + AutomaticNull + + IgnoreError + ReportAndStop + ReportAndStop + + InMemory + + + DimensionPermission + DimensionPermission + Role + Allowed + true() + + + 1033 + Unknown + + + __XL_RowNumber + __XL_RowNumber + RowNumber + Key + + + Integer + 4 + Error + + + + + WChar + 4 + ZeroOrBlank + + + + + DateKey + One + None + DateKey + + + FullDateAlternateKey + None + FullDateAlternateKey + + + DayNumberOfWeek + None + DayNumberOfWeek + + + EnglishDayNameOfWeek + None + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + None + SpanishDayNameOfWeek + + + FrenchDayNameOfWeek + None + FrenchDayNameOfWeek + + + DayNumberOfMonth + None + DayNumberOfMonth + + + DayNumberOfYear + None + DayNumberOfYear + + + WeekNumberOfYear + None + WeekNumberOfYear + + + EnglishMonthName + None + EnglishMonthName + + + SpanishMonthName + None + SpanishMonthName + + + FrenchMonthName + None + FrenchMonthName + + + MonthNumberOfYear + None + MonthNumberOfYear + + + CalendarQuarter + None + CalendarQuarter + + + CalendarYear + None + CalendarYear + + + CalendarSemester + None + CalendarSemester + + + FiscalQuarter + None + FiscalQuarter + + + FiscalYear + None + FiscalYear + + + FiscalSemester + None + FiscalSemester + + + Key + false + + + + + Format + + + + + + DateKey + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + Key + + + + + Format + + + + + + FullDateAlternateKey + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + Key + General Date + + + + + Format + + + + + + DayNumberOfWeek + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + Key + + + + + Format + + + + + + EnglishDayNameOfWeek + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + Key + + + + + Format + + + + + + SpanishDayNameOfWeek + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + Key + + + + + Format + + + + + + FrenchDayNameOfWeek + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + Key + + + + + Format + + + + + + DayNumberOfMonth + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + Key + + + + + Format + + + + + + DayNumberOfYear + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + Key + + + + + Format + + + + + + WeekNumberOfYear + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + Key + + + + + Format + + + + + + EnglishMonthName + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + Key + + + + + Format + + + + + + SpanishMonthName + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + Key + + + + + Format + + + + + + FrenchMonthName + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + Key + + + + + Format + + + + + + MonthNumberOfYear + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + Key + + + + + Format + + + + + + CalendarQuarter + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + Key + + + + + Format + + + + + + CalendarYear + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + Key + + + + + Format + + + + + + CalendarSemester + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + Key + + + + + Format + + + + + + FiscalQuarter + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + Key + + + + + Format + + + + + + FiscalYear + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + Key + + + + + Format + + + + + + FiscalSemester + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + Key + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DimProduct + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + DataMarketWebBaseUrl + + + DataMarketWebQueryUrl + + + + Sandbox + + AutomaticNull + + IgnoreError + ReportAndStop + ReportAndStop + + InMemory + 1033 + Unknown + + + __XL_RowNumber + __XL_RowNumber + RowNumber + Key + + + Integer + 4 + Error + + + + + WChar + 4 + ZeroOrBlank + + + + + ProductKey + One + None + ProductKey + + + ProductAlternateKey + None + ProductAlternateKey + + + ProductSubcategoryKey + None + ProductSubcategoryKey + + + WeightUnitMeasureCode + None + WeightUnitMeasureCode + + + SizeUnitMeasureCode + None + SizeUnitMeasureCode + + + EnglishProductName + None + EnglishProductName + + + SpanishProductName + None + SpanishProductName + + + FrenchProductName + None + FrenchProductName + + + StandardCost + None + StandardCost + + + FinishedGoodsFlag + None + FinishedGoodsFlag + + + Color + None + Color + + + SafetyStockLevel + None + SafetyStockLevel + + + ReorderPoint + None + ReorderPoint + + + ListPrice + None + ListPrice + + + Size + None + Size + + + SizeRange + None + SizeRange + + + Weight + None + Weight + + + DaysToManufacture + None + DaysToManufacture + + + ProductLine + None + ProductLine + + + DealerPrice + None + DealerPrice + + + Class + None + Class + + + Style + None + Style + + + ModelName + None + ModelName + + + EnglishDescription + None + EnglishDescription + + + FrenchDescription + None + FrenchDescription + + + ChineseDescription + None + ChineseDescription + + + ArabicDescription + None + ArabicDescription + + + HebrewDescription + None + HebrewDescription + + + ThaiDescription + None + ThaiDescription + + + GermanDescription + None + GermanDescription + + + JapaneseDescription + None + JapaneseDescription + + + TurkishDescription + None + TurkishDescription + + + StartDate + None + StartDate + + + EndDate + None + EndDate + + + Status + None + Status + + + Key + false + + + + + Format + + + + + + ProductKey + ProductKey + + + BigInt + -1 + Error + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductKey + + + Key + + + + + Format + + + + + + ProductAlternateKey + ProductAlternateKey + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductAlternateKey + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductAlternateKey + + + Key + + + + + Format + + + + + + ProductSubcategoryKey + ProductSubcategoryKey + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductSubcategoryKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductSubcategoryKey + + + Key + + + + + Format + + + + + + WeightUnitMeasureCode + WeightUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + WeightUnitMeasureCode + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + WeightUnitMeasureCode + + + Key + + + + + Format + + + + + + SizeUnitMeasureCode + SizeUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeUnitMeasureCode + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeUnitMeasureCode + + + Key + + + + + Format + + + + + + EnglishProductName + EnglishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishProductName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishProductName + + + Key + + + + + Format + + + + + + SpanishProductName + SpanishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SpanishProductName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SpanishProductName + + + Key + + + + + Format + + + + + + FrenchProductName + FrenchProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchProductName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchProductName + + + Key + + + + + Format + + + + + + + + StandardCost + StandardCost + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StandardCost + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StandardCost + + + Key + \$#,0.00;(\$#,0.00);\$#,0.00 + + + + + Format + + + + + + FinishedGoodsFlag + FinishedGoodsFlag + + + Boolean + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FinishedGoodsFlag + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FinishedGoodsFlag + + + Key + "TRUE";"TRUE";"FALSE" + + + + + Format + + + + + + Color + Color + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Color + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Color + + + Key + + + + + Format + + + + + + SafetyStockLevel + SafetyStockLevel + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SafetyStockLevel + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SafetyStockLevel + + + Key + + + + + Format + + + + + + ReorderPoint + ReorderPoint + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ReorderPoint + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ReorderPoint + + + Key + + + + + Format + + + + + + + + ListPrice + ListPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ListPrice + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ListPrice + + + Key + \$#,0.00;(\$#,0.00);\$#,0.00 + + + + + Format + + + + + + Size + Size + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Size + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Size + + + Key + + + + + Format + + + + + + SizeRange + SizeRange + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeRange + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeRange + + + Key + + + + + Format + + + + + + Weight + Weight + + + Double + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Weight + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Weight + + + Key + + + + + Format + + + + + + DaysToManufacture + DaysToManufacture + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DaysToManufacture + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DaysToManufacture + + + Key + + + + + Format + + + + + + ProductLine + ProductLine + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductLine + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductLine + + + Key + + + + + Format + + + + + + + + DealerPrice + DealerPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DealerPrice + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DealerPrice + + + Key + \$#,0.00;(\$#,0.00);\$#,0.00 + + + + + Format + + + + + + Class + Class + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Class + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Class + + + Key + + + + + Format + + + + + + Style + Style + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Style + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Style + + + Key + + + + + Format + + + + + + ModelName + ModelName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ModelName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ModelName + + + Key + + + + + Format + + + + + + EnglishDescription + EnglishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishDescription + + + Key + + + + + Format + + + + + + FrenchDescription + FrenchDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchDescription + + + Key + + + + + Format + + + + + + ChineseDescription + ChineseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ChineseDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ChineseDescription + + + Key + + + + + Format + + + + + + ArabicDescription + ArabicDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ArabicDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ArabicDescription + + + Key + + + + + Format + + + + + + HebrewDescription + HebrewDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + HebrewDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + HebrewDescription + + + Key + + + + + Format + + + + + + ThaiDescription + ThaiDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ThaiDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ThaiDescription + + + Key + + + + + Format + + + + + + GermanDescription + GermanDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + GermanDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + GermanDescription + + + Key + + + + + Format + + + + + + JapaneseDescription + JapaneseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + JapaneseDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + JapaneseDescription + + + Key + + + + + Format + + + + + + TurkishDescription + TurkishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + TurkishDescription + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + TurkishDescription + + + Key + + + + + Format + + + + + + StartDate + StartDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StartDate + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StartDate + + + Key + General Date + + + + + Format + + + + + + EndDate + EndDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EndDate + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EndDate + + + Key + General Date + + + + + Format + + + + + + Status + Status + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Status + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Status + + + Key + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + DataMarketWebBaseUrl + + + DataMarketWebQueryUrl + + + + Sandbox + + AutomaticNull + + IgnoreError + ReportAndStop + ReportAndStop + + InMemory + 1033 + Unknown + + + __XL_RowNumber + __XL_RowNumber + RowNumber + Key + + + Integer + 4 + Error + + + + + WChar + 4 + ZeroOrBlank + + + + + ProductKey + None + ProductKey + + + DateKey + None + DateKey + + + MovementDate + None + MovementDate + + + UnitCost + None + UnitCost + + + UnitsIn + None + UnitsIn + + + UnitsOut + None + UnitsOut + + + UnitsBalance + None + Units Balance + + + Key + false + + + + + Format + + + + + + ProductKey + ProductKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + Key + + + + + Format + + + + + + DateKey + DateKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + Key + + + + + Format + + + + + + MovementDate + MovementDate + + + Date + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + Key + General Date + + + + + Format + + + + + + + + UnitCost + UnitCost + + + Currency + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + Key + \$#,0.00;(\$#,0.00);\$#,0.00 + + + + + Format + + + + + + UnitsIn + UnitsIn + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + Key + + + + + Format + + + + + + UnitsOut + UnitsOut + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + Key + + + + + Format + + + + + + UnitsBalance + Units Balance + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + Key + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + 02b4ee41-10c3-4e36-a3ab-e3c3955de7e7 + + Many + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + DateKey + + + + + One + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + DateKey + + + + + + a68772bd-a0ba-42f3-bc51-10150b016dc3 + + Many + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + ProductKey + + + + + One + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + ProductKey + + + + + + + + + + Model + Model + + + DefaultMeasure + __No measures defined + + + 1033 + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + false + + + DateKey + + + FullDateAlternateKey + + + DayNumberOfWeek + + + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + + + FrenchDayNameOfWeek + + + DayNumberOfMonth + + + DayNumberOfYear + + + WeekNumberOfYear + + + EnglishMonthName + + + SpanishMonthName + + + FrenchMonthName + + + MonthNumberOfYear + + + CalendarQuarter + + + CalendarYear + + + CalendarSemester + + + FiscalQuarter + + + FiscalYear + + + FiscalSemester + + + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DimProduct + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + __XL_RowNumber + false + + + ProductKey + + + ProductAlternateKey + + + ProductSubcategoryKey + + + WeightUnitMeasureCode + + + SizeUnitMeasureCode + + + EnglishProductName + + + SpanishProductName + + + FrenchProductName + + + StandardCost + + + FinishedGoodsFlag + + + Color + + + SafetyStockLevel + + + ReorderPoint + + + ListPrice + + + Size + + + SizeRange + + + Weight + + + DaysToManufacture + + + ProductLine + + + DealerPrice + + + Class + + + Style + + + ModelName + + + EnglishDescription + + + FrenchDescription + + + ChineseDescription + + + ArabicDescription + + + HebrewDescription + + + ThaiDescription + + + GermanDescription + + + JapaneseDescription + + + TurkishDescription + + + StartDate + + + EndDate + + + Status + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + __XL_RowNumber + false + + + ProductKey + + + DateKey + + + MovementDate + + + UnitCost + + + UnitsIn + + + UnitsOut + + + UnitsBalance + + + + + + + CubePermission + CubePermission + Role + Allowed + None + + + CubePermission 1 + CubePermission 1 + Role2 + true + Allowed + None + + + + + New Perspective + New Perspective + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + CalendarQuarter + + + MonthNumberOfYear + + + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + + + SpanishMonthName + + + DateKey + + + DayNumberOfYear + + + CalendarSemester + + + DayNumberOfWeek + + + FiscalSemester + + + FullDateAlternateKey + + + CalendarYear + + + EnglishMonthName + + + FiscalQuarter + + + FrenchDayNameOfWeek + + + WeekNumberOfYear + + + DayNumberOfMonth + + + FiscalYear + + + FrenchMonthName + + + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + Class + + + Color + + + + + + + [Measures].[__No measures defined] + Member + + + + + New Perspective 1 + New Perspective 1 + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + Style + + + ModelName + + + WeightUnitMeasureCode + + + ArabicDescription + + + JapaneseDescription + + + ProductAlternateKey + + + StandardCost + + + Color + + + DaysToManufacture + + + DealerPrice + + + FrenchProductName + + + HebrewDescription + + + ProductKey + + + Size + + + EnglishDescription + + + FinishedGoodsFlag + + + ListPrice + + + ProductLine + + + SizeUnitMeasureCode + + + Class + + + ThaiDescription + + + TurkishDescription + + + FrenchDescription + + + ReorderPoint + + + SizeRange + + + SpanishProductName + + + Status + + + Weight + + + ChineseDescription + + + EndDate + + + GermanDescription + + + ProductSubcategoryKey + + + EnglishProductName + + + SafetyStockLevel + + + StartDate + + + + + + + [Measures].[__No measures defined] + Member + + + + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + __XL_Count DimDate + Count + BigInt + + BigInt + 8 + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + false + + + InMemory + + IgnoreError + ReportAndStop + ReportAndStop + + Regular + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + + + Integer + + DimDate + __XL_RowNumber + + + + Granularity + + + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + + + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + + Shared + + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + + f647e018-10fd-4068-9b11-200795cb98de + SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] + + InMemory + Regular + + IgnoreError + ReportAndStop + ReportAndStop + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DimProduct + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + __XL_Count DimProduct + Count + BigInt + + BigInt + 8 + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + false + + + InMemory + + IgnoreError + ReportAndStop + ReportAndStop + + Regular + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + __XL_RowNumber + + + Integer + + DimProduct + __XL_RowNumber + + + + Granularity + + + ProductKey + + + BigInt + -1 + Error + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductKey + + + + + + ProductAlternateKey + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductAlternateKey + + + + + + ProductSubcategoryKey + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductSubcategoryKey + + + + + + WeightUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + WeightUnitMeasureCode + + + + + + SizeUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeUnitMeasureCode + + + + + + EnglishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishProductName + + + + + + SpanishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SpanishProductName + + + + + + FrenchProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchProductName + + + + + + StandardCost + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StandardCost + + + + + + FinishedGoodsFlag + + + Boolean + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FinishedGoodsFlag + + + + + + Color + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Color + + + + + + SafetyStockLevel + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SafetyStockLevel + + + + + + ReorderPoint + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ReorderPoint + + + + + + ListPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ListPrice + + + + + + Size + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Size + + + + + + SizeRange + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeRange + + + + + + Weight + + + Double + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Weight + + + + + + DaysToManufacture + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DaysToManufacture + + + + + + ProductLine + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductLine + + + + + + DealerPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DealerPrice + + + + + + Class + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Class + + + + + + Style + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Style + + + + + + ModelName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ModelName + + + + + + EnglishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishDescription + + + + + + FrenchDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchDescription + + + + + + ChineseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ChineseDescription + + + + + + ArabicDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ArabicDescription + + + + + + HebrewDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + HebrewDescription + + + + + + ThaiDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ThaiDescription + + + + + + GermanDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + GermanDescription + + + + + + JapaneseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + JapaneseDescription + + + + + + TurkishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + TurkishDescription + + + + + + StartDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StartDate + + + + + + EndDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EndDate + + + + + + Status + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Status + + + + + + Shared + + + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DimProduct + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + + f647e018-10fd-4068-9b11-200795cb98de + SELECT [dbo].[DimProduct].* FROM [dbo].[DimProduct] + + InMemory + Regular + + IgnoreError + ReportAndStop + ReportAndStop + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + __XL_Count FactProductInventory + Count + BigInt + + BigInt + 8 + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + false + + + InMemory + + IgnoreError + ReportAndStop + ReportAndStop + + Regular + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + __XL_RowNumber + + + Integer + + FactProductInventory + __XL_RowNumber + + + + Granularity + + + ProductKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + + + + DateKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + + + + MovementDate + + + Date + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + + + + UnitCost + + + Currency + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + + + + UnitsIn + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + + + + UnitsOut + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + + + + UnitsBalance + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + + + + Shared + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + + + Integer + 4 + Error + + + + + + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + Granularity + + + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + Regular + 02b4ee41-10c3-4e36-a3ab-e3c3955de7e7 + + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + + + __XL_RowNumber + + + Integer + 4 + Error + + + + + + ProductKey + + + BigInt + -1 + Error + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductKey + + + + Granularity + + + ProductAlternateKey + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductAlternateKey + + + + + + ProductSubcategoryKey + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductSubcategoryKey + + + + + + WeightUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + WeightUnitMeasureCode + + + + + + SizeUnitMeasureCode + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeUnitMeasureCode + + + + + + EnglishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishProductName + + + + + + SpanishProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SpanishProductName + + + + + + FrenchProductName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchProductName + + + + + + StandardCost + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StandardCost + + + + + + FinishedGoodsFlag + + + Boolean + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FinishedGoodsFlag + + + + + + Color + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Color + + + + + + SafetyStockLevel + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SafetyStockLevel + + + + + + ReorderPoint + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ReorderPoint + + + + + + ListPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ListPrice + + + + + + Size + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Size + + + + + + SizeRange + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + SizeRange + + + + + + Weight + + + Double + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Weight + + + + + + DaysToManufacture + + + BigInt + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DaysToManufacture + + + + + + ProductLine + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ProductLine + + + + + + DealerPrice + + + Currency + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + DealerPrice + + + + + + Class + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Class + + + + + + Style + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Style + + + + + + ModelName + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ModelName + + + + + + EnglishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EnglishDescription + + + + + + FrenchDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + FrenchDescription + + + + + + ChineseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ChineseDescription + + + + + + ArabicDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ArabicDescription + + + + + + HebrewDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + HebrewDescription + + + + + + ThaiDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + ThaiDescription + + + + + + GermanDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + GermanDescription + + + + + + JapaneseDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + JapaneseDescription + + + + + + TurkishDescription + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + TurkishDescription + + + + + + StartDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + StartDate + + + + + + EndDate + + + Date + -1 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + EndDate + + + + + + Status + + + WChar + 32766 + Preserve + Remove + + DimProduct_aa8f5295-9dcd-48e3-9f50-26f091315c0e + Status + + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + Regular + a68772bd-a0ba-42f3-bc51-10150b016dc3 + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + + f647e018-10fd-4068-9b11-200795cb98de + SELECT [dbo].[FactProductInventory].* FROM [dbo].[FactProductInventory] + + InMemory + Regular + + IgnoreError + ReportAndStop + ReportAndStop + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + Sandbox + + + + MdxScript + MdxScript + + + CALCULATE; +CREATE MEMBER CURRENTCUBE.Measures.[__No measures defined] AS 1, VISIBLE = 0; +ALTER CUBE CURRENTCUBE UPDATE DIMENSION Measures, Default_Member = [__No measures defined]; + + + ---------------------------------------------------------- +-- PowerPivot measures command (do not modify manually) -- +---------------------------------------------------------- + + +CREATE MEASURE 'FactProductInventory'[Sum of UnitsIn]=SUM([UnitsIn]); + + + + FullName + Sum of UnitsIn + + + Table + FactProductInventory + + + + + ---------------------------------------------------------- +-- PowerPivot measures command (do not modify manually) -- +---------------------------------------------------------- + + +CREATE MEASURE 'FactProductInventory'[Average of UnitsIn]=AVERAGE([UnitsIn]); + + + + FullName + Average of UnitsIn + + + Table + FactProductInventory + + + + + ---------------------------------------------------------- +-- PowerPivot measures command (do not modify manually) -- +---------------------------------------------------------- + + +CREATE MEASURE 'FactProductInventory'[Count of UnitsIn]=COUNTROWS('FactProductInventory'); + + + + FullName + Count of UnitsIn + + + Table + FactProductInventory + + + + + + + + + Type + User + + + IsPrivate + False + + + Format + + + + + + [Sum of UnitsIn] + Member + '' + + + + + Type + User + + + IsPrivate + False + + + Format + + + + + + [Average of UnitsIn] + Member + '' + + + + + Type + User + + + IsPrivate + False + + + Format + + + + + + [Count of UnitsIn] + Member + '' + + + Measures.[__No measures defined] + Member + false + + + + + InMemory + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + + f647e018-10fd-4068-9b11-200795cb98de + SqlServer localhost AdventureWorksDW2014 + + + ConnectionEditUISource + SqlServer + + + Provider=SQLNCLI11;Data Source=.;Initial Catalog=AdventureWorksDW2014;Integrated Security=SSPI;Persist Security Info=false + + ImpersonateAccount + wadecb + + PT0S + + + + + Sandbox + Sandbox + f647e018-10fd-4068-9b11-200795cb98de + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Role + Role + + + Role2 + Role 2 + + + + + DatabasePermission + DatabasePermission + Role + Allowed + + + DatabasePermission 1 + DatabasePermission 1 + Role2 + true + Allowed + + + + + + diff --git a/BismNormalizer/BismNormalizer.Tests/Test1103_Target.xmla b/BismNormalizer/BismNormalizer.Tests/Test1103_Target.xmla new file mode 100644 index 0000000..0b6ffb6 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1103_Target.xmla @@ -0,0 +1,2548 @@ + + + + Test1103_Target + Test1103_Target + + + ClientCompatibilityLevel + 300 + + + 1103 + InMemory + 1033 + + Default + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + DataMarketWebBaseUrl + + + DataMarketWebQueryUrl + + + + Sandbox + + AutomaticNull + + IgnoreError + ReportAndStop + ReportAndStop + + InMemory + 1033 + Unknown + + + __XL_RowNumber + __XL_RowNumber + RowNumber + Key + + + Integer + 4 + Error + + + + + WChar + 4 + ZeroOrBlank + + + + + DateKey + One + None + DateKey + + + FullDateAlternateKey + None + FullDateAlternateKey + + + DayNumberOfWeek + None + DayNumberOfWeek + + + EnglishDayNameOfWeek + None + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + None + SpanishDayNameOfWeek + + + FrenchDayNameOfWeek + None + FrenchDayNameOfWeek + + + DayNumberOfMonth + None + DayNumberOfMonth + + + DayNumberOfYear + None + DayNumberOfYear + + + WeekNumberOfYear + None + WeekNumberOfYear + + + EnglishMonthName + None + EnglishMonthName + + + SpanishMonthName + None + SpanishMonthName + + + FrenchMonthName + None + FrenchMonthName + + + MonthNumberOfYear + None + MonthNumberOfYear + + + CalendarQuarter + None + CalendarQuarter + + + CalendarYear + None + CalendarYear + + + CalendarSemester + None + CalendarSemester + + + FiscalQuarter + None + FiscalQuarter + + + FiscalYear + None + FiscalYear + + + FiscalSemester + None + FiscalSemester + + + Key + false + + + + + Format + + + + + + DateKey + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + Key + + + + + Format + + + + + + FullDateAlternateKey + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + Key + General Date + + + + + Format + + + + + + DayNumberOfWeek + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + Key + + + + + Format + + + + + + EnglishDayNameOfWeek + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + Key + + + + + Format + + + + + + SpanishDayNameOfWeek + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + Key + + + + + Format + + + + + + FrenchDayNameOfWeek + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + Key + + + + + Format + + + + + + DayNumberOfMonth + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + Key + + + + + Format + + + + + + DayNumberOfYear + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + Key + + + + + Format + + + + + + WeekNumberOfYear + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + Key + + + + + Format + + + + + + EnglishMonthName + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + Key + + + + + Format + + + + + + SpanishMonthName + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + Key + + + + + Format + + + + + + FrenchMonthName + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + WChar + 32766 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + Key + + + + + Format + + + + + + MonthNumberOfYear + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + Key + + + + + Format + + + + + + CalendarQuarter + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + Key + + + + + Format + + + + + + CalendarYear + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + Key + + + + + Format + + + + + + CalendarSemester + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + Key + + + + + Format + + + + + + FiscalQuarter + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + Key + + + + + Format + + + + + + FiscalYear + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + Key + + + + + Format + + + + + + FiscalSemester + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + WChar + -1 + ZeroOrBlank + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + Key + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + DataMarketWebBaseUrl + + + DataMarketWebQueryUrl + + + + Sandbox + + AutomaticNull + + IgnoreError + ReportAndStop + ReportAndStop + + InMemory + 1033 + Unknown + + + __XL_RowNumber + __XL_RowNumber + RowNumber + Key + + + Integer + 4 + Error + + + + + WChar + 4 + ZeroOrBlank + + + + + ProductKey + None + ProductKey + + + DateKey + None + DateKey + + + MovementDate + None + MovementDate + + + UnitCost + None + UnitCost + + + UnitsIn + None + UnitsIn + + + UnitsOut + None + UnitsOut + + + UnitsBalance + None + UnitsBalance + + + Key + false + + + + + Format + + + + + + ProductKey + ProductKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + Key + + + + + Format + + + + + + DateKey + DateKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + Key + + + + + Format + + + + + + MovementDate + MovementDate + + + Date + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + Key + General Date + + + + + Format + + + + + + + + UnitCost + UnitCost + + + Currency + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + Key + \$#,0.00;(\$#,0.00);\$#,0.00 + + + + + Format + + + + + + UnitsIn + UnitsIn + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + Key + + + + + Format + + + + + + UnitsOut + UnitsOut + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + Key + + + + + Format + + + + + + UnitsBalance + UnitsBalance + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + + + WChar + -1 + ZeroOrBlank + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + Key + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + 02b4ee41-10c3-4e36-a3ab-e3c3955de7e7 + + Many + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + DateKey + + + + + One + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + DateKey + + + + + + + + + + Model + Model + + + DefaultMeasure + __No measures defined + + + 1033 + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + false + + + DateKey + + + FullDateAlternateKey + + + DayNumberOfWeek + + + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + + + FrenchDayNameOfWeek + + + DayNumberOfMonth + + + DayNumberOfYear + + + WeekNumberOfYear + + + EnglishMonthName + + + SpanishMonthName + + + FrenchMonthName + + + MonthNumberOfYear + + + CalendarQuarter + + + CalendarYear + + + CalendarSemester + + + FiscalQuarter + + + FiscalYear + + + FiscalSemester + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + __XL_RowNumber + false + + + ProductKey + + + DateKey + + + MovementDate + + + UnitCost + + + UnitsIn + + + UnitsOut + + + UnitsBalance + + + + + + + CubePermission 1 + CubePermission 1 + Role2 + true + Allowed + None + + + + + New Perspective + New Perspective + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + CalendarQuarter + + + MonthNumberOfYear + + + EnglishDayNameOfWeek + + + SpanishDayNameOfWeek + + + SpanishMonthName + + + DateKey + + + DayNumberOfYear + + + CalendarSemester + + + DayNumberOfWeek + + + FiscalSemester + + + FullDateAlternateKey + + + CalendarYear + + + EnglishMonthName + + + FiscalQuarter + + + FrenchDayNameOfWeek + + + WeekNumberOfYear + + + DayNumberOfMonth + + + FiscalYear + + + FrenchMonthName + + + + + + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + __XL_Count DimDate + Count + BigInt + + BigInt + 8 + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + false + + + InMemory + + IgnoreError + ReportAndStop + ReportAndStop + + Regular + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + + + Integer + + DimDate + __XL_RowNumber + + + + Granularity + + + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + + + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + + Shared + + + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DimDate + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + + f647e018-10fd-4068-9b11-200795cb98de + SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] + + InMemory + Regular + + IgnoreError + ReportAndStop + ReportAndStop + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + __XL_Count FactProductInventory + Count + BigInt + + BigInt + 8 + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + false + + + InMemory + + IgnoreError + ReportAndStop + ReportAndStop + + Regular + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + + + __XL_RowNumber + + + Integer + + FactProductInventory + __XL_RowNumber + + + + Granularity + + + ProductKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + ProductKey + + + + + + DateKey + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + + + + + + MovementDate + + + Date + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + MovementDate + + + + + + UnitCost + + + Currency + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitCost + + + + + + UnitsIn + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsIn + + + + + + UnitsOut + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsOut + + + + + + UnitsBalance + + + BigInt + -1 + Preserve + Remove + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + UnitsBalance + + + + + + Shared + + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + + + __XL_RowNumber + + + Integer + 4 + Error + + + + + + DateKey + + + BigInt + -1 + Error + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DateKey + + + + Granularity + + + FullDateAlternateKey + + + Date + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FullDateAlternateKey + + + + + + DayNumberOfWeek + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfWeek + + + + + + EnglishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishDayNameOfWeek + + + + + + SpanishDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishDayNameOfWeek + + + + + + FrenchDayNameOfWeek + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchDayNameOfWeek + + + + + + DayNumberOfMonth + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfMonth + + + + + + DayNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + DayNumberOfYear + + + + + + WeekNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + WeekNumberOfYear + + + + + + EnglishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + EnglishMonthName + + + + + + SpanishMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + SpanishMonthName + + + + + + FrenchMonthName + + + WChar + 32766 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FrenchMonthName + + + + + + MonthNumberOfYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + MonthNumberOfYear + + + + + + CalendarQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarQuarter + + + + + + CalendarYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarYear + + + + + + CalendarSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + CalendarSemester + + + + + + FiscalQuarter + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalQuarter + + + + + + FiscalYear + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalYear + + + + + + FiscalSemester + + + BigInt + -1 + Preserve + Remove + + DimDate_2cd6fdb1-0f87-44f2-a6a1-04722abf8fe3 + FiscalSemester + + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + DateKey + Regular + 02b4ee41-10c3-4e36-a3ab-e3c3955de7e7 + + + + + FactProductInventory_3142aa6e-4abb-40d3-9a0c-6d6d834bc117 + FactProductInventory + + + IsQueryEditorUsed + False + + + QueryEditorSerialization + + + TableWidgetSerialization + + + + f647e018-10fd-4068-9b11-200795cb98de + SELECT [dbo].[FactProductInventory].* FROM [dbo].[FactProductInventory] + + InMemory + Regular + + IgnoreError + ReportAndStop + ReportAndStop + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + Sandbox + + + + MdxScript + MdxScript + + + CALCULATE; +CREATE MEMBER CURRENTCUBE.Measures.[__No measures defined] AS 1, VISIBLE = 0; +ALTER CUBE CURRENTCUBE UPDATE DIMENSION Measures, Default_Member = [__No measures defined]; + + + ---------------------------------------------------------- +-- PowerPivot measures command (do not modify manually) -- +---------------------------------------------------------- + + +CREATE MEASURE 'FactProductInventory'[Average of UnitsIn]=AVERAGE([UnitsIn]); + + + + FullName + Average of UnitsIn + + + Table + FactProductInventory + + + + + ---------------------------------------------------------- +-- PowerPivot measures command (do not modify manually) -- +---------------------------------------------------------- + + +CREATE MEASURE 'FactProductInventory'[Count of UnitsIn]=COUNTA([UnitsIn]); + + + + FullName + Count of UnitsIn + + + Table + FactProductInventory + + + + + + + + + Type + User + + + IsPrivate + False + + + Format + + + + + + [Average of UnitsIn] + Member + '' + + + + + Type + User + + + IsPrivate + False + + + Format + + + + + + [Count of UnitsIn] + Member + '' + + + Measures.[__No measures defined] + Member + false + + + + + InMemory + + -PT1S + -PT1S + -PT1S + -PT1S + + + + + + + f647e018-10fd-4068-9b11-200795cb98de + SqlServer localhost AdventureWorksDW2014 + + + ConnectionEditUISource + SqlServer + + + Provider=SQLNCLI11;Data Source=.;Initial Catalog=AdventureWorksDW2014;Integrated Security=SSPI;Persist Security Info=false + + ImpersonateAccount + wadecb + + PT0S + + + + + Sandbox + Sandbox + f647e018-10fd-4068-9b11-200795cb98de + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Role2 + Role 2 + + + + + DatabasePermission 1 + DatabasePermission 1 + Role2 + true + Allowed + + + + + + diff --git a/BismNormalizer/BismNormalizer.Tests/Test1200.bsmn b/BismNormalizer/BismNormalizer.Tests/Test1200.bsmn new file mode 100644 index 0000000..72e11f1 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1200.bsmn @@ -0,0 +1,26 @@ + + + + false + localhost + Test1200_Source + Tabular1200 + + + false + localhost + Test1200_Target + + + true + true + true + true + true + false + true + Default + false + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer.Tests/Test1200_Source.xmla b/BismNormalizer/BismNormalizer.Tests/Test1200_Source.xmla new file mode 100644 index 0000000..2b93040 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1200_Source.xmla @@ -0,0 +1,2021 @@ +{ + "createOrReplace": { + "object": { + "database": "Test1200_Source" + }, + "database": { + "name": "Test1200_Source", + "id": "Test1200_Source", + "compatibilityLevel": 1200, + "model": { + "culture": "en-US", + "dataSources": [ + { + "name": "SqlServer localhost AdventureWorksDW2014", + "connectionString": "Provider=SQLNCLI11;Data Source=.;Initial Catalog=AdventureWorksDW2014;Integrated Security=SSPI;Persist Security Info=false", + "impersonationMode": "impersonateAccount", + "account": "wadecb", + "annotations": [ + { + "name": "ConnectionEditUISource", + "value": "SqlServer" + } + ] + } + ], + "tables": [ + { + "name": "DimCustomer", + "columns": [ + { + "name": "CustomerKey", + "dataType": "int64", + "sourceColumn": "CustomerKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "GeographyKey", + "dataType": "int64", + "sourceColumn": "GeographyKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerAlternateKey", + "dataType": "string", + "sourceColumn": "CustomerAlternateKey", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Title", + "dataType": "string", + "sourceColumn": "Title", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FirstName", + "dataType": "string", + "sourceColumn": "FirstName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MiddleName", + "dataType": "string", + "sourceColumn": "MiddleName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "LastName", + "dataType": "string", + "sourceColumn": "LastName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NameStyle", + "dataType": "boolean", + "sourceColumn": "NameStyle", + "formatString": "\"TRUE\";\"TRUE\";\"FALSE\"", + "sourceProviderType": "Boolean", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "BirthDate", + "dataType": "dateTime", + "sourceColumn": "BirthDate", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MaritalStatus", + "dataType": "string", + "sourceColumn": "MaritalStatus", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Suffix", + "dataType": "string", + "sourceColumn": "Suffix", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Gender", + "dataType": "string", + "sourceColumn": "Gender", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EmailAddress", + "dataType": "string", + "sourceColumn": "EmailAddress", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "YearlyIncome", + "dataType": "decimal", + "sourceColumn": "YearlyIncome", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TotalChildren", + "dataType": "int64", + "sourceColumn": "TotalChildren", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NumberChildrenAtHome", + "dataType": "int64", + "sourceColumn": "NumberChildrenAtHome", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishEducation", + "dataType": "string", + "sourceColumn": "EnglishEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishEducation", + "dataType": "string", + "sourceColumn": "SpanishEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchEducation", + "dataType": "string", + "sourceColumn": "FrenchEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishOccupation", + "dataType": "string", + "sourceColumn": "EnglishOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishOccupation", + "dataType": "string", + "sourceColumn": "SpanishOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchOccupation", + "dataType": "string", + "sourceColumn": "FrenchOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "HouseOwnerFlag", + "dataType": "string", + "sourceColumn": "HouseOwnerFlag", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NumberCarsOwned", + "dataType": "int64", + "sourceColumn": "NumberCarsOwned", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "AddressLine1", + "dataType": "string", + "sourceColumn": "AddressLine1", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "AddressLine2", + "dataType": "string", + "sourceColumn": "AddressLine2", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Phone", + "dataType": "string", + "sourceColumn": "Phone", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DateFirstPurchase", + "dataType": "dateTime", + "sourceColumn": "DateFirstPurchase", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CommuteDistance", + "dataType": "string", + "sourceColumn": "CommuteDistance", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimCustomer", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimCustomer].* FROM [dbo].[DimCustomer] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimCustomer].* FROM [dbo].[DimCustomer] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimCustomer" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimDate", + "columns": [ + { + "name": "DateKey", + "dataType": "int64", + "sourceColumn": "DateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FullDateAlternateKey", + "dataType": "dateTime", + "sourceColumn": "FullDateAlternateKey", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfWeek", + "dataType": "int64", + "sourceColumn": "DayNumberOfWeek", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishDayNameOfWeek", + "dataType": "string", + "sourceColumn": "EnglishDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishDayNameOfWeek", + "dataType": "string", + "sourceColumn": "SpanishDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchDayNameOfWeek", + "dataType": "string", + "sourceColumn": "FrenchDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfMonth", + "dataType": "int64", + "sourceColumn": "DayNumberOfMonth", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfYear", + "dataType": "int64", + "sourceColumn": "DayNumberOfYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "WeekNumberOfYear", + "dataType": "int64", + "sourceColumn": "WeekNumberOfYear", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishMonthName", + "dataType": "string", + "sourceColumn": "EnglishMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishMonthName", + "dataType": "string", + "sourceColumn": "SpanishMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchMonthName", + "dataType": "string", + "sourceColumn": "FrenchMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MonthNumberOfYear", + "dataType": "int64", + "sourceColumn": "MonthNumberOfYear", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarQuarter", + "dataType": "int64", + "sourceColumn": "CalendarQuarter", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarYear", + "dataType": "int64", + "sourceColumn": "CalendarYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarSemester", + "dataType": "int64", + "sourceColumn": "CalendarSemester", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalQuarter", + "dataType": "int64", + "sourceColumn": "FiscalQuarter", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalYear", + "dataType": "int64", + "sourceColumn": "FiscalYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalSemester", + "dataType": "int64", + "sourceColumn": "FiscalSemester", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimDate", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] " + } + } + ], + "hierarchies": [ + { + "name": "Calendar", + "levels": [ + { + "name": "CalendarYear", + "ordinal": 0, + "column": "CalendarYear" + }, + { + "name": "EnglishMonthName", + "ordinal": 1, + "column": "EnglishMonthName" + }, + { + "name": "DateKey", + "ordinal": 2, + "column": "DateKey" + } + ] + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimDate" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimProduct", + "columns": [ + { + "name": "ProductKey", + "dataType": "int64", + "sourceColumn": "ProductKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductAlternateKey", + "dataType": "string", + "sourceColumn": "ProductAlternateKey", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductSubcategoryKey", + "dataType": "int64", + "sourceColumn": "ProductSubcategoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "WeightUnitMeasureCode", + "dataType": "string", + "sourceColumn": "WeightUnitMeasureCode", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SizeUnitMeasureCode", + "dataType": "string", + "sourceColumn": "SizeUnitMeasureCode", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishProductName", + "dataType": "string", + "sourceColumn": "EnglishProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishProductName", + "dataType": "string", + "sourceColumn": "SpanishProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchProductName", + "dataType": "string", + "sourceColumn": "FrenchProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StandardCost", + "dataType": "decimal", + "sourceColumn": "StandardCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FinishedGoodsFlag", + "dataType": "boolean", + "sourceColumn": "FinishedGoodsFlag", + "formatString": "\"TRUE\";\"TRUE\";\"FALSE\"", + "sourceProviderType": "Boolean", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Color", + "dataType": "string", + "sourceColumn": "Color", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SafetyStockLevel", + "dataType": "int64", + "sourceColumn": "SafetyStockLevel", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ReorderPoint", + "dataType": "int64", + "sourceColumn": "ReorderPoint", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ListPrice", + "dataType": "decimal", + "sourceColumn": "ListPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Size", + "dataType": "string", + "sourceColumn": "Size", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SizeRange", + "dataType": "string", + "sourceColumn": "SizeRange", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Weight", + "dataType": "double", + "sourceColumn": "Weight", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DaysToManufacture", + "dataType": "int64", + "sourceColumn": "DaysToManufacture", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductLine", + "dataType": "string", + "sourceColumn": "ProductLine", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DealerPrice", + "dataType": "decimal", + "sourceColumn": "DealerPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Class", + "dataType": "string", + "sourceColumn": "Class", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Style", + "dataType": "string", + "sourceColumn": "Style", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ModelName", + "dataType": "string", + "sourceColumn": "ModelName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishDescription", + "dataType": "string", + "sourceColumn": "EnglishDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchDescription", + "dataType": "string", + "sourceColumn": "FrenchDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ChineseDescription", + "dataType": "string", + "sourceColumn": "ChineseDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ArabicDescription", + "dataType": "string", + "sourceColumn": "ArabicDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "HebrewDescription", + "dataType": "string", + "sourceColumn": "HebrewDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ThaiDescription", + "dataType": "string", + "sourceColumn": "ThaiDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "GermanDescription", + "dataType": "string", + "sourceColumn": "GermanDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "JapaneseDescription", + "dataType": "string", + "sourceColumn": "JapaneseDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TurkishDescription", + "dataType": "string", + "sourceColumn": "TurkishDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StartDate", + "dataType": "dateTime", + "sourceColumn": "StartDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EndDate", + "dataType": "dateTime", + "sourceColumn": "EndDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Status", + "dataType": "string", + "sourceColumn": "Status", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimProduct", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimProduct].* FROM [dbo].[DimProduct] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimProduct].* FROM [dbo].[DimProduct] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimProduct" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimPromotion", + "columns": [ + { + "name": "PromotionKey", + "dataType": "int64", + "sourceColumn": "PromotionKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "PromotionAlternateKey", + "dataType": "int64", + "sourceColumn": "PromotionAlternateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionName", + "dataType": "string", + "sourceColumn": "EnglishPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionName", + "dataType": "string", + "sourceColumn": "SpanishPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionName", + "dataType": "string", + "sourceColumn": "FrenchPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DiscountPct", + "dataType": "double", + "sourceColumn": "DiscountPct", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionType", + "dataType": "string", + "sourceColumn": "EnglishPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionType", + "dataType": "string", + "sourceColumn": "SpanishPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionType", + "dataType": "string", + "sourceColumn": "FrenchPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionCategory", + "dataType": "string", + "sourceColumn": "EnglishPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionCategory", + "dataType": "string", + "sourceColumn": "SpanishPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionCategory", + "dataType": "string", + "sourceColumn": "FrenchPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StartDate", + "dataType": "dateTime", + "sourceColumn": "StartDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EndDate", + "dataType": "dateTime", + "sourceColumn": "EndDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MinQty", + "dataType": "int64", + "sourceColumn": "MinQty", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MaxQty", + "dataType": "int64", + "sourceColumn": "MaxQty", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimPromotion", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimPromotion].* FROM [dbo].[DimPromotion] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimPromotion].* FROM [dbo].[DimPromotion] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimPromotion" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimSalesTerritory", + "columns": [ + { + "name": "SalesTerritoryKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryAlternateKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryAlternateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryRegion", + "dataType": "string", + "sourceColumn": "SalesTerritoryRegion", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryCountry", + "dataType": "string", + "sourceColumn": "SalesTerritoryCountry", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryGroup", + "dataType": "string", + "sourceColumn": "SalesTerritoryGroup", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimSalesTerritory", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimSalesTerritory].* FROM [dbo].[DimSalesTerritory] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimSalesTerritory].* FROM [dbo].[DimSalesTerritory] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimSalesTerritory" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "FactInternetSales", + "columns": [ + { + "name": "ProductKey", + "dataType": "int64", + "sourceColumn": "ProductKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderDateKey", + "dataType": "int64", + "sourceColumn": "OrderDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DueDateKey", + "dataType": "int64", + "sourceColumn": "DueDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ShipDateKey", + "dataType": "int64", + "sourceColumn": "ShipDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerKey", + "dataType": "int64", + "sourceColumn": "CustomerKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "PromotionKey", + "dataType": "int64", + "sourceColumn": "PromotionKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesOrderNumber", + "dataType": "string", + "sourceColumn": "SalesOrderNumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesOrderLineNumber", + "dataType": "int64", + "sourceColumn": "SalesOrderLineNumber", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "RevisionNumber", + "dataType": "int64", + "sourceColumn": "RevisionNumber", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderQuantity", + "dataType": "int64", + "sourceColumn": "OrderQuantity", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "UnitPrice", + "dataType": "decimal", + "sourceColumn": "UnitPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ExtendedAmount", + "dataType": "decimal", + "sourceColumn": "ExtendedAmount", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "UnitPriceDiscountPct", + "dataType": "double", + "sourceColumn": "UnitPriceDiscountPct", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DiscountAmount", + "dataType": "double", + "sourceColumn": "DiscountAmount", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductStandardCost", + "dataType": "decimal", + "sourceColumn": "ProductStandardCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TotalProductCost", + "dataType": "decimal", + "sourceColumn": "TotalProductCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesAmount", + "dataType": "decimal", + "sourceColumn": "SalesAmount", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TaxAmt", + "dataType": "decimal", + "sourceColumn": "TaxAmt", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Freight", + "dataType": "decimal", + "sourceColumn": "Freight", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CarrierTrackingNumber", + "dataType": "string", + "sourceColumn": "CarrierTrackingNumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerPONumber", + "dataType": "string", + "sourceColumn": "CustomerPONumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderDate", + "dataType": "dateTime", + "sourceColumn": "OrderDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DueDate", + "dataType": "dateTime", + "sourceColumn": "DueDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ShipDate", + "dataType": "dateTime", + "sourceColumn": "ShipDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "FactInternetSales", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[FactInternetSales].* FROM [dbo].[FactInternetSales] " + } + } + ], + "measures": [ + { + "name": "SalesAvg", + "expression": "AVERAGE([SalesAmount])", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "kpi": { + "targetExpression": "'FactInternetSales'[Sales]", + "targetFormatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "statusGraphic": "Traffic Light - Single", + "statusExpression": [ + "var x='FactInternetSales'[SalesAvg]/'FactInternetSales'[_SalesAvg Goal] return", + "if(ISBLANK(x),BLANK(),", + " If(x<0.26,-1,", + "\t If(x<0.8,0,1)", + " )", + ")", + "" + ], + "annotations": [ + { + "name": "GoalType", + "value": "Measure" + }, + { + "name": "KpiStatusType", + "value": "Linear" + }, + { + "name": "KpiThresholdType", + "value": "Percentage" + }, + { + "name": "KpiThresholdOrdering", + "value": "Ascending" + }, + { + "name": "KpiThresholdCount", + "value": "2" + }, + { + "name": "KpiThreshold_0", + "value": "26" + }, + { + "name": "KpiThreshold_1", + "value": "80" + } + ] + }, + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Sales", + "expression": [ + "VAR", + " CurrentSales = SUM ( FactInternetSales[OrderQuantity] )", + "VAR", + " SalesLastYear = CALCULATE (", + " SUM ( FactInternetSales[OrderQuantity] ),", + " SAMEPERIODLASTYEAR ( 'DimDate'[FullDateAlternateKey] )", + " )", + "RETURN", + " IF (", + " AND ( CurrentSales <> 0, SalesLastYear <> 0 ),", + " DIVIDE (", + " CurrentSales - SalesLastYear,", + " SalesLastYear", + " )", + " )" + ], + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[FactInternetSales].* FROM [dbo].[FactInternetSales] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "FactInternetSales" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + } + ], + "relationships": [ + { + "name": "e8247ff5-7756-4439-9b14-b840782b2991", + "toTable": "DimCustomer", + "toColumn": "CustomerKey", + "fromTable": "FactInternetSales", + "fromColumn": "CustomerKey" + }, + { + "name": "9dc6d42e-d1dd-47ee-99bb-66e6415b9853", + "toTable": "DimDate", + "toColumn": "DateKey", + "fromColumn": "OrderDateKey", + "fromTable": "FactInternetSales", + "isActive": false + }, + { + "name": "b91c8435-2504-460c-8c8e-dccf07f7afcf", + "toTable": "DimDate", + "toColumn": "DateKey", + "fromColumn": "DueDateKey", + "fromTable": "FactInternetSales", + "isActive": false + }, + { + "name": "efaeafff-93c4-42fd-8682-b2fe1f9b68f1", + "toTable": "DimProduct", + "toColumn": "ProductKey", + "fromTable": "FactInternetSales", + "fromColumn": "ProductKey" + }, + { + "name": "2036eea3-2630-464a-9272-bea5c306d2bf", + "toTable": "DimPromotion", + "toColumn": "PromotionKey", + "fromTable": "FactInternetSales", + "fromColumn": "PromotionKey" + } + ], + "cultures": [ + { + "name": "es-ES", + "translations": { + "model": { + "name": "Model", + "perspectives": [ + { + "name": "Sales Perspective", + "translatedCaption": "MuiBueno" + } + ] + } + } + }, + { + "name": "pt-BR", + "translations": { + "model": { + "name": "Model", + "tables": [ + { + "name": "DimDate", + "hierarchies": [ + { + "name": "Calendar", + "translatedCaption": "MuitoBom" + } + ] + }, + { + "name": "DimPromotion", + "translatedDescription": "MuitoBom", + "translatedCaption": "MuitoBom", + "columns": [ + { + "name": "PromotionKey", + "translatedCaption": "MuitoBom" + } + ] + } + ], + "perspectives": [ + { + "name": "Sales Perspective", + "translatedDescription": "MuitoBom", + "translatedCaption": "MuitoBom" + } + ] + } + } + } + ], + "perspectives": [ + { + "name": "Sales Perspective", + "tables": [ + { + "name": "DimDate", + "hierarchies": [ + { + "name": "Calendar" + } + ] + }, + { + "name": "DimSalesTerritory", + "columns": [ + { + "name": "SalesTerritoryAlternateKey" + }, + { + "name": "SalesTerritoryCountry" + }, + { + "name": "SalesTerritoryGroup" + }, + { + "name": "SalesTerritoryKey" + }, + { + "name": "SalesTerritoryRegion" + } + ] + }, + { + "name": "FactInternetSales", + "measures": [ + { + "name": "Sales" + }, + { + "name": "SalesAvg" + } + ] + } + ] + } + ] + } + } + } +} + diff --git a/BismNormalizer/BismNormalizer.Tests/Test1200_Target.xmla b/BismNormalizer/BismNormalizer.Tests/Test1200_Target.xmla new file mode 100644 index 0000000..a6377c5 --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/Test1200_Target.xmla @@ -0,0 +1,2013 @@ +{ + "createOrReplace": { + "object": { + "database": "Test1200_Target" + }, + "database": { + "name": "Test1200_Target", + "id": "Test1200_Target", + "compatibilityLevel": 1200, + "model": { + "culture": "en-US", + "dataSources": [ + { + "name": "SqlServer localhost AdventureWorksDW2014", + "connectionString": "Provider=SQLNCLI11;Data Source=.;Initial Catalog=AdventureWorksDW2014;Integrated Security=SSPI;Persist Security Info=false", + "impersonationMode": "impersonateAccount", + "account": "wadecb", + "annotations": [ + { + "name": "ConnectionEditUISource", + "value": "SqlServer" + } + ] + } + ], + "tables": [ + { + "name": "DimCustomer", + "columns": [ + { + "name": "CustomerKey", + "dataType": "int64", + "sourceColumn": "CustomerKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "GeographyKey", + "dataType": "int64", + "sourceColumn": "GeographyKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerAlternateKey", + "dataType": "string", + "sourceColumn": "CustomerAlternateKey", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Title", + "dataType": "string", + "sourceColumn": "Title", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FirstName", + "dataType": "string", + "sourceColumn": "FirstName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MiddleName", + "dataType": "string", + "sourceColumn": "MiddleName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "LastName", + "dataType": "string", + "sourceColumn": "LastName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NameStyle", + "dataType": "boolean", + "sourceColumn": "NameStyle", + "formatString": "\"TRUE\";\"TRUE\";\"FALSE\"", + "sourceProviderType": "Boolean", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "BirthDate", + "dataType": "dateTime", + "sourceColumn": "BirthDate", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MaritalStatus", + "dataType": "string", + "sourceColumn": "MaritalStatus", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Suffix", + "dataType": "string", + "sourceColumn": "Suffix", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Gender", + "dataType": "string", + "sourceColumn": "Gender", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EmailAddress", + "dataType": "string", + "sourceColumn": "EmailAddress", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "YearlyIncome", + "dataType": "decimal", + "sourceColumn": "YearlyIncome", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TotalChildren", + "dataType": "int64", + "sourceColumn": "TotalChildren", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NumberChildrenAtHome", + "dataType": "int64", + "sourceColumn": "NumberChildrenAtHome", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishEducation", + "dataType": "string", + "sourceColumn": "EnglishEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishEducation", + "dataType": "string", + "sourceColumn": "SpanishEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchEducation", + "dataType": "string", + "sourceColumn": "FrenchEducation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishOccupation", + "dataType": "string", + "sourceColumn": "EnglishOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishOccupation", + "dataType": "string", + "sourceColumn": "SpanishOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchOccupation", + "dataType": "string", + "sourceColumn": "FrenchOccupation", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "HouseOwnerFlag", + "dataType": "string", + "sourceColumn": "HouseOwnerFlag", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "NumberCarsOwned", + "dataType": "int64", + "sourceColumn": "NumberCarsOwned", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "AddressLine1", + "dataType": "string", + "sourceColumn": "AddressLine1", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "AddressLine2", + "dataType": "string", + "sourceColumn": "AddressLine2", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Phone", + "dataType": "string", + "sourceColumn": "Phone", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DateFirstPurchase", + "dataType": "dateTime", + "sourceColumn": "DateFirstPurchase", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CommuteDistance", + "dataType": "string", + "sourceColumn": "CommuteDistance", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimCustomer", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimCustomer].* FROM [dbo].[DimCustomer] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimCustomer].* FROM [dbo].[DimCustomer] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimCustomer" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimProduct", + "columns": [ + { + "name": "ProductKey", + "dataType": "int64", + "sourceColumn": "ProductKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductAlternateKey", + "dataType": "string", + "sourceColumn": "ProductAlternateKey", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductSubcategoryKey", + "dataType": "int64", + "sourceColumn": "ProductSubcategoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "WeightUnitMeasureCode", + "dataType": "string", + "sourceColumn": "WeightUnitMeasureCode", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SizeUnitMeasureCode", + "dataType": "string", + "sourceColumn": "SizeUnitMeasureCode", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishProductName", + "dataType": "string", + "sourceColumn": "EnglishProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishProductName", + "dataType": "string", + "sourceColumn": "SpanishProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchProductName", + "dataType": "string", + "sourceColumn": "FrenchProductName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StandardCost", + "dataType": "decimal", + "sourceColumn": "StandardCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FinishedGoodsFlag", + "dataType": "boolean", + "sourceColumn": "FinishedGoodsFlag", + "formatString": "\"TRUE\";\"TRUE\";\"FALSE\"", + "sourceProviderType": "Boolean", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Color", + "dataType": "string", + "sourceColumn": "Color", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SafetyStockLevel", + "dataType": "int64", + "sourceColumn": "SafetyStockLevel", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ReorderPoint", + "dataType": "int64", + "sourceColumn": "ReorderPoint", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ListPrice", + "dataType": "decimal", + "sourceColumn": "ListPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Size", + "dataType": "string", + "sourceColumn": "Size", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SizeRange", + "dataType": "string", + "sourceColumn": "SizeRange", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Weight", + "dataType": "double", + "sourceColumn": "Weight", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DaysToManufacture", + "dataType": "int64", + "sourceColumn": "DaysToManufacture", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductLine", + "dataType": "string", + "sourceColumn": "ProductLine", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DealerPrice", + "dataType": "decimal", + "sourceColumn": "DealerPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Class", + "dataType": "string", + "sourceColumn": "Class", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Style", + "dataType": "string", + "sourceColumn": "Style", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ModelName", + "dataType": "string", + "sourceColumn": "ModelName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishDescription", + "dataType": "string", + "sourceColumn": "EnglishDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchDescription", + "dataType": "string", + "sourceColumn": "FrenchDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ChineseDescription", + "dataType": "string", + "sourceColumn": "ChineseDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ArabicDescription", + "dataType": "string", + "sourceColumn": "ArabicDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "HebrewDescription", + "dataType": "string", + "sourceColumn": "HebrewDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ThaiDescription", + "dataType": "string", + "sourceColumn": "ThaiDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "GermanDescription", + "dataType": "string", + "sourceColumn": "GermanDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "JapaneseDescription", + "dataType": "string", + "sourceColumn": "JapaneseDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TurkishDescription", + "dataType": "string", + "sourceColumn": "TurkishDescription", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StartDate", + "dataType": "dateTime", + "sourceColumn": "StartDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EndDate", + "dataType": "dateTime", + "sourceColumn": "EndDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Status", + "dataType": "string", + "sourceColumn": "Status", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimProduct", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimProduct].* FROM [dbo].[DimProduct] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimProduct].* FROM [dbo].[DimProduct] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimProduct" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimSalesTerritory", + "columns": [ + { + "name": "SalesTerritoryKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryAlternateKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryAlternateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryRegion", + "dataType": "string", + "sourceColumn": "SalesTerritoryRegion", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryCountry", + "dataType": "string", + "sourceColumn": "SalesTerritoryCountry", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryGroup", + "dataType": "string", + "sourceColumn": "SalesTerritoryGroup", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimSalesTerritory", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimSalesTerritory].* FROM [dbo].[DimSalesTerritory] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimSalesTerritory].* FROM [dbo].[DimSalesTerritory] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimSalesTerritory" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "FactInternetSales", + "columns": [ + { + "name": "ProductKey", + "dataType": "int64", + "sourceColumn": "ProductKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderDateKey", + "dataType": "int64", + "sourceColumn": "OrderDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DueDateKey", + "dataType": "int64", + "sourceColumn": "DueDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ShipDateKey", + "dataType": "int64", + "sourceColumn": "ShipDateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerKey", + "dataType": "int64", + "sourceColumn": "CustomerKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "PromotionKey", + "dataType": "int64", + "sourceColumn": "PromotionKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CurrencyKey", + "dataType": "int64", + "sourceColumn": "CurrencyKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesTerritoryKey", + "dataType": "int64", + "sourceColumn": "SalesTerritoryKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesOrderNumber", + "dataType": "string", + "sourceColumn": "SalesOrderNumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesOrderLineNumber", + "dataType": "int64", + "sourceColumn": "SalesOrderLineNumber", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "RevisionNumber", + "dataType": "int64", + "sourceColumn": "RevisionNumber", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderQuantity", + "dataType": "int64", + "sourceColumn": "OrderQuantity", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "UnitPrice", + "dataType": "decimal", + "sourceColumn": "UnitPrice", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ExtendedAmount", + "dataType": "decimal", + "sourceColumn": "ExtendedAmount", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "UnitPriceDiscountPct", + "dataType": "double", + "sourceColumn": "UnitPriceDiscountPct", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DiscountAmount", + "dataType": "double", + "sourceColumn": "DiscountAmount", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ProductStandardCost", + "dataType": "decimal", + "sourceColumn": "ProductStandardCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TotalProductCost", + "dataType": "decimal", + "sourceColumn": "TotalProductCost", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SalesAmount", + "dataType": "decimal", + "sourceColumn": "SalesAmount", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "TaxAmt", + "dataType": "decimal", + "sourceColumn": "TaxAmt", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "Freight", + "dataType": "decimal", + "sourceColumn": "Freight", + "formatString": "\\$#,0.00;(\\$#,0.00);\\$#,0.00", + "sourceProviderType": "Currency", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CarrierTrackingNumber", + "dataType": "string", + "sourceColumn": "CarrierTrackingNumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CustomerPONumber", + "dataType": "string", + "sourceColumn": "CustomerPONumber", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "OrderDate", + "dataType": "dateTime", + "sourceColumn": "OrderDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DueDate", + "dataType": "dateTime", + "sourceColumn": "DueDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "ShipDate", + "dataType": "dateTime", + "sourceColumn": "ShipDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "FactInternetSales", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[FactInternetSales].* FROM [dbo].[FactInternetSales] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[FactInternetSales].* FROM [dbo].[FactInternetSales] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "FactInternetSales" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimCurrency", + "columns": [ + { + "name": "CurrencyKey", + "dataType": "int64", + "sourceColumn": "CurrencyKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CurrencyAlternateKey", + "dataType": "string", + "sourceColumn": "CurrencyAlternateKey", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CurrencyName", + "dataType": "string", + "sourceColumn": "CurrencyName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimCurrency", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimCurrency].* FROM [dbo].[DimCurrency] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimCurrency].* FROM [dbo].[DimCurrency] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimCurrency" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimPromotion", + "columns": [ + { + "name": "PromotionKey", + "dataType": "int64", + "sourceColumn": "PromotionKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "PromotionAlternateKey", + "dataType": "int64", + "sourceColumn": "PromotionAlternateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionName", + "dataType": "string", + "sourceColumn": "EnglishPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionName", + "dataType": "string", + "sourceColumn": "SpanishPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionName", + "dataType": "string", + "sourceColumn": "FrenchPromotionName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DiscountPct", + "dataType": "double", + "sourceColumn": "DiscountPct", + "sourceProviderType": "Double", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionType", + "dataType": "string", + "sourceColumn": "EnglishPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionType", + "dataType": "string", + "sourceColumn": "SpanishPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionType", + "dataType": "string", + "sourceColumn": "FrenchPromotionType", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishPromotionCategory", + "dataType": "string", + "sourceColumn": "EnglishPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishPromotionCategory", + "dataType": "string", + "sourceColumn": "SpanishPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchPromotionCategory", + "dataType": "string", + "sourceColumn": "FrenchPromotionCategory", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "StartDate", + "dataType": "dateTime", + "sourceColumn": "StartDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EndDate", + "dataType": "dateTime", + "sourceColumn": "EndDate", + "formatString": "General Date", + "sourceProviderType": "DBTimeStamp", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MinQty", + "dataType": "int64", + "sourceColumn": "MinQty", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MaxQty", + "dataType": "int64", + "sourceColumn": "MaxQty", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimPromotion", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimPromotion].* FROM [dbo].[DimPromotion] " + } + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimPromotion].* FROM [dbo].[DimPromotion] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimPromotion" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + }, + { + "name": "DimDate", + "columns": [ + { + "name": "DateKey", + "dataType": "int64", + "sourceColumn": "DateKey", + "sourceProviderType": "Integer", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FullDateAlternateKey", + "dataType": "dateTime", + "sourceColumn": "FullDateAlternateKey", + "formatString": "General Date", + "sourceProviderType": "DBDate", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfWeek", + "dataType": "int64", + "sourceColumn": "DayNumberOfWeek", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishDayNameOfWeek", + "dataType": "string", + "sourceColumn": "EnglishDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishDayNameOfWeek", + "dataType": "string", + "sourceColumn": "SpanishDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchDayNameOfWeek", + "dataType": "string", + "sourceColumn": "FrenchDayNameOfWeek", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfMonth", + "dataType": "int64", + "sourceColumn": "DayNumberOfMonth", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "DayNumberOfYear", + "dataType": "int64", + "sourceColumn": "DayNumberOfYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "WeekNumberOfYear", + "dataType": "int64", + "sourceColumn": "WeekNumberOfYear", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "EnglishMonthName", + "dataType": "string", + "sourceColumn": "EnglishMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "SpanishMonthName", + "dataType": "string", + "sourceColumn": "SpanishMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FrenchMonthName", + "dataType": "string", + "sourceColumn": "FrenchMonthName", + "sourceProviderType": "WChar", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "MonthNumberOfYear", + "dataType": "int64", + "sourceColumn": "MonthNumberOfYear", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarQuarter", + "dataType": "int64", + "sourceColumn": "CalendarQuarter", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarYear", + "dataType": "int64", + "sourceColumn": "CalendarYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "CalendarSemester", + "dataType": "int64", + "sourceColumn": "CalendarSemester", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalQuarter", + "dataType": "int64", + "sourceColumn": "FiscalQuarter", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalYear", + "dataType": "int64", + "sourceColumn": "FiscalYear", + "sourceProviderType": "SmallInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + }, + { + "name": "FiscalSemester", + "dataType": "int64", + "sourceColumn": "FiscalSemester", + "sourceProviderType": "UnsignedTinyInt", + "annotations": [ + { + "name": "Format", + "value": "" + } + ] + } + ], + "partitions": [ + { + "name": "DimDate", + "dataView": "full", + "source": { + "dataSource": "SqlServer localhost AdventureWorksDW2014", + "query": " SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] " + } + } + ], + "hierarchies": [ + { + "name": "Calendar", + "levels": [ + { + "name": "CalendarYear", + "ordinal": 0, + "column": "CalendarYear" + }, + { + "name": "EnglishMonthName", + "ordinal": 1, + "column": "EnglishMonthName" + }, + { + "name": "DateKey", + "ordinal": 2, + "column": "DateKey" + } + ] + } + ], + "annotations": [ + { + "name": "_TM_ExtProp_QueryDefinition", + "value": " SELECT [dbo].[DimDate].* FROM [dbo].[DimDate] " + }, + { + "name": "_TM_ExtProp_DbTableName", + "value": "DimDate" + }, + { + "name": "_TM_ExtProp_DbSchemaName", + "value": "dbo" + } + ] + } + ], + "relationships": [ + { + "name": "e8247ff5-7756-4439-9b14-b840782b2991", + "toTable": "DimCustomer", + "toColumn": "CustomerKey", + "fromTable": "FactInternetSales", + "fromColumn": "CustomerKey" + }, + { + "name": "efaeafff-93c4-42fd-8682-b2fe1f9b68f1", + "toTable": "DimProduct", + "toColumn": "ProductKey", + "fromTable": "FactInternetSales", + "fromColumn": "ProductKey" + }, + { + "name": "b91c8435-2504-460c-8c8e-dccf07f7afcf", + "toTable": "DimDate", + "toColumn": "DateKey", + "fromColumn": "DueDateKey", + "fromTable": "FactInternetSales", + "isActive": false + }, + { + "name": "f66d522a-17f1-42b6-83c6-1e0736511d97", + "toTable": "DimCurrency", + "toColumn": "CurrencyKey", + "fromTable": "FactInternetSales", + "fromColumn": "CurrencyKey" + }, + { + "name": "2036eea3-2630-464a-9272-bea5c306d2bf", + "toTable": "DimPromotion", + "toColumn": "PromotionKey", + "fromTable": "FactInternetSales", + "fromColumn": "PromotionKey" + }, + { + "name": "7a9b0a33-4b7a-47b9-b418-d00e7d2c9db3", + "toTable": "DimSalesTerritory", + "toColumn": "SalesTerritoryKey", + "fromTable": "FactInternetSales", + "fromColumn": "SalesTerritoryKey" + }, + { + "name": "479c958b-8d46-497f-9ec8-52925832633c", + "toTable": "DimDate", + "toColumn": "DateKey", + "fromTable": "FactInternetSales", + "fromColumn": "ShipDateKey" + }, + { + "name": "9dc6d42e-d1dd-47ee-99bb-66e6415b9853", + "toTable": "DimDate", + "toColumn": "DateKey", + "fromColumn": "OrderDateKey", + "fromTable": "FactInternetSales", + "isActive": false + } + ], + "cultures": [ + { + "name": "pt-BR", + "translations": { + "model": { + "name": "Model", + "tables": [ + { + "name": "DimPromotion", + "translatedDescription": "MuitoBom", + "translatedCaption": "MuitoBom", + "columns": [ + { + "name": "PromotionKey", + "translatedCaption": "MuitoBom" + } + ] + }, + { + "name": "DimDate", + "hierarchies": [ + { + "name": "Calendar", + "translatedCaption": "MuitoBom" + } + ] + } + ], + "perspectives": [ + { + "name": "Sales Perspective", + "translatedDescription": "MuitoBom", + "translatedCaption": "MuitoBom" + } + ] + } + } + } + ], + "perspectives": [ + { + "name": "Sales Perspective", + "tables": [ + { + "name": "DimDate", + "hierarchies": [ + { + "name": "Calendar" + } + ] + }, + { + "name": "DimSalesTerritory", + "columns": [ + { + "name": "SalesTerritoryAlternateKey" + }, + { + "name": "SalesTerritoryCountry" + }, + { + "name": "SalesTerritoryGroup" + }, + { + "name": "SalesTerritoryKey" + }, + { + "name": "SalesTerritoryRegion" + } + ] + }, + { + "name": "FactInternetSales" + } + ] + } + ] + } + } + } +} + diff --git a/BismNormalizer/BismNormalizer.Tests/TestPrep.ps1 b/BismNormalizer/BismNormalizer.Tests/TestPrep.ps1 new file mode 100644 index 0000000..65d5a0b --- /dev/null +++ b/BismNormalizer/BismNormalizer.Tests/TestPrep.ps1 @@ -0,0 +1,46 @@ +Function script:deletedb($databasename) +{ + $dbstring=$server.Databases |select-object name| select-string -simplematch $databasename + + if ($dbstring) + { + $db=$server.databases.item($databasename) + $db.drop() + Write-host "Deleted " $databasename + } + else + { + Write-host "Database " $databasename " DOES NOT exist" + } +} + +[Reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices") >$NULL + +$server = New-Object Microsoft.AnalysisServices.Server +$server.connect("localhost") + +deletedb "Test1103_Source" +deletedb "Test1103_Target" +deletedb "Test1200_Source" +deletedb "Test1200_Target" + +if (!($args.Length -eq 1 -and $args[0] -eq "-DeleteOnly")) +{ + $scriptcontent = Get-Content .\Test1103_Source.xmla + $server.Execute($scriptcontent) + Write-host "Created Test1103_Source" + $scriptcontent = Get-Content .\Test1103_Target.xmla + $server.Execute($scriptcontent) + Write-host "Created Test1103_Target" + $scriptcontent = Get-Content .\Test1200_Source.xmla + $server.Execute($scriptcontent) + Write-host "Created Test1200_Source" + $scriptcontent = Get-Content .\Test1200_Target.xmla + $server.Execute($scriptcontent) + Write-host "Created Test1200_Target" +} + +# Clean up +$server.Disconnect() +Write-host "" +Write-host "Disconnected" diff --git a/BismNormalizer/BismNormalizer.sln b/BismNormalizer/BismNormalizer.sln new file mode 100644 index 0000000..6cc762b --- /dev/null +++ b/BismNormalizer/BismNormalizer.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BismNormalizer", "BismNormalizer\BismNormalizer.csproj", "{E54D1347-06AE-41AC-A750-5BF8ECC80EC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BismNormalizer.CommandLine", "BismNormalizer.CommandLine\BismNormalizer.CommandLine.csproj", "{4C77E665-FA37-4793-8950-69AABD3DC626}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BismNormalizer.IconSetup", "BismNormalizer.IconSetup\BismNormalizer.IconSetup.csproj", "{849693FC-AD82-4323-8A96-D6A0F6D97566}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BismNormalizer.Tests", "BismNormalizer.Tests\BismNormalizer.Tests.csproj", "{3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E54D1347-06AE-41AC-A750-5BF8ECC80EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E54D1347-06AE-41AC-A750-5BF8ECC80EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E54D1347-06AE-41AC-A750-5BF8ECC80EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E54D1347-06AE-41AC-A750-5BF8ECC80EC5}.Release|Any CPU.Build.0 = Release|Any CPU + {4C77E665-FA37-4793-8950-69AABD3DC626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C77E665-FA37-4793-8950-69AABD3DC626}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C77E665-FA37-4793-8950-69AABD3DC626}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C77E665-FA37-4793-8950-69AABD3DC626}.Release|Any CPU.Build.0 = Release|Any CPU + {849693FC-AD82-4323-8A96-D6A0F6D97566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {849693FC-AD82-4323-8A96-D6A0F6D97566}.Debug|Any CPU.Build.0 = Debug|Any CPU + {849693FC-AD82-4323-8A96-D6A0F6D97566}.Release|Any CPU.ActiveCfg = Release|Any CPU + {849693FC-AD82-4323-8A96-D6A0F6D97566}.Release|Any CPU.Build.0 = Release|Any CPU + {3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AE5D06F-A054-4AD1-83A9-9F0B8B1FC0B4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/BismNormalizer/BismNormalizer/BismNormalizer.csproj b/BismNormalizer/BismNormalizer/BismNormalizer.csproj new file mode 100644 index 0000000..c1277a6 --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizer.csproj @@ -0,0 +1,524 @@ + + + + 15.0 + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + SAK + SAK + SAK + SAK + + + + + 12.0 + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + false + + + + Debug + AnyCPU + 2.0 + {E54D1347-06AE-41AC-A750-5BF8ECC80EC5} + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + BismNormalizer + BismNormalizer + true + Key.snk + v4.6.1 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + true + + + + + true + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + true + + + + False + + + False + + + False + C:\Program Files (x86)\Microsoft SQL Server\140\SDK\Assemblies\Microsoft.AnalysisServices.DLL + + + False + C:\Program Files (x86)\Microsoft SQL Server\140\SDK\Assemblies\Microsoft.AnalysisServices.Core.DLL + + + False + C:\Program Files (x86)\Microsoft SQL Server\140\SDK\Assemblies\Microsoft.AnalysisServices.Tabular.DLL + + + False + C:\Program Files (x86)\Microsoft SQL Server\140\SDK\Assemblies\Microsoft.AnalysisServices.Tabular.Json.DLL + + + + ..\packages\Microsoft.Office.Interop.Excel.15.0.4795.1000\lib\net20\Microsoft.Office.Interop.Excel.dll + True + + + True + + + ..\packages\Microsoft.VisualStudio.Imaging.14.3.25407\lib\net45\Microsoft.VisualStudio.Imaging.dll + True + + + ..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Immutable.10.0.10.0.30319\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Immutable.12.0.12.0.21003\lib\net45\Microsoft.VisualStudio.Shell.Immutable.12.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll + True + + + ..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll + True + + + + true + + + true + + + ..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll + True + + + ..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + + + ..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll + True + + + ..\packages\Microsoft.VisualStudio.Threading.14.1.111\lib\net45\Microsoft.VisualStudio.Threading.dll + True + + + ..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll + True + + + ..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + BlobCredentials.cs + + + + + + + + + + + + + + + + + + + + + + + + + UserControl + + + ComparisonControl.cs + + + + + + + + Form + + + Deployment.cs + + + Form + + + ProcessingErrorMessage.cs + + + Form + + + ImpersonationCredentials.cs + + + Settings.settings + True + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Component + + + + + + + + + + Form + + + Connections.cs + + + + + + Component + + + EditorTextBox.cs + + + + Form + + + Options.cs + + + + UserControl + + + ValidationOutput.cs + + + UserControl + + + ValidationOutputButton.cs + + + UserControl + + + ValidationOutputButtons.cs + + + True + True + VSPackage.resx + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + true + Always + + + + + + + + + + + + + + + Designer + + + Designer + + + + + + + + true + Always + + + true + Always + + + + True + True + Resources.resx + + + + + + + + + ComparisonControl.cs + Designer + + + Connections.cs + + + Deployment.cs + + + EditorPane.cs + Designer + + + EditorTextBox.cs + Designer + + + BlobCredentials.cs + + + ProcessingErrorMessage.cs + + + ImpersonationCredentials.cs + + + Options.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ValidationOutput.cs + + + ValidationOutputButton.cs + + + ValidationOutputButtons.cs + + + true + VSPackage + Designer + ResXFileCodeGenerator + VSPackage.Designer.cs + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + Designer + + + + + + + + Menus.ctmenu + + + + + + + + + Always + true + + + + + False + Microsoft .NET Framework 4.5.2 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + true + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/BismNormalizer.obproj.map b/BismNormalizer/BismNormalizer/BismNormalizer.obproj.map new file mode 100644 index 0000000..f6152b5 --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizer.obproj.map @@ -0,0 +1,4130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/BismNormalizer.shfbproj b/BismNormalizer/BismNormalizer/BismNormalizer.shfbproj new file mode 100644 index 0000000..12ff0ad --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizer.shfbproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + 2.0 + {6ca43b3a-3a45-4c6a-9e5f-1a4e7a45d9f2} + 2015.6.5.0 + + Documentation + Documentation + Documentation + + .NET Framework 4.5 + .\Help\ + Documentation + en-US + + + + + 100 + OnlyWarningsAndErrors + Website + False + True + False + True + + Provides an object API used to perform database comparison and merging for SQL Server Analysis Services tabular models. +Provides abstract classes and interfaces to be inherited or implemented by classes in the BismNormalizer.TabularCompare.MultidimensionalMetadata and BismNormalizer.TabularCompare.TabularMetadata namespaces. +Provides classes for comparisons of tabular models that use tabular metadata (not multidimensional) with compatibility level 1200. + +Provides classes for comparisons of tabular models that use multidimensional metadata (not tabular) with compatibility level 1100 and 1103. + + -1 + 100 + 100 + -1 + BISM Normalizer + BISM Normalizer + Msdn + 100 + VS + 1.0.0.0 + 2 + False + C# + Blank + False + VS2013 + False + Guid + BISM Normalizer API Reference + AboveNamespaces + InheritedMembers, InheritedFrameworkMembers, ProtectedInternalAsProtected + + + + + + + + + + + + + + + + + + + + + + + + + + + OnBuildSuccess + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/BismNormalizer.targets b/BismNormalizer/BismNormalizer/BismNormalizer.targets new file mode 100644 index 0000000..c8b8f99 --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizer.targets @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + @(SolutionDir->'%(Fullpath)') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\bin\ReleaseObfusc + $(ObfuscDir)\Extract + $(ObfuscDir)\BismNormalizer.dll + $(ObfuscDir)\BismNormalizer.vsix + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BismNormalizer/BismNormalizer/BismNormalizer.vsct b/BismNormalizer/BismNormalizer/BismNormalizer.vsct new file mode 100644 index 0000000..140adb5 --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizer.vsct @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BismNormalizer/BismNormalizer/BismNormalizerPackage.cs b/BismNormalizer/BismNormalizer/BismNormalizerPackage.cs new file mode 100644 index 0000000..d142314 --- /dev/null +++ b/BismNormalizer/BismNormalizer/BismNormalizerPackage.cs @@ -0,0 +1,536 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; +using System.ComponentModel.Design; +using Microsoft.Win32; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; +using EnvDTE80; +using BismNormalizer.TabularCompare.UI; +using System.Drawing; + +namespace BismNormalizer +{ + [PackageRegistration(UseManagedResourcesOnly = true)] + [InstalledProductRegistration("#110", "#112", "3", IconResourceID = 400)] + [ProvideMenuResource("Menus.ctmenu", 1)] + [ProvideToolWindow(typeof(WarningList), + Style = VsDockStyle.Tabbed, + Transient = true, //Transient means will not show up automatically when reopen VS + Window = "D78612C7-9962-4B83-95D9-268046DAD23A" //this is the guid of the VS error window (NOT YOUR CUSTOM WINDOW) + )] + [ProvideEditorExtension(typeof(EditorFactory), ".bsmn", 50, + ProjectGuid = VSConstants.CLSID.MiscellaneousFilesProject_string, + TemplateDir = "Templates", + NameResourceID = 105, + DefaultName = "BismNormalizer")] + [ProvideEditorExtension(typeof(EditorFactory), ".bsmn", 1000, + ProjectGuid = "{6870E480-7721-4708-BFB8-9AE898AA21B3}", //GUID for tabular BI projects + TemplateDir = "Templates", + NameResourceID = 105, + DefaultName = "BismNormalizer")] + [ProvideKeyBindingTable(GuidList.guidBismNormalizerEditorFactoryString, 102)] + [ProvideEditorLogicalView(typeof(EditorFactory), VSConstants.LOGVIEWID.Any_string)] //VSConstants.LOGVIEWID.TextView_string)] + [ProvideAutoLoad(UIContextGuids.SolutionHasSingleProject)] //Microsoft.VisualStudio.VSConstants.UICONTEXT.NoSolution_string + [Guid(GuidList.guidBismNormalizerPkgString)] + public sealed class BismNormalizerPackage : Package, IDisposable + { + private DTE2 _dte; + private ValidationOutput _validationOutput; + private List _editorPanes; + private DteInitializer _dteInitializer; + private IVsWindowFrame _toolWindowFrame; + private EditorFactory _editorFactory; + + public BismNormalizerPackage() { } + + protected override void Initialize() + { + base.Initialize(); + InitializeDTE(); + + _editorPanes = new List(); + + base.RegisterEditorFactory(new EditorFactory(this)); + + OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService; + if ( null != mcs ) + { + //Command for New Comparison from Tools menu + CommandID menuToolMenuNewComparisonCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidToolMenuNewComparison); + MenuCommand menuToolMenuNewComparison = new MenuCommand(NewComparison, menuToolMenuNewComparisonCommandID ); + mcs.AddCommand( menuToolMenuNewComparison ); + + //Command for New Comparison from project context menu in solution explorer + CommandID menuProjectMenuNewComparisonCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidProjectMenuNewComparison); + OleMenuCommand menuProjectMenuNewComparison = new OleMenuCommand(AddNewComparison, menuProjectMenuNewComparisonCommandID); + menuProjectMenuNewComparison.BeforeQueryStatus += menuItem_BeforeQueryStatusNewComparison; + mcs.AddCommand(menuProjectMenuNewComparison); + + //Command for View Code from file context menu in solution explorer + CommandID menuFileMenuViewCodeCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidFileMenuViewCode); + OleMenuCommand menuFileMenuViewCode = new OleMenuCommand(ViewCode, menuFileMenuViewCodeCommandID); + menuFileMenuViewCode.BeforeQueryStatus += menuItem_BeforeQueryStatusCodeBehind; + mcs.AddCommand(menuFileMenuViewCode); + + //Command for View Designer from file context menu in solution explorer + CommandID menuFileMenuViewDesignerCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidFileMenuViewDesigner); + OleMenuCommand menuFileMenuViewDesigner = new OleMenuCommand(ViewDesigner, menuFileMenuViewDesignerCommandID); + menuFileMenuViewDesigner.BeforeQueryStatus += menuItem_BeforeQueryStatusCodeBehind; + mcs.AddCommand(menuFileMenuViewDesigner); + + //Command for View Designer from file context menu in solution explorer + CommandID menuFileMenuInstallIconCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidFileMenuInstallIcon); + OleMenuCommand menuFileMenuInstallIcon = new OleMenuCommand(InstallIcon, menuFileMenuInstallIconCommandID); + menuFileMenuInstallIcon.BeforeQueryStatus += menuItem_BeforeQueryStatusInstallIcon; + mcs.AddCommand(menuFileMenuInstallIcon); + + //Command for BISM Normalizer Warning List + CommandID toolwndCommandID = new CommandID(GuidList.guidBismNormalizerCmdSet, (int)PkgCmdIDList.cmdidValidationOutput); + MenuCommand menuValidationOutput = new MenuCommand(InitializeToolWindow, toolwndCommandID); + mcs.AddCommand( menuValidationOutput ); + } + } + + private void NewComparison(object sender, EventArgs e) + { + try + { + _dte.ItemOperations.NewFile(@"BISM Normalizer\Tabular Compare"); + } + catch (Exception) + { + ShowMessage("Cannot launch BISM Normalizer. Please check installation, or try creating a new text file with .bsmn extension.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_CRITICAL); + } + } + + private void AddNewComparison(object sender, EventArgs e) + { + try + { + EnvDTE.ProjectItem projItem = _dte.ItemOperations.AddNewItem(@"BISM Normalizer\Tabular Compare"); + //Can't use while can't change name of file while editor open: + //_dte.ItemOperations.OpenFile(projItem.FileNames[0]); + } + catch (Exception) + { + ShowMessage("Cannot add BISM Normalizer comparison. Please check installation, or try creating a new text file with .bsmn extension.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_CRITICAL); + } + } + + private void ViewCode(object sender, EventArgs e) + { + try + { + OleMenuCommand menuCommand = sender as OleMenuCommand; + + if (menuCommand != null) + { + menuCommand.Visible = false; // default to not visible + + if (_dte != null && _dte.SelectedItems != null && _dte.SelectedItems.Count == 1) //only support 1 selected file + { + foreach (EnvDTE.SelectedItem selectedItem in _dte.SelectedItems) + { + if (selectedItem.Name != null && + selectedItem.Name.ToUpper().EndsWith(".bsmn".ToUpper()) && + selectedItem.ProjectItem != null + ) + { + EnvDTE.ProjectItem projItem = selectedItem.ProjectItem; + _dte.ItemOperations.OpenFile(projItem.FileNames[0], EnvDTE.Constants.vsViewKindCode); + break; + } + } + } + } + } + catch (Exception) + { + ShowMessage("Cannot view code. Please check installation.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_CRITICAL); + } + } + + private void ViewDesigner(object sender, EventArgs e) + { + try + { + OleMenuCommand menuCommand = sender as OleMenuCommand; + + if (menuCommand != null) + { + menuCommand.Visible = false; // default to not visible + + if (_dte != null && _dte.SelectedItems != null && _dte.SelectedItems.Count == 1) //only support 1 selected file + { + foreach (EnvDTE.SelectedItem selectedItem in _dte.SelectedItems) + { + if (selectedItem.Name != null && + selectedItem.Name.ToUpper().EndsWith(".bsmn".ToUpper()) && + selectedItem.ProjectItem != null + ) + { + EnvDTE.ProjectItem projItem = selectedItem.ProjectItem; + _dte.ItemOperations.OpenFile(projItem.FileNames[0], EnvDTE.Constants.vsViewKindPrimary); + break; + } + } + } + } + } + catch (Exception) + { + ShowMessage("Cannot view designer. Please check installation.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_CRITICAL); + } + } + + private void InstallIcon(object sender, EventArgs e) + { + string message = "Setting up icon for Solution Explorer will require running a separate process as administrator."; + IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + Guid clsid = Guid.Empty; + int result; + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox( + 0, + ref clsid, + "Bism Normalizer", + message, + string.Empty, + 0, + OLEMSGBUTTON.OLEMSGBUTTON_OKCANCEL, + OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, + OLEMSGICON.OLEMSGICON_WARNING, + 0, // false + out result)); + if (result != 1) + { + // If !=OK then backout + return; + } + + try + { + ProcessStartInfo proc = new ProcessStartInfo(); + proc.UseShellExecute = true; + string workingDirectory = System.Reflection.Assembly.GetExecutingAssembly().Location.Replace("\\BismNormalizer.dll", ""); + proc.WorkingDirectory = workingDirectory; + proc.FileName = workingDirectory + "\\BismNormalizer.IconSetup.exe"; + proc.Verb = "runas"; + Process.Start(proc); + } + catch (Exception exc) + { + ShowMessage(exc.Message, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_CRITICAL); + } + } + + void menuItem_BeforeQueryStatusNewComparison(object sender, EventArgs e) + { + OleMenuCommand menuCommand = sender as OleMenuCommand; + + if (menuCommand != null) + { + menuCommand.Visible = false; // default to not visible + + if (_dte != null) + { + Array selectedProjects = (Array)_dte.ActiveSolutionProjects; + + //only support 1 selected project + if (selectedProjects.Length == 1) + { + EnvDTE.Project project = (EnvDTE.Project)selectedProjects.GetValue(0); + + if (project.FullName.EndsWith(".smproj")) + { + menuCommand.Visible = true; + } + } + } + } + } + + void menuItem_BeforeQueryStatusCodeBehind(object sender, EventArgs e) + { + OleMenuCommand menuCommand = sender as OleMenuCommand; + + if (menuCommand != null) + { + menuCommand.Visible = false; // default to not visible + + if (_dte != null && _dte.SelectedItems != null && _dte.SelectedItems.Count == 1) //only support 1 selected file + { + foreach (EnvDTE.SelectedItem selectedItem in _dte.SelectedItems) + { + if (selectedItem.Name != null && + selectedItem.Name.ToUpper().EndsWith(".bsmn".ToUpper()) + ) + { + menuCommand.Visible = true; + } + } + } + } + } + + void menuItem_BeforeQueryStatusInstallIcon(object sender, EventArgs e) + { + OleMenuCommand menuCommand = sender as OleMenuCommand; + + if (menuCommand != null) + { + menuCommand.Visible = false; // default to not visible + + if (_dte != null && _dte.SelectedItems != null && _dte.SelectedItems.Count == 1) //only support 1 selected file + { + foreach (EnvDTE.SelectedItem selectedItem in _dte.SelectedItems) + { + if (selectedItem.Name != null && + selectedItem.Name.ToUpper().EndsWith(".bsmn".ToUpper()) + ) + { + //Check if icon already installed and .bsmn files are associated with VS + try + { + if (Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.bsmn\\UserChoice", false) == null) + { + menuCommand.Visible = true; + } + } + catch { } + } + } + } + } + } + + public void ShowMessage(string message, OLEMSGBUTTON msgButton, OLEMSGICON msgIcon) + { + IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + Guid clsid = Guid.Empty; + int result; + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox( + 0, + ref clsid, + "Bism Normalizer", + string.Format(CultureInfo.CurrentCulture, message, this.ToString()), + string.Empty, + 0, + msgButton, + OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, + msgIcon, + 0, // false + out result)); + } + + private void InitializeToolWindow(object sender, EventArgs e) + { + InitializeToolWindowInternal(); + } + + internal void InitializeToolWindowInternal(float dpiFactor = 0) + { + ToolWindowPane window = this.FindToolWindow(typeof(WarningList), 0, true); + if ((null == window) || (null == window.Frame)) + { + throw new NotSupportedException(Resources.CanNotCreateWindow); + } + _validationOutput = (ValidationOutput)window.Window; + if (dpiFactor != 0) + { + _validationOutput.Rescale(dpiFactor); + } + _toolWindowFrame = (IVsWindowFrame)window.Frame; + ShowToolWindow(); + } + + public void ShowToolWindow() + { + if (_toolWindowFrame != null) + { + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(_toolWindowFrame.Show()); + } + } + + private void InitializeDTE() + { + IVsShell shellService; + + this._dte = this.GetService(typeof(SDTE)) as DTE2; + + if (this._dte == null) // The IDE is not yet fully initialized + { + shellService = this.GetService(typeof(SVsShell)) as IVsShell; + this._dteInitializer = new DteInitializer(shellService, this.InitializeDTE); + } + else + { + this._dteInitializer = null; + _documentEvents = ((EnvDTE80.Events2)_dte.Events).get_DocumentEvents(); + _documentEvents.DocumentOpened += new EnvDTE._dispDocumentEvents_DocumentOpenedEventHandler(DocumentEvents_DocumentOpened); + + //Unfortunately, this does not fire for tabular projects - only regular C# type projects + _projectItemEvents = ((EnvDTE80.Events2)_dte.Events).ProjectItemsEvents; + _projectItemEvents.ItemRenamed += _projectItemEvents_ItemRenamed; + } + } + + public DTE2 Dte => this._dte; + + public ValidationOutput ValidationOutput + { + get + { + return this._validationOutput; + } + set + { + this._validationOutput = value; + } + } + + public List EditorPanes => _editorPanes; + + #region IDisposable Pattern + /// + /// Releases the resources used by the Package object. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases the resources used by the Package object. + /// + /// This parameter determines whether the method has been called directly or indirectly by a user's code. + protected override void Dispose(bool disposing) + { + try + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Dispose() of: {0}", this.ToString())); + if (disposing) + { + if (_editorFactory != null) + { + _editorFactory.Dispose(); + _editorFactory = null; + } + GC.SuppressFinalize(this); + } + } + finally + { + base.Dispose(disposing); + } + } + #endregion + + EnvDTE.DocumentEvents _documentEvents; + void DocumentEvents_DocumentOpened(EnvDTE.Document document) + { + try + { + if (document.FullName.EndsWith(".bim")) + { + string message = ""; + foreach (EditorPane editorPane in EditorPanes) + { + if (editorPane.BismNormalizerForm != null && editorPane.BismNormalizerForm.CompareState != CompareState.NotCompared) + { + // check if open diff has project that contains BIM file being opened. + if (editorPane.BismNormalizerForm.ComparisonInfo.ConnectionInfoSource.UseProject) + { + foreach (EnvDTE.ProjectItem projectItem in editorPane.BismNormalizerForm.ComparisonInfo.ConnectionInfoSource.Project.ProjectItems) + { + if (projectItem.Document != null && projectItem.Document.FullName == document.FullName) + { + editorPane.BismNormalizerForm.SetNotComparedState(); + message += " - " + editorPane.Name + "\n"; + break; + } + } + } + if (editorPane.BismNormalizerForm.CompareState != CompareState.NotCompared && editorPane.BismNormalizerForm.ComparisonInfo.ConnectionInfoTarget.UseProject) + { + foreach (EnvDTE.ProjectItem projectItem in editorPane.BismNormalizerForm.ComparisonInfo.ConnectionInfoTarget.Project.ProjectItems) + { + if (projectItem.Document != null && projectItem.Document.FullName == document.FullName) + { + editorPane.BismNormalizerForm.SetNotComparedState(); + message += " - " + editorPane.Name + "\n"; + break; + } + } + } + } + } + if (message.Length > 0) + { + ShowMessage("Opening this file will invalidate the following comparisons.\n" + message, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_WARNING); + } + } + } + catch { } + } + + EnvDTE.ProjectItemsEvents _projectItemEvents; + void _projectItemEvents_ItemRenamed(EnvDTE.ProjectItem projectItem, string oldName) + { + if (projectItem.IsOpen && oldName.EndsWith(".bsmn") && projectItem.Document != null) + { + this.ShowMessage("Changing file name while editor is open is not supported. File will close now.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_WARNING); + projectItem.Document.Close(); + } + } + } + + public class DteInitializer : IVsShellPropertyEvents + { + private IVsShell shellService; + private uint cookie; + private Action callback; + + public DteInitializer(IVsShell shellService, Action callback) + { + int hr; + + this.shellService = shellService; + this.callback = callback; + + // Set an event handler to detect when the IDE is fully initialized + hr = this.shellService.AdviseShellPropertyChanges(this, out this.cookie); + + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr); + } + + int IVsShellPropertyEvents.OnShellPropertyChange(int propid, object var) + { + int hr; + bool isZombie; + + if (propid == (int)__VSSPROPID.VSSPROPID_Zombie) + { + isZombie = (bool)var; + + if (!isZombie) + { + // Release the event handler to detect when the IDE is fully initialized + hr = this.shellService.UnadviseShellPropertyChanges(this.cookie); + + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr); + + this.cookie = 0; + + this.callback(); + } + } + return VSConstants.S_OK; + } + } + +} diff --git a/BismNormalizer/BismNormalizer/DemoHarness.cs b/BismNormalizer/BismNormalizer/DemoHarness.cs new file mode 100644 index 0000000..97c2575 --- /dev/null +++ b/BismNormalizer/BismNormalizer/DemoHarness.cs @@ -0,0 +1,37 @@ + + +#if DEBUG + +using BismNormalizer.TabularCompare; +using BismNormalizer.TabularCompare.Core; +using System.Diagnostics; + +namespace BismNormalizer +{ + public static class DemoHarness + { + public static void Main() + { + using (Comparison c = ComparisonFactory.CreateComparison("C:\\TabularCompare1.bsmn")) + { + c.Connect(); + c.CompareTabularModels(); + //c.ComparisonObjects + + c.ValidationMessage += HandleValidationMessage; + c.ValidateSelection(); + c.Update(); + + c.Disconnect(); + } + } + + private static void HandleValidationMessage(object sender, ValidationMessageEventArgs e) + { + Debug.WriteLine(e.Message); + } + } +} + +#endif + diff --git a/BismNormalizer/BismNormalizer/EditorFactory.cs b/BismNormalizer/BismNormalizer/EditorFactory.cs new file mode 100644 index 0000000..e6726ab --- /dev/null +++ b/BismNormalizer/BismNormalizer/EditorFactory.cs @@ -0,0 +1,190 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; + +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + +namespace BismNormalizer +{ + /// + /// Factory for creating our editor object. Extends from the IVsEditoryFactory interface + /// + [Guid(GuidList.guidBismNormalizerEditorFactoryString)] + public sealed class EditorFactory : IVsEditorFactory, IDisposable + { + private BismNormalizerPackage editorPackage; + private ServiceProvider vsServiceProvider; + + + public EditorFactory(BismNormalizerPackage package) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering {0} constructor", this.ToString())); + + this.editorPackage = package; + } + + /// + /// Since we create a ServiceProvider which implements IDisposable we + /// also need to implement IDisposable to make sure that the ServiceProvider's + /// Dispose method gets called. + /// + public void Dispose() + { + if (vsServiceProvider != null) + { + vsServiceProvider.Dispose(); + } + } + + #region IVsEditorFactory Members + + /// + /// Used for initialization of the editor in the environment + /// + /// pointer to the service provider. Can be used to obtain instances of other interfaces + /// + /// + public int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider psp) + { + vsServiceProvider = new ServiceProvider(psp); + return VSConstants.S_OK; + } + + public object GetService(Type serviceType) => vsServiceProvider.GetService(serviceType); + + // This method is called by the Environment (inside IVsUIShellOpenDocument:: + // OpenStandardEditor and OpenSpecificEditor) to map a LOGICAL view to a + // PHYSICAL view. A LOGICAL view identifies the purpose of the view that is + // desired (e.g. a view appropriate for Debugging [LOGVIEWID_Debugging], or a + // view appropriate for text view manipulation as by navigating to a find + // result [LOGVIEWID_TextView]). A PHYSICAL view identifies an actual type + // of view implementation that an IVsEditorFactory can create. + // + // NOTE: Physical views are identified by a string of your choice with the + // one constraint that the default/primary physical view for an editor + // *MUST* use a NULL string as its physical view name (*pbstrPhysicalView = NULL). + // + // NOTE: It is essential that the implementation of MapLogicalView properly + // validates that the LogicalView desired is actually supported by the editor. + // If an unsupported LogicalView is requested then E_NOTIMPL must be returned. + // + // NOTE: The special Logical Views supported by an Editor Factory must also + // be registered in the local registry hive. LOGVIEWID_Primary is implicitly + // supported by all editor types and does not need to be registered. + // For example, an editor that supports a ViewCode/ViewDesigner scenario + // might register something like the following: + // HKLM\Software\Microsoft\VisualStudio\\Editors\ + // {...guidEditor...}\ + // LogicalViews\ + // {...LOGVIEWID_TextView...} = s '' + // {...LOGVIEWID_Code...} = s '' + // {...LOGVIEWID_Debugging...} = s '' + // {...LOGVIEWID_Designer...} = s 'Form' + // + public int MapLogicalView(ref Guid rguidLogicalView, out string pbstrPhysicalView) + { + pbstrPhysicalView = null; // initialize out parameter + + // we support only a single physical view + if (VSConstants.LOGVIEWID_Primary == rguidLogicalView) + { + return VSConstants.S_OK; // primary view uses NULL as pbstrPhysicalView + } + else if (VSConstants.LOGVIEWID.TextView_guid == rguidLogicalView) + { + // Our editor supports FindInFiles, therefore we need to declare support for LOGVIEWID_TextView. + // In addition our EditorPane implements IVsCodeWindow and we also provide the + // VSSettings (pkgdef) metadata statement that we support LOGVIEWID_TextView via the following + // attribute on our Package class: + // [ProvideEditorLogicalView(typeof(EditorFactory), VSConstants.LOGVIEWID.TextView_string)] + + pbstrPhysicalView = null; // our primary view implements IVsCodeWindow + return VSConstants.S_OK; + } + else + { + return VSConstants.E_NOTIMPL; // you must return E_NOTIMPL for any unrecognized rguidLogicalView values + } + } + + public int Close() => VSConstants.S_OK; + + /// + /// Used by the editor factory to create an editor instance. the environment first determines the + /// editor factory with the highest priority for opening the file and then calls + /// IVsEditorFactory.CreateEditorInstance. If the environment is unable to instantiate the document data + /// in that editor, it will find the editor with the next highest priority and attempt to so that same + /// thing. + /// NOTE: The priority of our editor is 32 as mentioned in the attributes on the package class. + /// + /// Since our editor supports opening only a single view for an instance of the document data, if we + /// are requested to open document data that is already instantiated in another editor, or even our + /// editor, we return a value VS_E_INCOMPATIBLEDOCDATA. + /// + /// Flags determining when to create the editor. Only open and silent flags + /// are valid + /// + /// path to the file to be opened + /// name of the physical view + /// pointer to the IVsHierarchy interface + /// Item identifier of this editor instance + /// This parameter is used to determine if a document buffer + /// (DocData object) has already been created + /// + /// Pointer to the IUnknown interface for the DocView object + /// Pointer to the IUnknown interface for the DocData object + /// Caption mentioned by the editor for the doc window + /// the Command UI Guid. Any UI element that is visible in the editor has + /// to use this GUID. This is specified in the .vsct file + /// + /// Flags for CreateDocumentWindow + /// + [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public int CreateEditorInstance( + uint grfCreateDoc, + string pszMkDocument, + string pszPhysicalView, + IVsHierarchy pvHier, + uint itemid, + System.IntPtr punkDocDataExisting, + out System.IntPtr ppunkDocView, + out System.IntPtr ppunkDocData, + out string pbstrEditorCaption, + out Guid pguidCmdUI, + out int pgrfCDW) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering {0} CreateEditorInstace()", this.ToString())); + + // Initialize to null + ppunkDocView = IntPtr.Zero; + ppunkDocData = IntPtr.Zero; + pguidCmdUI = GuidList.guidBismNormalizerEditorFactory; + pgrfCDW = 0; + pbstrEditorCaption = null; + + // Validate inputs + if ((grfCreateDoc & (VSConstants.CEF_OPENFILE | VSConstants.CEF_SILENT)) == 0) + { + return VSConstants.E_INVALIDARG; + } + if (punkDocDataExisting != IntPtr.Zero) + { + return VSConstants.VS_E_INCOMPATIBLEDOCDATA; + } + + // Create the Document (editor) + EditorPane NewEditor = new EditorPane(editorPackage); + ppunkDocView = Marshal.GetIUnknownForObject(NewEditor); + ppunkDocData = Marshal.GetIUnknownForObject(NewEditor); + pbstrEditorCaption = ""; + return VSConstants.S_OK; + } + + #endregion + } +} diff --git a/BismNormalizer/BismNormalizer/EditorPane.cs b/BismNormalizer/BismNormalizer/EditorPane.cs new file mode 100644 index 0000000..48e012c --- /dev/null +++ b/BismNormalizer/BismNormalizer/EditorPane.cs @@ -0,0 +1,1150 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.IO; +using System.Drawing; +using System.Globalization; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.TextManager.Interop; +using Microsoft.VisualStudio.Shell; +using EnvDTE; + +using ISysServiceProvider = System.IServiceProvider; +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; +using VSStd97CmdID = Microsoft.VisualStudio.VSConstants.VSStd97CmdID; +using BismNormalizer.TabularCompare.UI; + +namespace BismNormalizer +{ + /// + /// This control host the editor (an extended RichTextBox) and is responsible for + /// handling the commands targeted to the editor as well as saving and loading + /// the document. This control also implement the search and replace functionalities. + /// + + /////////////////////////////////////////////////////////////////////////////// + // Having an entry in the new file dialog. + // + // For our file type should appear under "General" in the new files dialog, we need the following:- + // - A .vsdir file in the same directory as NewFileItems.vsdir (generally under Common7\IDE\NewFileItems). + // In our case the file name is Editor.vsdir but we only require a file with .vsdir extension. + // - An empty bsmn file in the same directory as NewFileItems.vsdir. In + // our case we chose TabularCompare.bsmn. Note this file name appears in Editor.vsdir + // (see vsdir file format below) + // - Three text strings in our language specific resource. File Resources.resx :- + // - "Rich Text file" - this is shown next to our icon. + // - "A blank rich text file" - shown in the description window + // in the new file dialog. + // - "TabularCompare" - This is the base file name. New files will initially + // be named as TabularCompare1.bsmn, TabularCompare2.bsmn... etc. + /////////////////////////////////////////////////////////////////////////////// + // Editor.vsdir contents:- + // TabularCompare.bsmn|{3085E1D6-A938-478e-BE49-3546C09A1AB1}|#106|80|#109|0|401|0|#107 + // + // The fields in order are as follows:- + // - TabularCompare.bsmn - our empty bsmn file + // - {db16ff5e-400a-4cb7-9fde-cb3eab9d22d2} - our Editor package guid + // - #106 - the ID of "Rich Text file" in the resource + // - 80 - the display ordering priority + // - #109 - the ID of "A blank rich text file" in the resource + // - 0 - resource dll string (we don't use this) + // - 401 - the ID of our icon + // - 0 - various flags (we don't use this - se vsshell.idl) + // - #107 - the ID of "bsmn" + /////////////////////////////////////////////////////////////////////////////// + + //This is required for Find In files scenario to work properly. This provides a connection point + //to the event interface + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] + [ComSourceInterfaces(typeof(IVsTextViewEvents))] + [ComVisible(true)] + public sealed class EditorPane : Microsoft.VisualStudio.Shell.WindowPane, + IVsPersistDocData, //to Enable persistence functionality for document data + IPersistFileFormat, //to enable the programmatic loading or saving of an object in a format specified by the user. + IVsFileChangeEvents,//to notify the client when file changes on disk + IVsDocDataFileChangeControl //to Determine whether changes to files made outside of the editor should be ignored + { + private const uint MyFormat = 0; + private const string MyExtension = ".bsmn"; + + private class EditorProperties + { + private EditorPane editor; + public EditorProperties(EditorPane Editor) + { + editor = Editor; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string FileName => editor.FileName; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool DataChanged => editor.DataChanged; + } + + #region Fields + private BismNormalizerPackage bismNormalizerPackage; + + private string fileName = string.Empty; + private bool isDirty; + // Flag true when we are loading the file. It is used to avoid to change the isDirty flag + // when the changes are related to the load operation. + private bool loading; + // This flag is true when we are asking the QueryEditQuerySave service if we can edit the + // file. It is used to avoid to have more than one request queued. + private bool gettingCheckoutStatus; + private ComparisonControl bismNormalizerForm; + private string name; + + private Microsoft.VisualStudio.Shell.SelectionContainer selContainer; + private ITrackSelection trackSel; + private IVsFileChangeEx vsFileChangeEx; + + private Timer FileChangeTrigger = new Timer(); + + private Timer FNFStatusbarTrigger = new Timer(); + + private bool fileChangedTimerSet; + private int ignoreFileChangeLevel; + //private bool backupObsolete = true; + private uint vsFileChangeCookie; + //private string[] fontListArray; + + //private object findState; + //private bool lockImage; + private ArrayList textSpanArray = new ArrayList(); + //private IVsTextImage spTextImage; + + //private IExtensibleObjectSite extensibleObjectSite; + + #endregion + + public ComparisonControl BismNormalizerForm => bismNormalizerForm; + + #region "Window.Pane Overrides" + /// + /// Constructor that calls the Microsoft.VisualStudio.Shell.WindowPane constructor then + /// our initialization functions. + /// + /// Our Package instance. + public EditorPane(BismNormalizerPackage package) + : base(null) + { + PrivateInit(package); + } + + /// + /// This is a required override from the Microsoft.VisualStudio.Shell.WindowPane class. + /// It returns the extended rich text box that we host. + /// + public override IWin32Window Window => this.bismNormalizerForm; + + #endregion + + /// + /// Initialization routine for the Editor. Loads the list of properties for the bsmn document + /// which will show up in the properties window + /// + /// + private void PrivateInit(BismNormalizerPackage package) + { + bismNormalizerPackage = package; + loading = false; + gettingCheckoutStatus = false; + trackSel = null; + + Control.CheckForIllegalCrossThreadCalls = false; + // Create an ArrayList to store the objects that can be selected + ArrayList listObjects = new ArrayList(); + + // Create the object that will show the document's properties + // on the properties window. + EditorProperties prop = new EditorProperties(this); + listObjects.Add(prop); + + // Create the SelectionContainer object. + selContainer = new Microsoft.VisualStudio.Shell.SelectionContainer(true, false); + selContainer.SelectableObjects = listObjects; + selContainer.SelectedObjects = listObjects; + + // Create and initialize the editor + + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(EditorPane)); + this.bismNormalizerForm = new ComparisonControl(); + this.bismNormalizerForm.BismNormalizerPackage = bismNormalizerPackage; + this.bismNormalizerForm.ComparisonEditorPane = this; + bismNormalizerPackage.EditorPanes.Add(this); + + resources.ApplyResources(this.bismNormalizerForm, "editorControl", CultureInfo.CurrentUICulture); + + // To show when user can save file + this.bismNormalizerForm.ComparisonChanged += new System.EventHandler(this.OnComparisonChanged); + + + } + + /// + /// returns the name of the file currently loaded + /// + public string FileName => fileName; + + /// + /// returns the name of the window + /// + public string Name + { + get + { + if (string.IsNullOrEmpty(name)) + { + int lastSlash = fileName.LastIndexOf("\\") + 1; + return fileName.Substring(lastSlash); + } + else + return name; + } + } + + /// + /// returns whether the contents of file have changed since the last save + /// + public bool DataChanged => isDirty; + + /// + /// returns an instance of the ITrackSelection service object + /// + private ITrackSelection TrackSelection + { + get + { + if (trackSel == null) + { + trackSel = (ITrackSelection)GetService(typeof(ITrackSelection)); + } + return trackSel; + } + } + + /// + /// Clean up any resources being used. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "FileChangeTrigger")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "FNFStatusbarTrigger")] + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (bismNormalizerForm != null) + { + bismNormalizerForm.Dispose(); + bismNormalizerForm = null; + } + GC.SuppressFinalize(this); + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Gets an instance of the RunningDocumentTable (RDT) service which manages the set of currently open + /// documents in the environment and then notifies the client that an open document has changed + /// + private void NotifyDocChanged() + { + // Make sure that we have a file name + if (fileName.Length == 0) + return; + + // Get a reference to the Running Document Table + IVsRunningDocumentTable runningDocTable = (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable)); + + uint docCookie; + IVsHierarchy hierarchy; + uint itemID; + IntPtr docData = IntPtr.Zero; + + try { + // Lock the document + int hr = runningDocTable.FindAndLockDocument( + (uint)_VSRDTFLAGS.RDT_ReadLock, + fileName, + out hierarchy, + out itemID, + out docData, + out docCookie + ); + + ErrorHandler.ThrowOnFailure(hr); + + // Send the notification + hr = runningDocTable.NotifyDocumentChanged(docCookie, (uint)__VSRDTATTRIB.RDTA_DocDataReloaded); + + // Unlock the document. + // Note that we have to unlock the document even if the previous call failed. + ErrorHandler.ThrowOnFailure(runningDocTable.UnlockDocument((uint)_VSRDTFLAGS.RDT_ReadLock, docCookie)); + + // Check ff the call to NotifyDocChanged failed. + ErrorHandler.ThrowOnFailure(hr); + } + finally + { + if (docData != IntPtr.Zero) + Marshal.Release(docData); + } + } + + + + int Microsoft.VisualStudio.OLE.Interop.IPersist.GetClassID(out Guid pClassID) + { + pClassID = GuidList.guidBismNormalizerEditorFactory; + return VSConstants.S_OK; + } + + #region IPersistFileFormat Members + + /// + /// Notifies the object that it has concluded the Save transaction + /// + /// Pointer to the file name + /// S_OK if the function succeeds + // TODO: Add Editor.SaveCompleted implementation + int IPersistFileFormat.SaveCompleted(string pszFilename) => VSConstants.S_OK; + + /// + /// Returns the path to the object's current working file + /// + /// Pointer to the file name + /// Value that indicates the current format of the file as a zero based index + /// into the list of formats. Since we support only a single format, we need to return zero. + /// Subsequently, we will return a single element in the format list through a call to GetFormatList. + /// + int IPersistFileFormat.GetCurFile(out string ppszFilename, out uint pnFormatIndex) + { + // We only support 1 format so return its index + pnFormatIndex = MyFormat; + ppszFilename = fileName; + return VSConstants.S_OK; + } + + /// + /// Initialization for the object + /// + /// Zero based index into the list of formats that indicates the current format + /// of the file + /// S_OK if the method succeeds + int IPersistFileFormat.InitNew(uint nFormatIndex) + { + if (nFormatIndex != MyFormat) + { + return VSConstants.E_INVALIDARG; + } + // until someone change the file, we can consider it not dirty as + // the user would be annoyed if we prompt him to save an empty file + isDirty = false; + return VSConstants.S_OK; + } + + /// + /// Returns the class identifier of the editor type + /// + /// pointer to the class identifier + /// S_OK if the method succeeds + int IPersistFileFormat.GetClassID(out Guid pClassID) + { + ErrorHandler.ThrowOnFailure(((Microsoft.VisualStudio.OLE.Interop.IPersist)this).GetClassID(out pClassID)); + return VSConstants.S_OK; + } + + /// + /// Provides the caller with the information necessary to open the standard common "Save As" dialog box. + /// This returns an enumeration of supported formats, from which the caller selects the appropriate format. + /// Each string for the format is terminated with a newline (\n) character. + /// The last string in the buffer must be terminated with the newline character as well. + /// The first string in each pair is a display string that describes the filter, such as "Text Only + /// (*.txt)". The second string specifies the filter pattern, such as "*.txt". To specify multiple filter + /// patterns for a single display string, use a semicolon to separate the patterns: "*.htm;*.html;*.asp". + /// A pattern string can be a combination of valid file name characters and the asterisk (*) wildcard character. + /// Do not include spaces in the pattern string. The following string is an example of a file pattern string: + /// "HTML File (*.htm; *.html; *.asp)\n*.htm;*.html;*.asp\nText File (*.txt)\n*.txt\n." + /// + /// Pointer to a string that contains pairs of format filter strings + /// S_OK if the method succeeds + int IPersistFileFormat.GetFormatList(out string ppszFormatList) + { + char Endline = (char)'\n'; + string FormatList = string.Format(CultureInfo.InvariantCulture, "My Editor (*{0}){1}*{0}{1}{1}", MyExtension, Endline); + ppszFormatList = FormatList; + return VSConstants.S_OK; + } + + /// + /// Loads the file content into the textbox + /// + /// Pointer to the full path name of the file to load + /// file format mode + /// determines if the file should be opened as read only + /// S_OK if the method succeeds + int IPersistFileFormat.Load(string pszFilename, uint grfMode, int fReadOnly) + { + if (pszFilename == null) + { + return VSConstants.E_INVALIDARG; + } + + loading = true; + int hr = VSConstants.S_OK; + try + { + // Show the wait cursor while loading the file + IVsUIShell VsUiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + if (VsUiShell != null) + { + // Note: we don't want to throw or exit if this call fails, so + // don't check the return code. + hr = VsUiShell.SetWaitCursor(); + } + + // Load the file + //cbw commented out + //StreamReader str = new StreamReader(pszFilename); + //string rtfSignature = "{\\rtf"; + //string lineRead = null; + //try + //{ + // lineRead = str.ReadLine(); + //} + //finally + //{ + // str.Close(); + //} + //if (lineRead != null && lineRead.Contains(rtfSignature)) + //{ + // //try loading with Rich Text initially + // bismNormalizerForm.LoadFile(pszFilename); + //} + //else + //{ + // bismNormalizerForm.LoadFile(pszFilename); + //} + + bismNormalizerForm.LoadFile(pszFilename); + + + isDirty = false; + + //Determine if the file is read only on the file system + FileAttributes fileAttrs = File.GetAttributes(pszFilename); + + int isReadOnly = (int)fileAttrs & (int)FileAttributes.ReadOnly; + + //Set readonly if either the file is readonly for the user or on the file system + if (0 == isReadOnly && 0 == fReadOnly) + SetReadOnly(false); + else + SetReadOnly(true); + + + // Notify to the property window that some of the selected objects are changed + ITrackSelection track = TrackSelection; + if (null != track) + { + hr = track.OnSelectChange((ISelectionContainer)selContainer); + if (ErrorHandler.Failed(hr)) + return hr; + } + + // Hook up to file change notifications + if (String.IsNullOrEmpty(fileName) || 0 != String.Compare(fileName, pszFilename, true, CultureInfo.CurrentCulture)) + { + fileName = pszFilename; + SetFileChangeNotification(pszFilename, true); + + // Notify the load or reload + NotifyDocChanged(); + } + } + finally + { + loading = false; + } + return VSConstants.S_OK; + } + + /// + /// Determines whether an object has changed since being saved to its current file + /// + /// true if the document has changed + /// S_OK if the method succeeds + int IPersistFileFormat.IsDirty(out int pfIsDirty) + { + if (isDirty) + { + pfIsDirty = 1; + } + else + { + pfIsDirty = 0; + } + return VSConstants.S_OK; + } + + /// + /// Save the contents of the textbox into the specified file. If doing the save on the same file, we need to + /// suspend notifications for file changes during the save operation. + /// + /// Pointer to the file name. If the pszFilename parameter is a null reference + /// we need to save using the current file + /// + /// Boolean value that indicates whether the pszFileName parameter is to be used + /// as the current working file. + /// If remember != 0, pszFileName needs to be made the current file and the dirty flag needs to be cleared after the save. + /// Also, file notifications need to be enabled for the new file and disabled for the old file + /// If remember == 0, this save operation is a Save a Copy As operation. In this case, + /// the current file is unchanged and dirty flag is not cleared + /// + /// Zero based index into the list of formats that indicates the format in which + /// the file will be saved + /// S_OK if the method succeeds + int IPersistFileFormat.Save(string pszFilename, int fRemember, uint nFormatIndex) + { + int hr = VSConstants.S_OK; + bool doingSaveOnSameFile = false; + // If file is null or same --> SAVE + if (pszFilename == null || pszFilename == fileName) + { + fRemember = 1; + doingSaveOnSameFile = true; + } + + //Suspend file change notifications for only Save since we don't have notifications setup + //for SaveAs and SaveCopyAs (as they are different files) + if (doingSaveOnSameFile) + this.SuspendFileChangeNotification(pszFilename, 1); + + try + { + bismNormalizerForm.SaveFile(pszFilename); + } + catch (ArgumentException) + { + hr = VSConstants.E_FAIL; + } + catch (IOException) + { + hr = VSConstants.E_FAIL; + } + finally + { + //restore the file change notifications + if (doingSaveOnSameFile) + this.SuspendFileChangeNotification(pszFilename, 0); + } + + if (VSConstants.E_FAIL == hr) + return hr; + + //Save and Save as + if (fRemember != 0) + { + //Save as + if (null != pszFilename && !fileName.Equals(pszFilename)) + { + SetFileChangeNotification(fileName, false); //remove notification from old file + SetFileChangeNotification(pszFilename, true); //add notification for new file + fileName = pszFilename; //cache the new file name + } + isDirty = false; + SetReadOnly(false); //set read only to false since you were successfully able + //to save to the new file + } + + ITrackSelection track = TrackSelection; + if (null != track) + { + hr = track.OnSelectChange((ISelectionContainer)selContainer); + } + + // Since all changes are now saved properly to disk, there's no need for a backup. + //backupObsolete = false; + return hr; + } + + #endregion + + #region IVsPersistDocData Members + + /// + /// Used to determine if the document data has changed since the last time it was saved + /// + /// Will be set to 1 if the data has changed + /// S_OK if the function succeeds + int IVsPersistDocData.IsDocDataDirty(out int pfDirty) => ((IPersistFileFormat)this).IsDirty(out pfDirty); + + /// + /// Saves the document data. Before actually saving the file, we first need to indicate to the environment + /// that a file is about to be saved. This is done through the "SVsQueryEditQuerySave" service. We call the + /// "QuerySaveFile" function on the service instance and then proceed depending on the result returned as follows: + /// If result is QSR_SaveOK - We go ahead and save the file and the file is not read only at this point. + /// If result is QSR_ForceSaveAs - We invoke the "Save As" functionality which will bring up the Save file name + /// dialog + /// If result is QSR_NoSave_Cancel - We cancel the save operation and indicate that the document could not be saved + /// by setting the "pfSaveCanceled" flag + /// If result is QSR_NoSave_Continue - Nothing to do here as the file need not be saved + /// + /// Flags which specify the file save options: + /// VSSAVE_Save - Saves the current file to itself. + /// VSSAVE_SaveAs - Prompts the User for a filename and saves the file to the file specified. + /// VSSAVE_SaveCopyAs - Prompts the user for a filename and saves a copy of the file with a name specified. + /// VSSAVE_SilentSave - Saves the file without prompting for a name or confirmation. + /// + /// Pointer to the path to the new document + /// value 1 if the document could not be saved + /// + int IVsPersistDocData.SaveDocData(Microsoft.VisualStudio.Shell.Interop.VSSAVEFLAGS dwSave, out string pbstrMkDocumentNew, out int pfSaveCanceled) + { + pbstrMkDocumentNew = null; + pfSaveCanceled = 0; + int hr = VSConstants.S_OK; + + switch (dwSave) + { + case VSSAVEFLAGS.VSSAVE_Save: + case VSSAVEFLAGS.VSSAVE_SilentSave: + { + IVsQueryEditQuerySave2 queryEditQuerySave = (IVsQueryEditQuerySave2)GetService(typeof(SVsQueryEditQuerySave)); + + // Call QueryEditQuerySave + uint result = 0; + hr = queryEditQuerySave.QuerySaveFile( + fileName, // filename + 0, // flags + null, // file attributes + out result); // result + if (ErrorHandler.Failed(hr)) + return hr; + + // Process according to result from QuerySave + switch ((tagVSQuerySaveResult)result) + { + case tagVSQuerySaveResult.QSR_NoSave_Cancel: + // Note that this is also case tagVSQuerySaveResult.QSR_NoSave_UserCanceled because these + // two tags have the same value. + pfSaveCanceled = ~0; + break; + + case tagVSQuerySaveResult.QSR_SaveOK: + { + // Call the shell to do the save for us + IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + hr = uiShell.SaveDocDataToFile(dwSave, (IPersistFileFormat)this, fileName, out pbstrMkDocumentNew, out pfSaveCanceled); + if (ErrorHandler.Failed(hr)) + return hr; + } + break; + + case tagVSQuerySaveResult.QSR_ForceSaveAs: + { + // Call the shell to do the SaveAS for us + IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + hr = uiShell.SaveDocDataToFile(VSSAVEFLAGS.VSSAVE_SaveAs, (IPersistFileFormat)this, fileName, out pbstrMkDocumentNew, out pfSaveCanceled); + if (ErrorHandler.Failed(hr)) + return hr; + } + break; + + case tagVSQuerySaveResult.QSR_NoSave_Continue: + // In this case there is nothing to do. + break; + + default: + throw new NotSupportedException("Unsupported result from QEQS"); + } + break; + } + case VSSAVEFLAGS.VSSAVE_SaveAs: + case VSSAVEFLAGS.VSSAVE_SaveCopyAs: + { + // Make sure the file name as the right extension + if (String.Compare(MyExtension, System.IO.Path.GetExtension(fileName), true, CultureInfo.CurrentCulture) != 0) + { + fileName += MyExtension; + } + // Call the shell to do the save for us + IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + hr = uiShell.SaveDocDataToFile(dwSave, (IPersistFileFormat)this, fileName, out pbstrMkDocumentNew, out pfSaveCanceled); + if (ErrorHandler.Failed(hr)) + return hr; + break; + } + default: + throw new ArgumentException("Unsupported Save flag"); + }; + + return VSConstants.S_OK; + } + + /// + /// Loads the document data from the file specified + /// + /// Path to the document file which needs to be loaded + /// S_Ok if the method succeeds + int IVsPersistDocData.LoadDocData(string pszMkDocument) => ((IPersistFileFormat)this).Load(pszMkDocument, 0, 0); + + /// + /// Used to set the initial name for unsaved, newly created document data + /// + /// String containing the path to the document. We need to ignore this parameter + /// + /// S_OK if the method succeeds + int IVsPersistDocData.SetUntitledDocPath(string pszDocDataPath) + { + name = pszDocDataPath; + return ((IPersistFileFormat)this).InitNew(MyFormat); + } + + /// + /// Returns the Guid of the editor factory that created the IVsPersistDocData object + /// + /// Pointer to the class identifier of the editor type + /// S_OK if the method succeeds + int IVsPersistDocData.GetGuidEditorType(out Guid pClassID) => ((IPersistFileFormat)this).GetClassID(out pClassID); + + /// + /// Close the IVsPersistDocData object + /// + /// S_OK if the function succeeds + int IVsPersistDocData.Close() + { + if (bismNormalizerPackage.EditorPanes.Contains(this)) + { + bismNormalizerPackage.EditorPanes.Remove(this); + } + //doing from ComparisonControl.OnHandleDestroyed instead + //if (bismNormalizerForm != null) + //{ + // bismNormalizerForm.ClearMessages(); + //} + return VSConstants.S_OK; + } + + /// + /// Determines if it is possible to reload the document data + /// + /// set to 1 if the document can be reloaded + /// S_OK if the method succeeds + int IVsPersistDocData.IsDocDataReloadable(out int pfReloadable) + { + // Allow file to be reloaded + pfReloadable = 1; + return VSConstants.S_OK; + } + + /// + /// Renames the document data + /// + /// + /// + /// + /// + /// + // TODO: Add EditorPane.RenameDocData implementation + int IVsPersistDocData.RenameDocData(uint grfAttribs, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew) => VSConstants.S_OK; + + /// + /// Reloads the document data + /// + /// Flag indicating whether to ignore the next file change when reloading the document data. + /// This flag should not be set for us since we implement the "IVsDocDataFileChangeControl" interface in order to + /// indicate ignoring of file changes + /// + /// S_OK if the method succeeds + int IVsPersistDocData.ReloadDocData(uint grfFlags) => ((IPersistFileFormat)this).Load(fileName, grfFlags, 0); + + /// + /// Called by the Running Document Table when it registers the document data. + /// + /// Handle for the document to be registered + /// Pointer to the IVsHierarchy interface + /// Item identifier of the document to be registered from VSITEM + /// + //Nothing to do here + int IVsPersistDocData.OnRegisterDocData(uint docCookie, IVsHierarchy pHierNew, uint itemidNew) => VSConstants.S_OK; + + #endregion + + #region IVsFileChangeEvents Members + + /// + /// Notify the editor of the changes made to one or more files + /// + /// Number of files that have changed + /// array of the files names that have changed + /// Array of the flags indicating the type of changes + /// + int IVsFileChangeEvents.FilesChanged(uint cChanges, string[] rgpszFile, uint[] rggrfChange) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t**** Inside FilesChanged ****")); + + //check the different parameters + if (0 == cChanges || null == rgpszFile || null == rggrfChange) + return VSConstants.E_INVALIDARG; + + //ignore file changes if we are in that mode + if (ignoreFileChangeLevel != 0) + return VSConstants.S_OK; + + for (uint i = 0; i < cChanges; i++) + { + if (!String.IsNullOrEmpty(rgpszFile[i]) && String.Compare(rgpszFile[i], fileName, true, CultureInfo.CurrentCulture) == 0) + { + // if the readonly state (file attributes) have changed we can immediately update + // the editor to match the new state (either readonly or not readonly) immediately + // without prompting the user. + if (0 != (rggrfChange[i] & (int)_VSFILECHANGEFLAGS.VSFILECHG_Attr)) + { + FileAttributes fileAttrs = File.GetAttributes(fileName); + int isReadOnly = (int)fileAttrs & (int)FileAttributes.ReadOnly; + SetReadOnly(isReadOnly != 0); + } + // if it looks like the file contents have changed (either the size or the modified + // time has changed) then we need to prompt the user to see if we should reload the + // file. it is important to not synchronously reload the file inside of this FilesChanged + // notification. first it is possible that there will be more than one FilesChanged + // notification being sent (sometimes you get separate notifications for file attribute + // changing and file size/time changing). also it is the preferred UI style to not + // prompt the user until the user re-activates the environment application window. + // this is why we use a timer to delay prompting the user. + if (0 != (rggrfChange[i] & (int)(_VSFILECHANGEFLAGS.VSFILECHG_Time | _VSFILECHANGEFLAGS.VSFILECHG_Size))) + { + if (!fileChangedTimerSet) + { + FileChangeTrigger = new Timer(); + fileChangedTimerSet = true; + FileChangeTrigger.Interval = 1000; + FileChangeTrigger.Tick += new EventHandler(this.OnFileChangeEvent); + FileChangeTrigger.Enabled = true; + } + } + } + } + + return VSConstants.S_OK; + } + + /// + /// Notify the editor of the changes made to a directory + /// + /// Name of the directory that has changed + /// + //Nothing to do here + int IVsFileChangeEvents.DirectoryChanged(string pszDirectory) => VSConstants.S_OK; + + #endregion + + #region IVsDocDataFileChangeControl Members + + /// + /// Used to determine whether changes to DocData in files should be ignored or not + /// + /// a non zero value indicates that the file changes should be ignored + /// + /// + int IVsDocDataFileChangeControl.IgnoreFileChanges(int fIgnore) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t **** Inside IgnoreFileChanges ****")); + + if (fIgnore != 0) + { + ignoreFileChangeLevel++; + } + else + { + if (ignoreFileChangeLevel > 0) + ignoreFileChangeLevel--; + + // We need to check here if our file has changed from "Read Only" + // to "Read/Write" or vice versa while the ignore level was non-zero. + // This may happen when a file is checked in or out under source + // code control. We need to check here so we can update our caption. + if (File.Exists(fileName)) + { + FileAttributes fileAttrs = File.GetAttributes(fileName); + int isReadOnly = (int)fileAttrs & (int)FileAttributes.ReadOnly; + SetReadOnly(isReadOnly != 0); + } + else + { + if (this.bismNormalizerPackage != null) + { + this.bismNormalizerPackage.ShowMessage("Changing file name while editor is open is not supported. Please close the file and reopen.", OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGICON.OLEMSGICON_WARNING); + } + } + } + return VSConstants.S_OK; + } + #endregion + + #region File Change Notification Helpers + + /// + /// In this function we inform the shell when we wish to receive + /// events when our file is changed or we inform the shell when + /// we wish not to receive events anymore. + /// + /// File name string + /// TRUE indicates advise, FALSE indicates unadvise. + /// Result of the operation + private int SetFileChangeNotification(string pszFileName, bool fStart) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t **** Inside SetFileChangeNotification ****")); + + int result = VSConstants.E_FAIL; + + //Get the File Change service + if (null == vsFileChangeEx) + vsFileChangeEx = (IVsFileChangeEx)GetService(typeof(SVsFileChangeEx)); + if (null == vsFileChangeEx) + return VSConstants.E_UNEXPECTED; + + // Setup Notification if fStart is TRUE, Remove if fStart is FALSE. + if (fStart) + { + if (vsFileChangeCookie == VSConstants.VSCOOKIE_NIL) + { + //Receive notifications if either the attributes of the file change or + //if the size of the file changes or if the last modified time of the file changes + result = vsFileChangeEx.AdviseFileChange(pszFileName, + (uint)(_VSFILECHANGEFLAGS.VSFILECHG_Attr | _VSFILECHANGEFLAGS.VSFILECHG_Size | _VSFILECHANGEFLAGS.VSFILECHG_Time), + (IVsFileChangeEvents)this, + out vsFileChangeCookie); + if (vsFileChangeCookie == VSConstants.VSCOOKIE_NIL) + return VSConstants.E_FAIL; + } + } + else + { + if (vsFileChangeCookie != VSConstants.VSCOOKIE_NIL) + { + result = vsFileChangeEx.UnadviseFileChange(vsFileChangeCookie); + vsFileChangeCookie = VSConstants.VSCOOKIE_NIL; + } + } + return result; + } + + /// + /// In this function we suspend receiving file change events for + /// a file or we reinstate a previously suspended file depending + /// on the value of the given fSuspend flag. + /// + /// File name string + /// TRUE indicates that the events needs to be suspended + /// + + private int SuspendFileChangeNotification(string pszFileName, int fSuspend) + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t **** Inside SuspendFileChangeNotification ****")); + + if (null == vsFileChangeEx) + vsFileChangeEx = (IVsFileChangeEx)GetService(typeof(SVsFileChangeEx)); + if (null == vsFileChangeEx) + return VSConstants.E_UNEXPECTED; + + if (0 == fSuspend) + { + // we are transitioning from suspended to non-suspended state - so force a + // sync first to avoid asynchronous notifications of our own change + if (vsFileChangeEx.SyncFile(pszFileName) == VSConstants.E_FAIL) + return VSConstants.E_FAIL; + } + + //If we use the VSCOOKIE parameter to specify the file, then pszMkDocument parameter + //must be set to a null reference and vice versa + return vsFileChangeEx.IgnoreFile(vsFileChangeCookie, null, fSuspend); + } + #endregion + + + /// + /// Used to ReadOnly property for the Rich TextBox and correspondingly update the editor caption + /// + /// Indicates whether the file loaded is Read Only or not + private void SetReadOnly(bool _isFileReadOnly) + { + //this.bismNormForm.ReadOnly = _isFileReadOnly; + ////update editor caption with "[Read Only]" or "" as necessary + //IVsWindowFrame frame = (IVsWindowFrame)GetService(typeof(SVsWindowFrame)); + //string editorCaption = ""; + //if (_isFileReadOnly) + // editorCaption = this.GetResourceString("@100"); + //ErrorHandler.ThrowOnFailure(frame.SetProperty((int)__VSFPROPID.VSFPROPID_EditorCaption, editorCaption)); + //backupObsolete = true; + } + + /// + /// This event is triggered when one of the files loaded into the environment has changed outside of the + /// editor + /// + /// + /// + private void OnFileChangeEvent(object sender, System.EventArgs e) + { + //Disable the timer + FileChangeTrigger.Enabled = false; + + + //---------------------- + //cbw, had to do this to avoid error that package is not sited + IVsResourceManager resourceManager = (IVsResourceManager)GetService(typeof(SVsResourceManager)); + if (resourceManager == null) + { + return; + } + //---------------------- + + + + string message = this.GetResourceString("@101"); //get the message string from the resource + IVsUIShell VsUiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); + int result = 0; + Guid tempGuid = Guid.Empty; + if (VsUiShell != null) + { + //Show up a message box indicating that the file has changed outside of VS environment + ErrorHandler.ThrowOnFailure(VsUiShell.ShowMessageBox(0, ref tempGuid, fileName, message, null, 0, + OLEMSGBUTTON.OLEMSGBUTTON_YESNOCANCEL, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, + OLEMSGICON.OLEMSGICON_QUERY, 0, out result)); + } + //if the user selects "Yes", reload the current file + if (result == (int)DialogResult.Yes) + { + ErrorHandler.ThrowOnFailure(((IVsPersistDocData)this).ReloadDocData(0)); + } + + fileChangedTimerSet = false; + } + + /// + /// This method loads a localized string based on the specified resource. + /// + /// Resource to load + /// String loaded for the specified resource + internal string GetResourceString(string resourceName) + { + string resourceValue; + IVsResourceManager resourceManager = (IVsResourceManager)GetService(typeof(SVsResourceManager)); + if (resourceManager == null) + { + throw new InvalidOperationException("Could not get SVsResourceManager service. Make sure the package is Sited before calling this method"); + } + Guid packageGuid = bismNormalizerPackage.GetType().GUID; + int hr = resourceManager.LoadResourceString(ref packageGuid, -1, resourceName, out resourceValue); + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr); + return resourceValue; + } + + /// + /// This function asks to the QueryEditQuerySave service if it is possible to + /// edit the file. + /// + private bool CanEditFile() + { + Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t**** CanEditFile called ****")); + + // Check the status of the recursion guard + if (gettingCheckoutStatus) + return false; + + try + { + // Set the recursion guard + gettingCheckoutStatus = true; + + // Get the QueryEditQuerySave service + IVsQueryEditQuerySave2 queryEditQuerySave = (IVsQueryEditQuerySave2)GetService(typeof(SVsQueryEditQuerySave)); + + // Now call the QueryEdit method to find the edit status of this file + string[] documents = { this.fileName }; + uint result; + uint outFlags; + + // Note that this function can popup a dialog to ask the user to checkout the file. + // When this dialog is visible, it is possible to receive other request to change + // the file and this is the reason for the recursion guard. + int hr = queryEditQuerySave.QueryEditFiles( + 0, // Flags + 1, // Number of elements in the array + documents, // Files to edit + null, // Input flags + null, // Input array of VSQEQS_FILE_ATTRIBUTE_DATA + out result, // result of the checkout + out outFlags // Additional flags + ); + if (ErrorHandler.Succeeded(hr) && (result == (uint)tagVSQueryEditResult.QER_EditOK)) + { + // In this case (and only in this case) we can return true from this function. + return true; + } + } + + finally + { + gettingCheckoutStatus = false; + } + return false; + } + + /// + /// This event is triggered when there contents of the file are changed inside the editor + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Microsoft.VisualStudio.Shell.Interop.ITrackSelection.OnSelectChange(Microsoft.VisualStudio.Shell.Interop.ISelectionContainer)")] + private void OnComparisonChanged(object sender, System.EventArgs e) + { + // During the load operation the text of the control will change, but + // this change must not be stored in the status of the document. + if (!loading) + { + // The only interesting case is when we are changing the document + // for the first time + if (!isDirty) + { + // Check if the QueryEditQuerySave service allow us to change the file + if (!CanEditFile()) + { + // We can not change the file (e.g. a checkout operation failed), + // so undo the change and exit. + + //cbw todo :bismNormForm.RichTextBoxControl.Undo(); + return; + } + + // It is possible to change the file, so update the status. + isDirty = true; + ITrackSelection track = TrackSelection; + if (null != track) + { + // Note: here we don't need to check the return code. + track.OnSelectChange((ISelectionContainer)selContainer); + } + //backupObsolete = true; + } + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/EditorPane.resx b/BismNormalizer/BismNormalizer/EditorPane.resx new file mode 100644 index 0000000..ea010bd --- /dev/null +++ b/BismNormalizer/BismNormalizer/EditorPane.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Top, Bottom, Left, Right + + + + 8, 8 + + + 136, 136 + + + + 0 + + + + + + textBox1 + + + $this + + + 0 + + + True + + + EditorPane + + + System.Windows.Forms.UserControl, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/Fakes/Microsoft.Office.Interop.Excel.fakes b/BismNormalizer/BismNormalizer/Fakes/Microsoft.Office.Interop.Excel.fakes new file mode 100644 index 0000000000000000000000000000000000000000..418f53ac42e04844ea06c9e8fbf7d607edfd6c11 GIT binary patch literal 276 zcmZvY!3x4K5JcxJ_z$5U&{U673mz0as5kGmq$swgm>%@!)z^shB1_25WOjDQ`;}{> zQdcr-dvPRKYkDC4I_$fw{|rylRvw*uz`U&(mtIOf$;jSZJyV zdyI0em2`)$X>*sTah5dlY~8M z{750QeM8K%7so&b8;s<%xJTBQjDWL;+a#?+Pi4k2fD91V{$_Y0Pwv3ieE%xBPKKvm z*_yIx%|ITsacM!H5i&AFKq7VF= zPaqcZciWw+k>1~ztr5T?6t$1igCJZ%brhle6}@iQP!6y)j~`?9ff!I z+kV$v@~L@A`mm&Cr{`zUahZZv|c%?_@m42q6c3#-TiMt(dQ_(*(+q(JPz4 zY{^g{=)TEDiU!1(@aZ1v^9J~qBAU@RZbx8CpAknIx89*-77MI zrkPQr-v8^9w-44R*?hCR+)wq+=yt*n4M>pK6^kZs;hO+lIJ@dDulW$7^17JxQMrS& zg_cAxEwkYzS9pV!mOh$! + /// This class will contain all methods that we need to import. + /// + internal class NativeMethods + { + public const int WM_LBUTTONDOWN = 0x0201; + public const int WM_LBUTTONDBLCLK = 0x0203; + public const int WM_RBUTTONDOWN = 0x0204; + public const int WM_MBUTTONDOWN = 0x0207; + + //Including a private constructor to prevent a compiler-generated default constructor + private NativeMethods() + { + } + + // Import the SendMessage function from user32.dll + [DllImport("user32.dll")] + public static extern IntPtr SendMessage(IntPtr hwnd, + int Msg, + IntPtr wParam, + [MarshalAs(UnmanagedType.IUnknown)] out object lParam); + } +} \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/PkgCmdID.cs b/BismNormalizer/BismNormalizer/PkgCmdID.cs new file mode 100644 index 0000000..da4adf3 --- /dev/null +++ b/BismNormalizer/BismNormalizer/PkgCmdID.cs @@ -0,0 +1,16 @@ +// PkgCmdID.cs +// MUST match PkgCmdID.h +using System; + +namespace BismNormalizer +{ + static class PkgCmdIDList + { + public const uint cmdidToolMenuNewComparison = 0x100; + public const uint cmdidValidationOutput = 0x101; + public const uint cmdidProjectMenuNewComparison = 0x0103; + public const uint cmdidFileMenuViewCode = 0x0104; + public const uint cmdidFileMenuViewDesigner = 0x0105; + public const uint cmdidFileMenuInstallIcon = 0x0106; + }; +} \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/Properties/AssemblyInfo.cs b/BismNormalizer/BismNormalizer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..027fb55 --- /dev/null +++ b/BismNormalizer/BismNormalizer/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BismNormalizer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("BismNormalizer")] +[assembly: AssemblyProduct("BismNormalizer")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] +[assembly: NeutralResourcesLanguage("en-US")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("4.0.0.11")] +[assembly: AssemblyFileVersion("4.0.0.11")] diff --git a/BismNormalizer/BismNormalizer/Resources.Designer.cs b/BismNormalizer/BismNormalizer/Resources.Designer.cs new file mode 100644 index 0000000..6168698 --- /dev/null +++ b/BismNormalizer/BismNormalizer/Resources.Designer.cs @@ -0,0 +1,151 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BismNormalizer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BismNormalizer.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ButtonSwitch { + get { + object obj = ResourceManager.GetObject("ButtonSwitch", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized string similar to Can not create tool window.. + /// + internal static string CanNotCreateWindow { + get { + return ResourceManager.GetString("CanNotCreateWindow", resourceCulture); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap LogoSmall { + get { + object obj = ResourceManager.GetObject("LogoSmall", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap LogoSmall1 { + get { + object obj = ResourceManager.GetObject("LogoSmall1", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Progress { + get { + object obj = ResourceManager.GetObject("Progress", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ProgressCancel { + get { + object obj = ResourceManager.GetObject("ProgressCancel", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ProgressError { + get { + object obj = ResourceManager.GetObject("ProgressError", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ProgressSuccess { + get { + object obj = ResourceManager.GetObject("ProgressSuccess", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized string similar to BISM Normalizer Warning List. + /// + internal static string ToolWindowTitle { + get { + return ResourceManager.GetString("ToolWindowTitle", resourceCulture); + } + } + } +} diff --git a/BismNormalizer/BismNormalizer/Resources.resx b/BismNormalizer/BismNormalizer/Resources.resx new file mode 100644 index 0000000..9d9525c --- /dev/null +++ b/BismNormalizer/BismNormalizer/Resources.resx @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Can not create tool window. + + + + Resources\Progress.gif;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + BISM Normalizer Warning List + + + Resources\LogoSmall.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\ButtonSwitch.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\LogoSmall.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\ProgressCheck.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\ProgressError.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\ProgressWarning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/Resources/1.png b/BismNormalizer/BismNormalizer/Resources/1.png new file mode 100644 index 0000000000000000000000000000000000000000..858d394bced4527a629a689b93047536ceb91f25 GIT binary patch literal 3147 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/3.png b/BismNormalizer/BismNormalizer/Resources/3.png new file mode 100644 index 0000000000000000000000000000000000000000..5f5385a7b10ce842f65095a9edb44b70a8ece554 GIT binary patch literal 2874 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/4.png b/BismNormalizer/BismNormalizer/Resources/4.png new file mode 100644 index 0000000000000000000000000000000000000000..910644393b2ca6c7847c06305817db7e3186833d GIT binary patch literal 2964 zcmV;F3v2X=P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0002LNkli2t&LUS1~UHt|DUKlz{SPIkeHm#5ET)E;u=Oub0fvE02lym5pf&EVgKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0003oNkl1xWBme*a literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/6.png b/BismNormalizer/BismNormalizer/Resources/6.png new file mode 100644 index 0000000000000000000000000000000000000000..f4524c90d001415492b2f55e60e4a9923c1a93df GIT binary patch literal 3165 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 hDn#Tnp`&qr4*#p6 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/BismNormalizerLogo.png b/BismNormalizer/BismNormalizer/Resources/BismNormalizerLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..247f7b2f10a5f94b4ffe9bba2bbdada467879ae9 GIT binary patch literal 3848 zcmV+j5BKniP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000CtNklC@Or(@ z?(Qy?N`=M6MT*5Dg+gKEB5D}$*v5v}zCP|7rkvk(cvfxj~=qZx_(1N;?& zXR?u{H8wUjn4h0dwUt;;Uduw{oCplv%t^m%X^31Fe1CtR-Q8Vwc6QPe7)E4J@WaDH z0QUCw%mm)#-tgJ_Op_+-t#xA&Q}A#Y3q1erIos`R=6=rcaPlF7@VNc>(jvox4=wP* ze!+Hgo1B-!QWkT1jt4g$aH39FYpwMa3GW$(1@9$b50Y#-az4CRC{cWy_(t`l%B_=I za05;`rPL~Qe5X(-P%IWx0q->i60opP;UPir1ggJR`O^Ht?8z*->p5)fz7a_a+(-cM z!6jLX6?pJ{;ybU}ubBNci}$C;vb)S(?`|gGMgkrlq6AA4O{_ppavE(rEW@g4b0)qH}9z7-E(FmiI6dne=SS+%%v}7(YEz>?!aP(R}$!HB5 zfqvk@_%N6N(@_Zx;X7tuy1OgeEX^(i2TG3PydSI=l3Y|qNZ^>`Xm;bhP$U!7eNp}rOTw?NuBHZVmc+_$=3L7K zkM3qJCgS1Y2jVO*dcYfl8?_Qmn%vg65zCK4D3mOr(H_x%Bl`f{a~x-382EuBu-00m zb-jg+P5Eb;1Lpv7rK7BNBy+IT(wuQqfqUT-;is*q{+TJT#ismknW|God8*^4CyGpL zIF7S0mUcRrv4!}f?I*1#ercU*v&=H@j^0tr*AO`qaFu(u`CWzk0Vf}b{ECUR&qQo& z%133s{_zzTf77!v{v(i4r*)o)JLPhp`@B7VOD$jHCO0z`7_8Ur(>=#U!gH_f*IeTo zdCBw0eH1Q5HC};>A3=7BByvgde;8Wp1+De_;9qf~R~EnPzX1T0JrJU&Fh5lQ0000< KMNUMnLSTYrKrNC0 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/BismNormalizerLogoText.png b/BismNormalizer/BismNormalizer/Resources/BismNormalizerLogoText.png new file mode 100644 index 0000000000000000000000000000000000000000..f1ff5562e43285a8b0f29ef0753a2e6ebe37fd12 GIT binary patch literal 10620 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/ButtonSwitch.png b/BismNormalizer/BismNormalizer/Resources/ButtonSwitch.png new file mode 100644 index 0000000000000000000000000000000000000000..f56a450249ea29616b67ecfae64effa8a59eff1a GIT binary patch literal 2954 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 ANB{r; literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Check.png b/BismNormalizer/BismNormalizer/Resources/Check.png new file mode 100644 index 0000000000000000000000000000000000000000..80232f1c54503dc38884b2c65a76dd15201d9594 GIT binary patch literal 3346 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 ZJOB;erTg41K=J?p002ovPDHLkV1jvDh!g+- literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/CreateAction.png b/BismNormalizer/BismNormalizer/Resources/CreateAction.png new file mode 100644 index 0000000000000000000000000000000000000000..0446f3a9be67c2a7fcd0eb2e81f8d2bca5ddf5e1 GIT binary patch literal 2845 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/CreateActionGrey.png b/BismNormalizer/BismNormalizer/Resources/CreateActionGrey.png new file mode 100644 index 0000000000000000000000000000000000000000..938533b568991aabaa21a5531b13e1e0fbf5320a GIT binary patch literal 1035 zcmV+m1oZofP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<8AY({UO#lFTB>(_`g8%^e z{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+ZAnByRCwBiP(cpBAP6hX-nsZRvxmwK zj15I62}Yolw4xY-Xt6wEQPmKqEC2+6o}dTVkjP8Rabg$`0HUZ$G+5&KxtJov+jM>Y z@;l)pPP7o+re$Q3=;Wo=EV7U}3+`IX9<&`pGU`9>0RT2wBoo`O8NmPm002ovPDHLk FV1nD4wk`kw literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Culture.png b/BismNormalizer/BismNormalizer/Resources/Culture.png new file mode 100644 index 0000000000000000000000000000000000000000..c180c5a8c490a6b7ac5c2a5223c6b7869a4b8821 GIT binary patch literal 3028 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

eU}*OA;-w@cBF)MXQAt=hN?17B>FuE7g&98M z>ZgQ-BPrxtF4zAuAR^?K0x4#~R*r~@j<(5AsYWFlRBq!j8vxXECR{g|nl;h&7_d26 zoaOJg)@3jxI3!v^#134^iKWwh9yuTGu&zaCZG9j3)J*=5~R=F-jIVu)@ z0^Aq9gG7Xoz1z${=9dB~U5Gg!uYp!BTp#ze988XCjEM*(h)Jul6s>E_``9-<-=@b$ z@(c8wKf>il%@Ya|;ns&A9qWlc3ti^zZo`0K70ZE(059r~m)}07*qo IM6N<$g2$rbRR910 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/DeleteAction.png b/BismNormalizer/BismNormalizer/Resources/DeleteAction.png new file mode 100644 index 0000000000000000000000000000000000000000..7125bddcbee33f85b64ef6eb045f92cced80a89f GIT binary patch literal 3170 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

eU}*OA;-w@cBF)MXQAt=hN?17B>FuE7g&98M z>ZgQ-BPrxtF4zAuAR^?K0x4#~R*r~@j<(5AsYWFlRBq!j8vxXECR{g|nl;h&7_d26 zoaOJg)@3jxI3!v^#134^iKWwh9yuTGu&zaCZG9j3)J*=5~R=F-jIVu)@ z0^Aq9gG7Xoz1z${=9dB~U5Gg!uYp!BTp#ze988XCjEM*(h)Jul6s>E_``9-<-=@b$ z@(c8wKf>il%@Ya|;ns&A9qWlc3ti^zZo`0K70ZE(059r~m)}07*qo IM6N<$g2$rbRR910 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/DeleteActionGrey.png b/BismNormalizer/BismNormalizer/Resources/DeleteActionGrey.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf08e142db21d255456e9c49a9e4686644e8e8f GIT binary patch literal 1190 zcmV;X1X=ruP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<8AY({UO#lFTB>(_`g8%^e z{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-2uVaiRCwBikugid01$=06rH3>GZ=q` zI92dJm`TJ*I=Iyz(4XMoP*NQ$txzF2xH(EXNNGkFM-d!iQyNX^B=k9$m}IcN+wR`o zy?bYf<){G0e+0o$9fhS~NibAK0G@ywk{~OGday=jfp7%Cl@+^iR(sVwdv+@VKxxEd zM{RXAYkG3@ah~Okd~xjd4`D%thF{%HlN&}+Su*a`7gU*;>3CozFGtP0I>4Nh8LJq4 z6+}XOdwnryjpNg81|)AN_W?-!{sF9G+qKJ)xkJy22Wh7eq1tlf=+pgC?r*-Rt8oAK zijDvp@71kb(NvE-o2>{TJUN<3lp_v2m3;X7SxfOf01+30>lc7JaR2}S07*qoM6N<$ Eg1NK(j{pDw literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Error.png b/BismNormalizer/BismNormalizer/Resources/Error.png new file mode 100644 index 0000000000000000000000000000000000000000..ac9ea3b76f5a573ab654470488327bd139e4bae3 GIT binary patch literal 3021 zcmV;;3o`VHP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0002^NklK?=e!5Zw5Lc*)UE2<;2(UFr)oAJB{7!F%xozQG6h10(4lan<&Rs_2+5Ef>#vzahNgg!M08;|@fN;%8!LuM#SIm;gTS+o=o(c`k3Z!8C6 zIp7zV>vk7k2klel4ria1X1XKbbLuONkK{42^1l~$ryq**QGQ7qETjZMM&e&_c9W(I`JM%`qt P00000NkvXXu0mjfQ#+kR literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Expression Old.png b/BismNormalizer/BismNormalizer/Resources/Expression Old.png new file mode 100644 index 0000000000000000000000000000000000000000..db194a59baa737b3746f7bd3215396958d36e5c6 GIT binary patch literal 2935 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 hDn#Tnp`&qr4*#p6 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Expression.png b/BismNormalizer/BismNormalizer/Resources/Expression.png new file mode 100644 index 0000000000000000000000000000000000000000..46555cccdcd1d578fa5052e6fecbf75dfb2c1d4f GIT binary patch literal 1524 zcmbVMdrZ`J9Iv1#r#KcE2|7ksk-+e-k9(BsIN-S6$*Bhj2*)c@+5#1>ExjU#Iz#Xg zjvVNgO-QyZLo+2V^RZ3AF`_5|bx!AF&M6L;=@vJXZDg4v2G&E)?GNK0YtsH+pU>y} z{e2(x=47o5Q!P;e01#%+By*K9!T&A@QNF&ZUY9aN%T}kHCsfJ}l*9sNMyOyxyNjw~ zb6JXMtUJf106;L$j@sbGqSPy}48Oo1L(E$=2gCTK>NQ0D$^7y@S? za*YXk;*`^o16l-$1@#((nnnybh#NJi9>;a~3J^n3J&b5!EuzM>gbpEe7&!YtiZzL; zByvgHtSx0_f~sX%Bw)Cqp+VE2)d*4*j2ewb7{Oo+Q!5CyyOEbEkD7Nc3n-ARo0d3H z<^&$}D^e9gy=;OMPoG@DB|02Wg?aaEpp=lo9!i8!4FbDd{(H@!-EuDbuN%)qyYm}G z7S3heLcK&Q`KVkLASDY1N{*!KOoPs6iLdcotW0)@v0MmW|~Y``w3*q!B~&*c&piKS#g$`^#%Kn3Jf3$oy@7DUiunaMbqP{{F&(BMw=r|8Lz zNmk-EvW!gN}OdM*dOREpZQTyEhOr<_jgdLHoLR^W{ zCa(8f2Ub_r)NRWcdaJB`U~;5nx9;)y+NQ|1gP%TJhz||kT>r2j{B%iXXzPzjSA5Y< z^TDd5_JyZX^}S;`qbBe%y!OGC(Z%r(Jjt+XzvUWjc{g!%^&7z#=TBbs3Z9K0Q&GQ# z+_*LEJby^s_Q{toeCXPD{^EwZ1Jg(Q>_z599}QpGzqIsl?ftyJb!U#XMWlzcaQ0Kh z(M1zoOPW6K>iuD1f9KZrUz0lG4KXjBZr}IESH~aTEz?JRyHea%b9BQlX2ROCzQ>mf zsZ_hCIxdrN@O$6SAyAdObE13l#7N=hw6pW)RKqvoHm^N$`EXLptFfZrE5Jd+m2cSw#nJzc5QgDGZxj>rqmQrqQ(Yua(1{YSk6eudZzwxzhB$ngBLn}KHvD-zG5C<*pu7n z<@^`UL}X15W8xU|c;jcR&4o4jx~|7rTaTC@h9P`sTpZ!4lY^P!m;9+u@$^H^xp4mP z-RpP;_RgBQFrSpSeZ(=HKWp<|!_$jdab7s{geho`+_*84z MrM+2QMD-qB0o2VOd;kCd literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Images.png b/BismNormalizer/BismNormalizer/Resources/Images.png new file mode 100644 index 0000000000000000000000000000000000000000..3dc294bfd06045be4de32107570088feff1740ce GIT binary patch literal 4056 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Package.ico b/BismNormalizer/BismNormalizer/Resources/Package.ico new file mode 100644 index 0000000000000000000000000000000000000000..7645b7c212da65aaa8a0de64d2dac18a89278381 GIT binary patch literal 1150 zcmbu8F>ZrE5Jd+m2cSw#nJzc5QgDGZxj>rqmQrqQ(Yua(1{YSk6eudZzwxzhB$ngBLn}KHvD-zG5C<*pu7n z<@^`UL}X15W8xU|c;jcR&4o4jx~|7rTaTC@h9P`sTpZ!4lY^P!m;9+u@$^H^xp4mP z-RpP;_RgBQFrSpSeZ(=HKWp<|!_$jdab7s{geho`+_*84z MrM+2QMD-qB0o2VOd;kCd literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Perspective.png b/BismNormalizer/BismNormalizer/Resources/Perspective.png new file mode 100644 index 0000000000000000000000000000000000000000..d8247ea1a517fc50e93667d37dc411ff9993c2fc GIT binary patch literal 3169 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 Hu0mjflbG(V literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Plus.png b/BismNormalizer/BismNormalizer/Resources/Plus.png new file mode 100644 index 0000000000000000000000000000000000000000..38bb32511720f60d53db534ee99a1dc8432ec294 GIT binary patch literal 2929 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 efZG2T^ceu;UH+mYU2Ql30000`TsqNzZsKk zDajU--4-5V0I1|UivLP2xmjs>S()uH6-!a`6B8163$n4hCoy)6#*OVxM1^^>Tf2|m;%;r z|HjF{9wv*mcHczsJ0>&#&_p`h%SKO3gnA&hZzwS3k zL2uK+I~YuVh%g3e^xqNW)F6jGA_i}s$v3XpO{|YV5M4)%Q|;!}BT}9ae@_ppWX@VM-~d z5DF76u5fwccp=V1p5JqeeHfBYu!pTi%4+_H(~o+Fk)TO*iE!-N-OuRPCu=}`&QV7p zTBSb=Qr`pCur7Yr)dIP(tGTM(m~F1#c_@!N@q07X62M*CT3>mskuq6CT$t6;e%Etozu zOhInZksA*{x<7&d56qlrh!}lIk-R|=BGob3IS!|N@EmYeP~6}zhvJA{r@0VW-nYc@ z)IckxN)At_ED7s$QZeC^2kKCpsd+(+o z{>YQe-;$poVzK90lK6!feE8ZTejebZh`kCqLS#A~$(=ZZXC;ORFNmS4FLonFR~O<3 z#WqKqekQRdq~L-iQ!z`&o)LYh5XPXP2g#b&Q7bmAk8((shFemY#c`UbP5x zYVHOVuIXydYnMlcq^_MSkrqXkcIWA0$Ld%gS z+36WFm<_R+pD$Q7Ve!XqwGvC_!f|nHSTIVWmE+8SoF&m?*hxGDBDO+^ECYkwqrhZ2 zd~!eRx6Fc$8pEogpFFqel*JFp;J{~30v87;H2UBm|N4N$e-KF#9J6XEkJ2Ac%)!yt^>k>M|3+mPCG=vA$%DW;C{+ zLJKIt@iBA7UXuypDr_^dU*~psgi5O3-k0VAKVoQ7N+V5$o{9Xx8Lg%JFMB4}g`~6= zP%S;UG?KEVhB8GrWzTfxQA3*YAqh5ccTU>Ng*Vpfr`FQTqvov<8|FUPTON%3vuL$q zxc43{jEH_G+A0Ii{3vV1woeSlYbSiP%v0;E9!|`uCm_ zIY0JN-S4pnEw{wIy)sk#PQ}_dBR){Fz4OE@T}^jh3{|jx+M?~lIR#N$7$!p_m-UO~ z##6_!&>SBnl%Byr=w)&6^i_*Ll`54AktCT5BL1u65Dp-b_@R)qa6^EIB+W;}$QC*- z%aFk9$r7m{y9M}m|9;Bueg%8pZK_$2HwKE|48R(4OY@QV982r+kS4D`bMtsHM?$0oztD zi(w7bQRuxfDuE|_N$wgNT^6HvTu+Km;4)NwEpeWoXj7@e{Ac_^GtJHmOvPn)%;r9R zNQz#TPMe=13~i*@)9x%-D>59|ZQoA|!o?AdZFW}_bc6H~eOB63Xx5SBIqlo1)Am$+ zxAONlbfUn(=_uX={qqF}W^zjlJM3CJe?vo_E^Z|&wYUFiPPCtgL@iNm9XNGp=KN{e z!r$#?lYCs!f#t8W3xY%cKcSZpbd#I6aCV z5QP}h%!WFAybM`Np*2z;_C$&22ZE*^0(#F)XPAet|ZK`SgwX=J*tu|@-QG0zM zy-%r|!Ouzz=(()WufA45dUJBK{$sCGkF-oG z zPOL8-yZp$1M(bFxBaO>J(frTmu2{tWQXqh2>73#cJRb!xmCRR8hrU`TV!JKcZ>l%S zn`|9k;ip3`o|`Rm-vHjY$!+DA+Y zl--v}_6?e0AEiR+!__igya@cwCQ&{KP{@TKQX}`8#|08*E#KuWwD7dEeD$5iv{DX~ zkexSd?zSrZ;i4d*%VmCxu&D~&PBNKZCV6C8C4s&ijte{t}gDUW9ynsa+nSyzz=;-~M1r02ZCOh3gP4M&0=^}Lp{j2Xp0;A_VFENH98HejQg%}O^PHUv)5VAn~rW# zM^a|TY}dND_6cL_Ow|jLx3L;FYK)TLOhb8m-Zr2f!!TOw^(a)M&JAnmj88u`jUL z46`mcB;-oSfX#1^Z2xddRFH1u?t?_+(IX};G)n)sE_EPiy=!(6CW5+B2PO}Bl`!#f zyOC1dT@>agOtYKZr-6vMy{(8Pppq&by`zO_+lgose)Mw&bM4MxwU*;_2R z7Y9&>v2?pKgs3<1DQ+TQyvNXYpx3=Uk(07^IⅅF|%l_mUGa9q)z13YCB$fuI-h_ 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/ProgressCheck.png b/BismNormalizer/BismNormalizer/Resources/ProgressCheck.png new file mode 100644 index 0000000000000000000000000000000000000000..419f14963c73192a91fbd318f5cd75c4876afaf3 GIT binary patch literal 3300 zcmV))_P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0006HNklNT-i%=X}s@5f7CUWgC^f4MCD@tPhb+2_9S zAAB^P8{lsoo%ap^z?pJDXDCv0DX>KbMj45_Cy7&nQP{#=ygF3oMKP?w zCKLCb>!|OqcBc)W`$8u0;E!Fs3M(Mccy5ecy$UNJz~A_&84Ko`ePlfmsSubXOqY*WQ^2D zu>t~Yktvf^E|}JS5bKiwWyE#dST2~TcO5r0vW0FzVWkku1rzmAj1Yl*I!|bVe(Qp+ zdYYipoGA5y>Q9+Ly iACrmsn*HnF@y7t0w@KYN08^>}0000KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000FENkl1wu4}0i#>0!4BMX(eKY6y}LZ571}5+YJH8X?#yF+$)% zB#rfekeDC|Hbe}dv51I5q9EKDi5dwZ#E5}lVnjitkU)WjmThUfhwgtIFUpo(x;<>c zI5#_)*?*ol-(kLQgp`v1b7=Ye*=YGkb{+W+qoo^dK`~oRc&}k^7?wU&ZiVc)KHeZZLz@-gKrF~!rqkIyd zCu3s}c;-B2D0G<${%0Ggs&5o|2{`}eJgNQl6Q%xJ_J#>h&ut0qNe%hJDu?8fiKBtsGQ9+txLV z^7V%sRW<0lyD=u$3<|fZY6$FT!LF+ATXO+TlZ1mQBHPvtY=PD<*UEr-6|d4C?t6PL zs;>b5s=XLhH3Pzn-r0*MTu$NAjo4MyxEUgRnu~tg8fTp^*7f;c7`j2cE#D718-;+Dw^xyWA zXk3UH%`IXeIOmRK!P9e(>JmF~j94rt`ve&0PoXOVN4ws9lvoZCWgCCV3od1EK&H-~ z#J2657BJc(XvjfREtiO_LKTe+373FSC00jUwg8!o_Z>7?o~vh$()Iqr+y(2s1w+DJ z3Bm@~G;m!vBMtq9NFj#2WYRv*HNx-i!>Ozu6fWsaN#XHk2caGwi{%avp6T<@XiLha|f>+2U_+(^A!;b{P8kJJ9aVtTG#z%)&gk@GlB%ln-bH#CZC>(KjJtBhLE z)7~mi)lH{dUPISqcHmT1Q@VQJkf~-@%s{-d8F%Y)xO_Sbo-$Or3#Tx-X+IMyCyAUL zQOnnxWMbPJgbJK2^JP}Gq4GReZ{Q&4jC%<+e8TA2PcU}uSdp_Im{3J-#2r{v)|S@4`KS>@^`jEaYdCe+)8yF$>J^q@?e%b3OW1OkEKcf}`Gza-_6 zJrrsVswVS>8>ULa)bKyKj4?02PDMq9$opN;yz@iZx@!Z4sdfTB2cgO#;g+KiPih#! z34~v)Cp70N0)YUbP)LmEWfZ-1K}NpXLg#^R(Be_N9tR6dnDZDQG2S1ZT~qpz>aH zUB~P7Qd(L%5-OYVOdt+{V%0N2!X1qsH!?F=!gaU7vtXn02Z*>%E~mh QC;$Ke07*qoM6N<$f&=ZIlmGw# literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/ProgressWarning.png b/BismNormalizer/BismNormalizer/Resources/ProgressWarning.png new file mode 100644 index 0000000000000000000000000000000000000000..dc28f88deaf656ddc666717a0a00d7cd9eb267ea GIT binary patch literal 4306 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 QHvj+t07*qoM6N<$g8zR{y8r+H literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/Table.png b/BismNormalizer/BismNormalizer/Resources/Table.png new file mode 100644 index 0000000000000000000000000000000000000000..884dbf085fac97a4a4f20c2b3b39a4f2d8bbbced GIT binary patch literal 2866 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 QHvj+t07*qoM6N<$g8zR{y8r+H literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Resources/UpdateAction.png b/BismNormalizer/BismNormalizer/Resources/UpdateAction.png new file mode 100644 index 0000000000000000000000000000000000000000..7f07009124df3ccb931ba084a4990129c6a5b2d8 GIT binary patch literal 2880 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 literal 0 HcmV?d00001 diff --git a/BismNormalizer/BismNormalizer/Settings.Designer.cs b/BismNormalizer/BismNormalizer/Settings.Designer.cs new file mode 100644 index 0000000..700c466 --- /dev/null +++ b/BismNormalizer/BismNormalizer/Settings.Designer.cs @@ -0,0 +1,290 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BismNormalizer { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.1.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("localhost|")] + public string TargetServerAutoCompleteEntries { + get { + return ((string)(this["TargetServerAutoCompleteEntries"])); + } + set { + this["TargetServerAutoCompleteEntries"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string TargetCatalog { + get { + return ((string)(this["TargetCatalog"])); + } + set { + this["TargetCatalog"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("localhost|")] + public string SourceServerAutoCompleteEntries { + get { + return ((string)(this["SourceServerAutoCompleteEntries"])); + } + set { + this["SourceServerAutoCompleteEntries"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string SourceCatalog { + get { + return ((string)(this["SourceCatalog"])); + } + set { + this["SourceCatalog"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool InformationalMessagesVisible { + get { + return ((bool)(this["InformationalMessagesVisible"])); + } + set { + this["InformationalMessagesVisible"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool WarningsVisible { + get { + return ((bool)(this["WarningsVisible"])); + } + set { + this["WarningsVisible"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool ToolbarVisible { + get { + return ((bool)(this["ToolbarVisible"])); + } + set { + this["ToolbarVisible"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionRoles { + get { + return ((bool)(this["OptionRoles"])); + } + set { + this["OptionRoles"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionPartitions { + get { + return ((bool)(this["OptionPartitions"])); + } + set { + this["OptionPartitions"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool OptionMeasureDependencies { + get { + return ((bool)(this["OptionMeasureDependencies"])); + } + set { + this["OptionMeasureDependencies"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionPerspectives { + get { + return ((bool)(this["OptionPerspectives"])); + } + set { + this["OptionPerspectives"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool OptionMergePerspectives { + get { + return ((bool)(this["OptionMergePerspectives"])); + } + set { + this["OptionMergePerspectives"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionDisplayFolders { + get { + return ((bool)(this["OptionDisplayFolders"])); + } + set { + this["OptionDisplayFolders"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionActions { + get { + return ((bool)(this["OptionActions"])); + } + set { + this["OptionActions"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionTranslations { + get { + return ((bool)(this["OptionTranslations"])); + } + set { + this["OptionTranslations"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("Default")] + public string OptionProcessingOption { + get { + return ((string)(this["OptionProcessingOption"])); + } + set { + this["OptionProcessingOption"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionTransaction { + get { + return ((bool)(this["OptionTransaction"])); + } + set { + this["OptionTransaction"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool OptionAffectedTables { + get { + return ((bool)(this["OptionAffectedTables"])); + } + set { + this["OptionAffectedTables"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionCultures { + get { + return ((bool)(this["OptionCultures"])); + } + set { + this["OptionCultures"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool OptionMergeCultures { + get { + return ((bool)(this["OptionMergeCultures"])); + } + set { + this["OptionMergeCultures"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionRetainPartitions { + get { + return ((bool)(this["OptionRetainPartitions"])); + } + set { + this["OptionRetainPartitions"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OptionHighDpiLocal { + get { + return ((bool)(this["OptionHighDpiLocal"])); + } + set { + this["OptionHighDpiLocal"] = value; + } + } + } +} diff --git a/BismNormalizer/BismNormalizer/Settings.cs b/BismNormalizer/BismNormalizer/Settings.cs new file mode 100644 index 0000000..8e2c10b --- /dev/null +++ b/BismNormalizer/BismNormalizer/Settings.cs @@ -0,0 +1,27 @@ +namespace BismNormalizer +{ + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + internal sealed partial class Settings { + + public Settings() { + // // To add event handlers for saving and changing settings, uncomment the lines below: + // + // this.SettingChanging += this.SettingChangingEventHandler; + // + // this.SettingsSaving += this.SettingsSavingEventHandler; + // + } + + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { + // Add code to handle the SettingChangingEvent event here. + } + + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { + // Add code to handle the SettingsSaving event here. + } + } +} diff --git a/BismNormalizer/BismNormalizer/Settings.settings b/BismNormalizer/BismNormalizer/Settings.settings new file mode 100644 index 0000000..166a457 --- /dev/null +++ b/BismNormalizer/BismNormalizer/Settings.settings @@ -0,0 +1,72 @@ + + + + + + localhost| + + + + + + localhost| + + + + + + True + + + True + + + True + + + False + + + False + + + True + + + False + + + True + + + False + + + False + + + False + + + Default + + + False + + + True + + + False + + + True + + + False + + + False + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/Settings1.Designer.cs b/BismNormalizer/BismNormalizer/Settings1.Designer.cs new file mode 100644 index 0000000..5cbd9cd --- /dev/null +++ b/BismNormalizer/BismNormalizer/Settings1.Designer.cs @@ -0,0 +1,209 @@ +//Todo2: delete file + + +////------------------------------------------------------------------------------ +//// +//// This code was generated by a tool. +//// Runtime Version:4.0.30319.18052 +//// +//// Changes to this file may cause incorrect behavior and will be lost if +//// the code is regenerated. +//// +////------------------------------------------------------------------------------ + +//namespace BismNormalizer { + + +// [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] +// internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + +// private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + +// public static Settings Default { +// get { +// return defaultInstance; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("localhost|")] +// public string TargetServerAutoCompleteEntries { +// get { +// return ((string)(this["TargetServerAutoCompleteEntries"])); +// } +// set { +// this["TargetServerAutoCompleteEntries"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("")] +// public string TargetCatalog { +// get { +// return ((string)(this["TargetCatalog"])); +// } +// set { +// this["TargetCatalog"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("localhost|")] +// public string SourceServerAutoCompleteEntries { +// get { +// return ((string)(this["SourceServerAutoCompleteEntries"])); +// } +// set { +// this["SourceServerAutoCompleteEntries"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("")] +// public string SourceCatalog { +// get { +// return ((string)(this["SourceCatalog"])); +// } +// set { +// this["SourceCatalog"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("True")] +// public bool InformationalMessagesVisible { +// get { +// return ((bool)(this["InformationalMessagesVisible"])); +// } +// set { +// this["InformationalMessagesVisible"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("True")] +// public bool WarningsVisible { +// get { +// return ((bool)(this["WarningsVisible"])); +// } +// set { +// this["WarningsVisible"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("True")] +// public bool ToolbarVisible { +// get { +// return ((bool)(this["ToolbarVisible"])); +// } +// set { +// this["ToolbarVisible"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionRoles { +// get { +// return ((bool)(this["OptionRoles"])); +// } +// set { +// this["OptionRoles"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionPartitions { +// get { +// return ((bool)(this["OptionPartitions"])); +// } +// set { +// this["OptionPartitions"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("True")] +// public bool OptionMeasureDependencies { +// get { +// return ((bool)(this["OptionMeasureDependencies"])); +// } +// set { +// this["OptionMeasureDependencies"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionPerspectives { +// get { +// return ((bool)(this["OptionPerspectives"])); +// } +// set { +// this["OptionPerspectives"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("True")] +// public bool OptionMergePerspectives { +// get { +// return ((bool)(this["OptionMergePerspectives"])); +// } +// set { +// this["OptionMergePerspectives"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionDisplayFolders { +// get { +// return ((bool)(this["OptionDisplayFolders"])); +// } +// set { +// this["OptionDisplayFolders"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionActions { +// get { +// return ((bool)(this["OptionActions"])); +// } +// set { +// this["OptionActions"] = value; +// } +// } + +// [global::System.Configuration.UserScopedSettingAttribute()] +// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +// [global::System.Configuration.DefaultSettingValueAttribute("False")] +// public bool OptionTranslations { +// get { +// return ((bool)(this["OptionTranslations"])); +// } +// set { +// this["OptionTranslations"] = value; +// } +// } +// } +//} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/BlobKeyEventArgs.cs b/BismNormalizer/BismNormalizer/TabularCompare/BlobKeyEventArgs.cs new file mode 100644 index 0000000..65298bb --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/BlobKeyEventArgs.cs @@ -0,0 +1,35 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + ///

+ /// Initializes data for the Comparison.PasswordPrompt event. + /// + public class BlobKeyEventArgs : EventArgs + { + /// + /// Gets or sets the authentication kind. + /// + public string AuthenticationKind { get; set; } + + /// + /// Gets or sets the name of the data source. + /// + public string DataSourceName { get; set; } + + /// + /// Gets or sets the password. + /// + public string AccountKey { get; set; } + + /// + /// Gets or sets the privacy level. + /// + public string PrivacyLevel { get; set; } + + /// + /// Gets or sets a value indicating if the user cancelled the deployment. + /// + public bool UserCancelled { get; set; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/ComparisonFactory.cs b/BismNormalizer/BismNormalizer/TabularCompare/ComparisonFactory.cs new file mode 100644 index 0000000..709ea02 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/ComparisonFactory.cs @@ -0,0 +1,91 @@ +using Microsoft.AnalysisServices; +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Serialization; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Class for instantiation of Core.Comparison objects using simple factory design pattern. + /// + public static class ComparisonFactory + { + // Factory pattern: https://msdn.microsoft.com/en-us/library/orm-9780596527730-01-05.aspx + + private static List _supportedCompatibilityLevels = new List() { 1100, 1103, 1200, 1400 }; + + /// + /// Uses factory design pattern to return an object of type Core.Comparison, which is instantiated using MultidimensionalMetadata.Comparison or TabularMeatadata.Comparison depending on SSAS compatibility level. Use this overload when running in Visual Studio. + /// + /// ComparisonInfo object for the comparison. + /// If use decides not to close .bim file(s) in Visual Studio, returns true. + /// Core.Comparison object + public static Comparison CreateComparison(ComparisonInfo comparisonInfo, out bool userCancelled) + { + //This overload is for running in Visual Studio, so can set PromptForDatabaseProcessing = true + comparisonInfo.PromptForDatabaseProcessing = true; + + // Need to ensure compatibility levels get initialized here (instead of comparisonInfo initialization properties). This also serves to prep databases on workspace server while finding compatibility levels + comparisonInfo.InitializeCompatibilityLevels(out userCancelled); + if (userCancelled) + { + return null; + } + + return CreateComparisonInitialized(comparisonInfo); + } + + /// + /// Uses factory design pattern to return an object of type Core.Comparison, which is instantiated using MultidimensionalMetadata.Comparison or TabularMeatadata.Comparison depending on SSAS compatibility level. + /// + /// Full path to the BSMN file. + /// Core.Comparison object + public static Comparison CreateComparison(string bsmnFile) + { + ComparisonInfo comparisonInfo = ComparisonInfo.DeserializeBsmnFile(bsmnFile); + return CreateComparison(comparisonInfo); + } + + /// + /// Uses factory design pattern to return an object of type Core.Comparison, which is instantiated using MultidimensionalMetadata.Comparison or TabularMeatadata.Comparison depending on SSAS compatibility level. + /// + /// ComparisonInfo object for the comparison. + /// Core.Comparison object + public static Comparison CreateComparison(ComparisonInfo comparisonInfo) + { + comparisonInfo.InitializeCompatibilityLevels(); + return CreateComparisonInitialized(comparisonInfo); + } + + private static Comparison CreateComparisonInitialized(ComparisonInfo comparisonInfo) + { + if (comparisonInfo.SourceCompatibilityLevel != comparisonInfo.TargetCompatibilityLevel && !(comparisonInfo.SourceCompatibilityLevel == 1200 && comparisonInfo.TargetCompatibilityLevel == 1400)) + { + throw new ConnectionException($"This combination of mixed compatibility levels is not supported.\nSource is {Convert.ToString(comparisonInfo.SourceCompatibilityLevel)} and target is {Convert.ToString(comparisonInfo.TargetCompatibilityLevel)}."); + } + + if (comparisonInfo.SourceDirectQuery != comparisonInfo.TargetDirectQuery) + { + throw new ConnectionException($"Mixed DirectQuery settings are not supported.\nSource is {(comparisonInfo.SourceDirectQuery ? "On" : "Off")} and target is {(comparisonInfo.TargetDirectQuery ? "On" : "Off")}."); + } + + //We know both models have same compatibility level, but is it supported? + if (!_supportedCompatibilityLevels.Contains(comparisonInfo.SourceCompatibilityLevel)) + { + throw new ConnectionException($"Models have compatibility level of {Convert.ToString(comparisonInfo.SourceCompatibilityLevel)}, which is not supported by this version of BISM Normalizer.\nPlease check http://bism-normalizer.com/purchase for other versions."); + } + + if (comparisonInfo.SourceCompatibilityLevel >= 1200) + { + return new TabularMetadata.Comparison(comparisonInfo); + } + else + { + return new MultidimensionalMetadata.Comparison(comparisonInfo); + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/ComparisonInfo.cs b/BismNormalizer/BismNormalizer/TabularCompare/ComparisonInfo.cs new file mode 100644 index 0000000..50d9d1f --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/ComparisonInfo.cs @@ -0,0 +1,238 @@ +using EnvDTE; +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Information about the comparison. This is serialized/deserialized to/from the BSMN file. + /// + public class ComparisonInfo + { + private ConnectionInfo _connectionInfoSource; + private ConnectionInfo _connectionInfoTarget; + private OptionsInfo _optionsInfo; + private SkipSelectionCollection _skipSelectionCollection; + private int _sourceCompatibilityLevel; + private int _targetCompatibilityLevel; + private bool _sourceDirectQuery; + private bool _targetDirectQuery; + private bool _promptForDatabaseProcessing; + + /// + /// Initializes a new instance of the ComparisonInfo class. + /// + /// + public ComparisonInfo() + { + _connectionInfoSource = new ConnectionInfo(); + _connectionInfoTarget = new ConnectionInfo(); + _optionsInfo = new OptionsInfo(); + _skipSelectionCollection = new SkipSelectionCollection(); + } + + #region Properties + + /// + /// Information about the source connection. + /// + public ConnectionInfo ConnectionInfoSource + { + get { return _connectionInfoSource; } + set { _connectionInfoSource = value; } + } + + /// + /// Information about the target connection. + /// + public ConnectionInfo ConnectionInfoTarget + { + get { return _connectionInfoTarget; } + set { _connectionInfoTarget = value; } + } + + /// + /// Information about the options selected for the comparison. + /// + public OptionsInfo OptionsInfo + { + get { return _optionsInfo; } + set { _optionsInfo = value; } + } + + /// + /// Collection of SkipSelection objects. + /// + public SkipSelectionCollection SkipSelections + { + get { return _skipSelectionCollection; } + set { _skipSelectionCollection = value; } + } + + /// + /// SSAS compatibility level for the source tabular model. + /// + [XmlIgnore()] + public int SourceCompatibilityLevel => _sourceCompatibilityLevel; + + /// + /// SSAS compatibility level for the target tabular model. + /// + [XmlIgnore()] + public int TargetCompatibilityLevel => _targetCompatibilityLevel; + + /// + /// Flag depending on whehter source tabular model is in DirectQuery mode. + /// + [XmlIgnore()] + public bool SourceDirectQuery => _sourceDirectQuery; + + /// + /// Flag depending on whehter target tabular model is in DirectQuery mode. + /// + [XmlIgnore()] + public bool TargetDirectQuery => _targetDirectQuery; + + /// + /// Flag is false if simple database deployment occurs without processing, and without raising the Comparison.DatabaseDeployment event. Typically set for execution from command line. + /// + [XmlIgnore()] + public bool PromptForDatabaseProcessing + { + get { return _promptForDatabaseProcessing; } + set { _promptForDatabaseProcessing = value; } + } + + #endregion + + /// + /// Deserialize BSMN file into instance of ComparisonInfo. + /// + /// BSMN file to be deserialized. + /// Deserialized instance of ComparisonInfo. + public static ComparisonInfo DeserializeBsmnFile(string bsmnFile) + { + if (!File.Exists(bsmnFile)) + { + throw new FileNotFoundException($"File not found {bsmnFile}."); + } + XmlSerializer reader = new XmlSerializer(typeof(ComparisonInfo)); + StreamReader file = new StreamReader(bsmnFile); + return (ComparisonInfo)reader.Deserialize(file); + } + + /// + /// Finds models' compatibility levels (and preps databases on workspace servers for comparison). This overload to be used when client is not Visual Studio - e.g. command line. + /// + /// + /// + public void InitializeCompatibilityLevels() + { + ConnectionInfoSource.InitializeCompatibilityLevel(); + ConnectionInfoTarget.InitializeCompatibilityLevel(); + + PopulateDatabaseProperties(); + } + + /// + /// Finds model compatibility levels (and preps databases on workspace servers for comparison). This overload to be used when running in Visual Studio. Allows user to cancel if doesn't want to close .bim file(s). + /// + /// + /// + /// + public void InitializeCompatibilityLevels(out bool userCancelled) + { + //Check if any open bim files that need to be closed + bool closedSourceBimFile; + bool closedTargetBimFile; + CloseProjectBimFiles(out closedSourceBimFile, out closedTargetBimFile, out userCancelled); + if (userCancelled) + { + return; + } + + //Passing closedSourceBimFile so doesn't run bim file script if user just closed it (more efficient) + ConnectionInfoSource.InitializeCompatibilityLevel(closedSourceBimFile); + ConnectionInfoTarget.InitializeCompatibilityLevel(closedTargetBimFile); + + PopulateDatabaseProperties(); + } + + private void PopulateDatabaseProperties() + { + _sourceCompatibilityLevel = ConnectionInfoSource.CompatibilityLevel; + _targetCompatibilityLevel = ConnectionInfoTarget.CompatibilityLevel; + + _sourceDirectQuery = ConnectionInfoSource.DirectQuery; + _targetDirectQuery = ConnectionInfoTarget.DirectQuery; + } + + private void CloseProjectBimFiles(out bool closeSourceBimFile, out bool closeTargetBimFile, out bool userCancelled) + { + closeSourceBimFile = false; + closeTargetBimFile = false; + userCancelled = false; + List projectItemsToClose = new List(); + + if (ConnectionInfoSource.UseProject) + { + foreach (ProjectItem sourceProjectItem in ConnectionInfoSource.Project.ProjectItems) + { + if (sourceProjectItem.Name.EndsWith(".bim") && sourceProjectItem.IsOpen) + { + projectItemsToClose.Add(sourceProjectItem); + closeSourceBimFile = true; + break; + } + } + } + + if (ConnectionInfoTarget.UseProject) + { + foreach (ProjectItem targetProjectItem in ConnectionInfoTarget.Project.ProjectItems) + { + if (targetProjectItem.Name.EndsWith(".bim") && targetProjectItem.IsOpen) + { + // check if user has source/target as the same project + if (!(projectItemsToClose.Count == 1 && projectItemsToClose[0].Document.FullName == targetProjectItem.Document.FullName)) + { + projectItemsToClose.Add(targetProjectItem); + closeTargetBimFile = true; + break; + } + } + } + } + if (projectItemsToClose.Count > 0) + { + string filesToClose = ""; + foreach (ProjectItem projectItemToClose in projectItemsToClose) + { + filesToClose += $"\n- {projectItemToClose.ContainingProject.Name.Replace(".smproj", "")}\\{projectItemToClose.Name}"; + } + + if (MessageBox.Show($"BISM Normalizer needs to close the following file(s) that are\nopen in Visual Studio. Do you want to continue?{filesToClose}", "BISM Normalizer", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) + { + userCancelled = true; + } + else + { + foreach (ProjectItem projectItemToClose in projectItemsToClose) + { + if (projectItemToClose.Document.Saved) + { + projectItemToClose.Document.Close(vsSaveChanges.vsSaveChangesNo); + } + else + { + projectItemToClose.Document.Close(vsSaveChanges.vsSaveChangesYes); + } + } + } + } + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/ConnectionInfo.cs b/BismNormalizer/BismNormalizer/TabularCompare/ConnectionInfo.cs new file mode 100644 index 0000000..479a931 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/ConnectionInfo.cs @@ -0,0 +1,513 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using System.Security.Principal; +using Microsoft.AnalysisServices; +using EnvDTE; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System.Linq; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Information about a connection. This is serialized/deserialized to/from the BSMN file. + /// + public class ConnectionInfo + { + #region Private Variables + + private bool _useProject = false; + private string _serverName; + private string _databaseName; + private string _projectName; + private string _projectFile; + private int _compatibilityLevel; + private bool _directQuery; + private string _bimFileFullName; + private EnvDTE.Project _project; + private string _deploymentServerName; + private string _deploymentServerDatabase; + private string _deploymentServerCubeName; + private DirectoryInfo _projectDirectoryInfo; + + #endregion + + /// + /// Initializes a new instance of the ConnectionInfo class. + /// + public ConnectionInfo() + { + } + + /// + /// A Boolean specifying whether the connection represents a project in Visual Studio or a database on a server. + /// + public bool UseProject + { + get { return _useProject; } + set { _useProject = value; } + } + + /// + /// Name of the server on which the tabular model resides. + /// + public string ServerName + { + get { return _serverName; } + set { _serverName = value; } + } + + /// + /// The name of the database for the connection. + /// + public string DatabaseName + { + get { return _databaseName; } + set { _databaseName = value; } + } + + /// + /// The name of the project for the connection. + /// + public string ProjectName + { + get { return _projectName; } + set { _projectName = value; } + } + + /// + /// The full path and name of the project file. Used for running in command-line mode. + /// + public string ProjectFile + { + get { return _projectFile; } + set { _projectFile = value; } + } + + /// + /// The SSAS compatibility level for the connection. + /// + [XmlIgnore()] + public int CompatibilityLevel => _compatibilityLevel; + + /// + /// A Boolean specifying whether the tabular model for the connection is running in DirectQuery mode. + /// + [XmlIgnore()] + public bool DirectQuery => _directQuery; + + /// + /// Project running in Visual Studio. + /// + [XmlIgnore()] + public EnvDTE.Project Project + { + get { return _project; } + set { _project = value; } + } + + /// + /// Full path to the BIM file for the project. + /// + [XmlIgnore()] + public string BimFileFullName => _bimFileFullName; + + /// + /// The deployment server from the project file. + /// + [XmlIgnore()] + public string DeploymentServerName => _deploymentServerName; + + /// + /// The deployment database from the project file. + /// + [XmlIgnore()] + public string DeploymentServerDatabase => _deploymentServerDatabase; + + /// + /// The deployment cube from the project file. + /// + [XmlIgnore()] + public string DeploymentServerCubeName => _deploymentServerCubeName; + + private void ReadSettingsFile() + { + FileInfo[] files = _projectDirectoryInfo.GetFiles("*.settings", SearchOption.TopDirectoryOnly); + FileInfo settingsFile = null; + + string currentUserName = WindowsIdentity.GetCurrent().Name; + int startPos1 = currentUserName.IndexOf("\\") + 1; + if (startPos1 != -1) + { + currentUserName = currentUserName.Substring(startPos1); + } + + foreach (FileInfo file in files) + { + if (file.Name.Contains(currentUserName)) + { + settingsFile = file; + break; + } + } + + if (settingsFile == null) + { + if (_project == null) + { + //Probably running in command-line mode + if (String.IsNullOrEmpty(_serverName) || String.IsNullOrEmpty(_databaseName)) + { + throw new ConnectionException($"Project {_projectName} Server Name and/or Database Name not populated.\nGenerate a new BSMN in Visual Studio."); + } + else + { + return; + } + } + else + { + throw new ConnectionException($"Could not read workspace settings file for Project {_projectName}.\nGenerate a new settings file by opening the .bim file in Visual Studio."); + } + } + + /* can't use xmlreader or xmldoc because throws error saying "Data at the root level is invalid" + XmlDocument document = new XmlDocument(); + document.Load(settingsFile.FullName); + XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable); + nsmgr.AddNamespace("myns1", "http://schemas.microsoft.com/myns1"); + + XmlNode serverNameNode = document.SelectSingleNode("//myns1:ServerName", nsmgr); + if (serverNameNode == null) throw new ConnectionException("Could not read workspace server name from settings file for Project " + _projectName); + _serverName = serverNameNode.InnerText; + + XmlNode databaseNameNode = document.SelectSingleNode("//myns1:DatabaseName", nsmgr); + if (databaseNameNode == null) throw new ConnectionException("Could not read workspace database name from settings file for Project " + _projectName); + _databaseName = databaseNameNode.InnerText; + */ + + using (StreamReader settingsFileStreamReader = settingsFile.OpenText()) + { + string settingsFileContents = settingsFileStreamReader.ReadToEnd(); + + int startPos = settingsFileContents.IndexOf(""); + int endPos = settingsFileContents.IndexOf(""); + if (startPos != -1 && endPos != -1 && startPos < endPos) + { + startPos = startPos + 12; + _serverName = settingsFileContents.Substring(startPos, endPos - startPos); + } + else + { + throw new ConnectionException("Could not read workspace server name from settings file for Project " + _projectName); + } + + startPos = settingsFileContents.IndexOf(""); + endPos = settingsFileContents.IndexOf(""); + if (startPos != -1 && endPos != -1 && startPos < endPos) + { + startPos = startPos + 14; + _databaseName = settingsFileContents.Substring(startPos, endPos - startPos); + } + else + { + throw new ConnectionException("Could not read workspace database name from settings file for Project " + _projectName); + } + } + } + + public void ReadProjectFile() + { + XmlDocument projectFileDoc = new XmlDocument(); + projectFileDoc.Load(_projectFile); + XmlNamespaceManager nsmgr = new XmlNamespaceManager(projectFileDoc.NameTable); + nsmgr.AddNamespace("myns1", "http://schemas.microsoft.com/developer/msbuild/2003"); + + //Populate deployment server properties + XmlNode deploymentServerNameNode = null; + XmlNode deploymentServerDatabaseNode = null; + XmlNode deploymentServerCubeNameNode = null; + + //Try to populate from active configuration + if (_project != null) + { + string configurationName = _project.ConfigurationManager?.ActiveConfiguration?.ConfigurationName; + + if (!String.IsNullOrEmpty(configurationName)) + { + deploymentServerNameNode = projectFileDoc.SelectSingleNode($"//myns1:PropertyGroup[contains(@Condition,'{configurationName}')]/myns1:DeploymentServerName", nsmgr); + deploymentServerDatabaseNode = projectFileDoc.SelectSingleNode($"//myns1:PropertyGroup[contains(@Condition,'{configurationName}')]/myns1:DeploymentServerDatabase", nsmgr); + deploymentServerCubeNameNode = projectFileDoc.SelectSingleNode($"//myns1:PropertyGroup[contains(@Condition,'{configurationName}')]/myns1:DeploymentServerCubeName", nsmgr); + } + } + + //If not populated - e.g. in command-line mode, get values without Condition attribute filter + if (deploymentServerNameNode == null) + { + deploymentServerNameNode = projectFileDoc.SelectSingleNode("//myns1:PropertyGroup/myns1:DeploymentServerName", nsmgr); + } + if (deploymentServerDatabaseNode == null) + { + deploymentServerDatabaseNode = projectFileDoc.SelectSingleNode("//myns1:PropertyGroup/myns1:DeploymentServerDatabase", nsmgr); + } + if (deploymentServerCubeNameNode == null) + { + deploymentServerCubeNameNode = projectFileDoc.SelectSingleNode("//myns1:PropertyGroup/myns1:DeploymentServerCubeName", nsmgr); + } + + _deploymentServerName = deploymentServerNameNode?.InnerText; + _deploymentServerDatabase = deploymentServerDatabaseNode?.InnerText; + _deploymentServerCubeName = deploymentServerCubeNameNode?.InnerText; + + //Get path to BIM file + if (_project != null) + { + foreach (ProjectItem projectItem in _project.ProjectItems) + { + if (projectItem.Name.EndsWith(".bim") && projectItem.FileCount > 0) + { + _bimFileFullName = projectItem.FileNames[0]; + break; + } + } + } + else + { + //Probably running in command-line mode + XmlNodeList compileNodes = projectFileDoc.SelectNodes("//myns1:ItemGroup/myns1:Compile", nsmgr); + if (compileNodes != null) + { + foreach (XmlNode compileNode in compileNodes) + { + if (compileNode.Attributes["Include"] != null && compileNode.Attributes["Include"].Value.ToUpper().EndsWith(".bim".ToUpper())) + { + FileInfo[] files = _projectDirectoryInfo.GetFiles(compileNode.Attributes["Include"].Value, SearchOption.TopDirectoryOnly); + if (files.Length > 0) + { + _bimFileFullName = files[0].FullName; + break; + } + } + } + } + } + } + + /// + /// This method ensures the tabular model is online and populates the CompatibilityLevel property. + /// + /// A Boolean specifying if the user cancelled the comparison. For the case where running in Visual Studio, the user has the option of cancelling if the project BIM file is open. + public void InitializeCompatibilityLevel(bool closedBimFile = false) + { + if (UseProject) + { + //Initialize _projectDirectoryInfo + FileInfo projectFileInfo; + if (_project == null) + { + //Probably running in command-line mode + projectFileInfo = new FileInfo(_projectFile); + } + else + { + projectFileInfo = new FileInfo(_project.FullName); + } + _projectDirectoryInfo = new DirectoryInfo(projectFileInfo.Directory.FullName); + + //Read settings file to get workspace server/db + ReadSettingsFile(); + + //Read project file to get deployment server/cube names, and bim file + ReadProjectFile(); + } + + Server amoServer = new Server(); + try + { + amoServer.Connect("Provider=MSOLAP;Data Source=" + this.ServerName); + } + catch (ConnectionException) when (UseProject) + { + //See if can find integrated workspace server + + bool foundServer = false; + + string tempDataDir = Path.GetTempPath() + @"Microsoft\Microsoft SQL Server\OLAP\LocalServer\Data"; + if (Directory.Exists(tempDataDir)) + { + var subDirs = Directory.GetDirectories(tempDataDir).OrderByDescending(d => new DirectoryInfo(d).CreationTime); //Need to order by descending in case old folders hanging around when VS was killed and SSDT didn't get a chance to clean up after itself + foreach (string subDir in subDirs) + { + string[] iniFilePath = Directory.GetFiles(subDir, "msmdsrv.ini"); + if (iniFilePath.Length == 1 && File.ReadAllText(iniFilePath[0]).Contains("" + _projectDirectoryInfo.FullName + @"\bin\Data")) //Todo: proper xml lookup + { + //Assuming this must be the folder, so now get the port number + string[] portFilePath = Directory.GetFiles(subDir, "msmdsrv.port.txt"); + if (portFilePath.Length == 1) + { + string port = File.ReadAllText(portFilePath[0]).Replace("\0", ""); + this.ServerName = $"localhost:{Convert.ToString(port)}"; + amoServer.Connect("Provider=MSOLAP;Data Source=" + this.ServerName); + foundServer = true; + break; + } + } + } + } + + if (!foundServer) + throw; + } + + ////non-admins can't see any ServerProperties: social.msdn.microsoft.com/Forums/sqlserver/en-US/3d0bf49c-9034-4416-9c51-77dc32bf8b73/determine-current-user-permissionscapabilities-via-amo-or-xmla + //if (!(amoServer.ServerProperties.Count > 0)) //non-admins can't see any ServerProperties + //{ + // throw new Microsoft.AnalysisServices.ConnectionException($"Current user {WindowsIdentity.GetCurrent().Name} is not an administrator on the Analysis Server " + this.ServerName); + //} + + if (amoServer.ServerMode != ServerMode.Tabular) + { + throw new ConnectionException($"Analysis Server {this.ServerName} is not running in Tabular mode"); + } + + Database tabularDatabase = amoServer.Databases.FindByName(this.DatabaseName); + if (tabularDatabase == null) + { + if (!this.UseProject) + { + throw new ConnectionException("Could not connect to database " + this.DatabaseName); + } + else + { + /* Check if folder exists using SystemGetSubdirs. If so attach. If not, do nothing - when execute BIM file below will create automatically. + Using XMLA to run SystemGetSubdirs rather than ADOMD.net here don't want a reference to ADOMD.net Dll. + Also, can't use Server.Execute method because it only takes XMLA execute commands (as opposed to XMLA discover commands), so need to submit the full soap envelope + */ + + string dataDir = amoServer.ServerProperties["DataDir"].Value; + if (dataDir.EndsWith("\\")) dataDir = dataDir.Substring(0, dataDir.Length - 1); + string commandStatement = String.Format("SystemGetSubdirs '{0}'", dataDir); + XmlNodeList rows = Core.Comparison.ExecuteXmlaCommand(amoServer, "", commandStatement); + + string dbDir = ""; + foreach (XmlNode row in rows) + { + XmlNode dirNode = null; + XmlNode allowedNode = null; + + foreach (XmlNode childNode in row.ChildNodes) + { + if (childNode.Name == "Dir") + { + dirNode = childNode; + } + else if (childNode.Name == "Allowed") + { + allowedNode = childNode; + } + } + + if (dirNode != null && allowedNode != null && dirNode.InnerText.Length >= this.DatabaseName.Length && dirNode.InnerText.Substring(0, this.DatabaseName.Length) == this.DatabaseName && allowedNode.InnerText.Length > 0 && allowedNode.InnerText == "1") + { + dbDir = dataDir + "\\" + dirNode.InnerText; + break; + } + } + + if (dbDir != "") + { + //attach + amoServer.Attach(dbDir); + amoServer.Refresh(); + tabularDatabase = amoServer.Databases.FindByName(this.DatabaseName); + } + } + } + + if (this.UseProject) + { + //_bimFileFullName = GetBimFileFullName(); + if (String.IsNullOrEmpty(_bimFileFullName)) + { + throw new ConnectionException("Could not load BIM file for Project " + this.ProjectName); + } + + if (!closedBimFile) //If just closed BIM file, no need to execute it + { + //Execute BIM file contents as script on workspace database + + //We don't know the compatibility level yet, so try parsing json, if fail, try xmla ... + try + { + //Replace "SemanticModel" with db name. + JObject jDocument = JObject.Parse(File.ReadAllText(_bimFileFullName)); + + if (jDocument["name"] == null || jDocument["id"] == null) + { + throw new ConnectionException("Could not read JSON in BIM file " + _bimFileFullName); + } + + jDocument["name"] = DatabaseName; + jDocument["id"] = DatabaseName; + + //Todo: see if Tabular helper classes for this once documentation available after CTP + string command = +$@"{{ + ""createOrReplace"": {{ + ""object"": {{ + ""database"": ""{DatabaseName}"" + }}, + ""database"": {jDocument.ToString()} + }} +}} +"; + amoServer.Execute(command); + + } + catch (JsonReaderException) + { + //Replace "SemanticModel" with db name. Could do a global replace, but just in case it's not called SemanticModel, use dom instead + //string xmlaScript = File.ReadAllText(xmlaFileFullName); + XmlDocument document = new XmlDocument(); + document.Load(_bimFileFullName); + XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable); + nsmgr.AddNamespace("myns1", "http://schemas.microsoft.com/analysisservices/2003/engine"); + + XmlNode objectDatabaseIdNode = document.SelectSingleNode("//myns1:Object/myns1:DatabaseID", nsmgr); + XmlNode objectDefinitionDatabaseIdNode = document.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:ID", nsmgr); + XmlNode objectDefinitionDatabaseNameNode = document.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:Name", nsmgr); + + if (objectDatabaseIdNode == null || objectDefinitionDatabaseIdNode == null || objectDefinitionDatabaseNameNode == null) + { + throw new ConnectionException("Could not access XMLA in BIM file " + _bimFileFullName); + } + + objectDatabaseIdNode.InnerText = DatabaseName; + objectDefinitionDatabaseIdNode.InnerText = DatabaseName; + objectDefinitionDatabaseNameNode.InnerText = DatabaseName; + + //1103, 1100 projects store the xmla as Alter (equivalent to createOrReplace), so just need to execute + amoServer.Execute(document.OuterXml); + } + } + + //need next lines in case just created the db using the Execute method + //amoServer.Refresh(); //todo workaround for bug 9719887 on 3/10/17 need to disconnect and reconnect + amoServer.Disconnect(); + amoServer.Connect("Provider=MSOLAP;Data Source=" + this.ServerName); + + tabularDatabase = amoServer.Databases.FindByName(this.DatabaseName); + } + + if (tabularDatabase == null) + { + throw new ConnectionException($"Can not load/find database {this.DatabaseName}."); + } + _compatibilityLevel = tabularDatabase.CompatibilityLevel; + _directQuery = ((tabularDatabase.Model != null && tabularDatabase.Model.DefaultMode == Microsoft.AnalysisServices.Tabular.ModeType.DirectQuery) || + tabularDatabase.DirectQueryMode == DirectQueryMode.DirectQuery || tabularDatabase.DirectQueryMode == DirectQueryMode.InMemoryWithDirectQuery || tabularDatabase.DirectQueryMode == DirectQueryMode.DirectQueryWithInMemory); + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/Core/Comparison.cs b/BismNormalizer/BismNormalizer/TabularCompare/Core/Comparison.cs new file mode 100644 index 0000000..5f2f6c7 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/Core/Comparison.cs @@ -0,0 +1,656 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using Excel = Microsoft.Office.Interop.Excel; +using Microsoft.AnalysisServices; +using System.Xml; + +namespace BismNormalizer.TabularCompare.Core +{ + /// + /// Represents a comparison of two SSAS tabular models. This class is extended by BismNormalizer.TabularCompare.MultidimensionalMetadata.Comparison and BismNormalizer.TabularCompare.TabularMetadata.Comparison depending on SSAS compatibility level. + /// + public abstract class Comparison : IDisposable + { + #region Protetced/Private Members + + protected List _comparisonObjects; + protected ComparisonInfo _comparisonInfo; + protected int _comparisonObjectCount = 0; + private int _compatibilityLevel; + + #endregion + + #region Properties + + /// + /// Collection of ComparisonObject instances. + /// + public List ComparisonObjects + { + get { return _comparisonObjects; } + set { _comparisonObjects = value; } + } + + /// + /// Compatibility level of the SSAS tabular models for this comparison. + /// + public int CompatibilityLevel => _compatibilityLevel; + + #endregion + + #region Events + + /// + /// Occurs when a validation message is surfaced, either warning or informational. + /// + public event EventHandler ValidationMessage; + + /// + /// Occurs when all messages for a validation are done, and need to dynamically resize the headers. + /// + public event EventHandler ResizeValidationHeaders; + + /// + /// Invokes the ValidationMessage event. + /// + /// ValidationMessageEventArgs object. + public virtual void OnValidationMessage(ValidationMessageEventArgs e) => ValidationMessage?.Invoke(this, e); + + /// + /// Invokes the ResizeValidationHeaders event. + /// + /// EventArgs object. + public virtual void OnResizeValidationHeaders(EventArgs e) => ResizeValidationHeaders?.Invoke(this, e); + + /// + /// Occurs during database deployment when a password is required for an impersonated account. + /// + public event EventHandler PasswordPrompt; + + /// + /// Invokes the PasswordPrompt event. + /// + /// ValidationMessageEventArgs object. + public virtual void OnPasswordPrompt(PasswordPromptEventArgs e) => PasswordPrompt?.Invoke(this, e); + + /// + /// Occurs during database deployment when a password is required for a blob key. + /// + public event EventHandler BlobKeyPrompt; + + /// + /// Invokes the BlobKeyPrompt event. + /// + /// ValidationMessageEventArgs object. + public virtual void OnBlobKeyPrompt(BlobKeyEventArgs e) => BlobKeyPrompt?.Invoke(this, e); + + /// + /// Occurs when a database is ready for deployment. + /// + public event EventHandler DatabaseDeployment; + + /// + /// Invokes the DatabaseDeployment event. + /// + /// DatabaseDeploymentEventArgs object. + public virtual void OnDatabaseDeployment(DatabaseDeploymentEventArgs e) => DatabaseDeployment?.Invoke(this, e); + + /// + /// Occurs when a deployment status message is surfaced. + /// + public event EventHandler DeploymentMessage; + + /// + /// Invokes the DeploymentMessage event. + /// + /// DeploymentMessageEventArgs object. + public virtual void OnDeploymentMessage(DeploymentMessageEventArgs e) => DeploymentMessage?.Invoke(this, e); + + /// + /// Occurs when a database deployment is complete. + /// + public event EventHandler DeploymentComplete; + + /// + /// Invokes the DeploymentComplete event. + /// + /// DeploymentCompleteEventArgs object. + public virtual void OnDeploymentComplete(DeploymentCompleteEventArgs e) => DeploymentComplete?.Invoke(this, e); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the Comparison class using a ComparisonInfo object. + /// + /// ComparisonInfo object typically deserialized from a BSMN file. + public Comparison(ComparisonInfo comparisonInfo) + { + _comparisonObjects = new List(); + _comparisonInfo = comparisonInfo; + //Supported compatibility level - with matching source/target compatibility levels - has already been validated at this point, so can safely use SourceCompatibilityLevel + _compatibilityLevel = comparisonInfo.SourceCompatibilityLevel; + } + + #endregion + + #region Abstract Methods + + /// + /// Connect to source and target tabular models, and instantiate their properties. + /// + public abstract void Connect(); + + /// + /// Disconnect from source and target tabular models. + /// + public abstract void Disconnect(); + + /// + /// Validate selection of actions to perform on target tabular model. Warnings and informational messages are provided by invoking ShowStatusMessageCallBack. + /// + public abstract void ValidateSelection(); + + /// + /// Update target tabular model with changes defined by actions in ComparisonObject instances. + /// + /// Flag to indicate whether update was successful. + public abstract bool Update(); + + /// + /// Gets a collection of ProcessingTable objects depending on Process Affected Tables option. + /// + /// Collection of ProcessingTable objects. + public abstract ProcessingTableCollection GetTablesToProcess(); + + /// + /// Deploy database to target server and perform processing if required. + /// + /// + public abstract void DatabaseDeployAndProcess(ProcessingTableCollection tablesToProcess); + + /// + /// Stop processing of deployed database. + /// + public abstract void StopProcessing(); + + /// + /// Generate script of target database including changes. + /// + /// Script. + public abstract string ScriptDatabase(); + + /// + /// Compare source and target tabular models. + /// + public abstract void CompareTabularModels(); + + #endregion + + /// + /// Finds ComparisonObject matching search criteria. + /// + /// + /// + /// + /// + /// + /// ComparisonObject matching search criteria. If none found, null is returned. + public ComparisonObject FindComparisonObjectByObjectInternalNames(string sourceObjectName, string sourceObjectId, string targetObjectName, string targetObjectId, ComparisonObjectType objType) + { + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + ComparisonObject matchedComparisonObj; + if (CheckComparisonObject(comparisonObject, sourceObjectName, sourceObjectId, targetObjectName, targetObjectId, objType, out matchedComparisonObj)) + { + return matchedComparisonObj; + } + } + // if didn't find a match, return null + return null; + } + + private bool CheckComparisonObject(ComparisonObject comparisonObject, string sourceObjectName, string sourceObjectId, string targetObjectName, string targetObjectId, ComparisonObjectType objType, out ComparisonObject matchedComparisonObj) + { + if (comparisonObject.SourceObjectName == sourceObjectName && comparisonObject.SourceObjectInternalName == sourceObjectId && comparisonObject.TargetObjectName == targetObjectName && comparisonObject.TargetObjectInternalName == targetObjectId && comparisonObject.ComparisonObjectType == objType) + { + matchedComparisonObj = comparisonObject; + return true; + } + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (CheckComparisonObject(childComparisonObject, sourceObjectName, sourceObjectId, targetObjectName, targetObjectId, objType, out matchedComparisonObj)) + { + if (matchedComparisonObj == null) + matchedComparisonObj = childComparisonObject; + return true; + } + } + // if didn't find a match, return null + matchedComparisonObj = null; + return false; + } + + /// + /// Generate Excel report of differences. + /// + /// + public void ReportDifferences(ProgressBar progBar) + { + try + { + progBar.Maximum = _comparisonObjectCount; + progBar.Value = 0; + + Excel.Application App = new Excel.Application(); + Excel.Workbook Wb = App.Workbooks.Add(); + //Wb.Sheets[2].Delete(); + //Wb.Sheets[1].Delete(); + Excel.Worksheet Ws = default(Excel.Worksheet); + Ws = Wb.ActiveSheet; + Ws.Name = "Bism Normalizer Report"; + int row = 1, lastDataSourceRow = -1, lastTableRow = -1; + + // set up headers + Ws.Cells[row, 1].Value = "Type"; + Ws.Columns[1].ColumnWidth = 20; + Ws.Cells[row, 2].Value = "Source Object Name"; + Ws.Columns[2].ColumnWidth = 41; + Ws.Cells[row, 3].Value = "Status"; + Ws.Columns[3].ColumnWidth = 18; + Ws.Cells[row, 4].Value = "Target Object Name"; + Ws.Columns[4].ColumnWidth = 41; + Ws.Range["A1:D1"].Select(); + Ws.Application.Selection.Font.Bold = true; + + //set up grouping + Ws.Outline.AutomaticStyles = false; + Ws.Outline.SummaryRow = (Excel.XlSummaryRow)Excel.Constants.xlAbove; + Ws.Outline.SummaryColumn = (Excel.XlSummaryColumn)Excel.Constants.xlLeft; + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + PopulateExcelRow(Ws, ref row, ref lastDataSourceRow, ref lastTableRow, comparisonObject, progBar); + } + + // do we need to close the last groups? + if (lastTableRow < row && lastTableRow != -1) + { + Ws.Application.Rows[Convert.ToString(lastTableRow + 1) + ":" + Convert.ToString(row)].Select(); + Ws.Application.Selection.Rows.Group(); + } + if (lastDataSourceRow < row && lastDataSourceRow != -1) + { + Ws.Application.Rows[Convert.ToString(lastDataSourceRow + 1) + ":" + Convert.ToString(row)].Select(); + Ws.Application.Selection.Rows.Group(); + } + + Ws.Cells[1, 1].Select(); + App.Visible = true; + progBar.Value = 0; + } + catch (System.Runtime.InteropServices.COMException exc) + { + throw new System.Runtime.InteropServices.COMException("Unable to create Excel report. Please check Excel is installed.", exc); + } + } + + /// + /// Refresh SkipSelections property. + /// + public void RefreshSkipSelectionsFromComparisonObjects() + { + _comparisonInfo.SkipSelections.Clear(); + + foreach (ComparisonObject comparisonObject in this.ComparisonObjects) + { + RefreshSkipSelectionsFromChildComparisonObjects(comparisonObject); + } + } + + private void RefreshSkipSelectionsFromChildComparisonObjects(ComparisonObject comparisonObject) + { + if (comparisonObject.Status != ComparisonObjectStatus.SameDefinition && comparisonObject.MergeAction == MergeAction.Skip && !_comparisonInfo.SkipSelections.Contains(comparisonObject)) + { + _comparisonInfo.SkipSelections.Add(new SkipSelection(comparisonObject)); + } + + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + RefreshSkipSelectionsFromChildComparisonObjects(childComparisonObject); + } + } + + /// + /// Refresh ComparisonObjects property. + /// + public void RefreshComparisonObjectsFromSkipSelections() + { + foreach (ComparisonObject comparisonObject in this.ComparisonObjects) + { + RefreshChildComparisonObjectsFromSkipSelections(comparisonObject); + } + } + + private void RefreshChildComparisonObjectsFromSkipSelections(ComparisonObject comparisonObject) + { + if (comparisonObject.Status != ComparisonObjectStatus.SameDefinition) + { + foreach (SkipSelection skipSelection in _comparisonInfo.SkipSelections) + { + if (comparisonObject.Status == skipSelection.Status && comparisonObject.ComparisonObjectType == skipSelection.ComparisonObjectType && (skipSelection.Status == ComparisonObjectStatus.MissingInSource || comparisonObject.SourceObjectInternalName == skipSelection.SourceObjectInternalName) && (skipSelection.Status == ComparisonObjectStatus.MissingInTarget || comparisonObject.TargetObjectInternalName == skipSelection.TargetObjectInternalName)) + { + comparisonObject.MergeAction = MergeAction.Skip; + break; + } + } + } + + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + RefreshChildComparisonObjectsFromSkipSelections(childComparisonObject); + } + } + + private void PopulateExcelRow(Excel.Worksheet Ws, ref int row, ref int lastDataSourceRow, ref int lastTableRow, ComparisonObject comparisonObject, ProgressBar progBar) + { + progBar.PerformStep(); + row += 1; + + // Close out groups if necessary + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource || comparisonObject.ComparisonObjectType == ComparisonObjectType.Table || comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective || comparisonObject.ComparisonObjectType == ComparisonObjectType.Culture || comparisonObject.ComparisonObjectType == ComparisonObjectType.Role || comparisonObject.ComparisonObjectType == ComparisonObjectType.Expression || comparisonObject.ComparisonObjectType == ComparisonObjectType.Action) //treat perspectives/cultures/roles/expressions like datasources for purpose of grouping + { + // do we need to close a table group? + if (lastTableRow + 1 < row && lastTableRow != -1) + { + Ws.Application.Rows[Convert.ToString(lastTableRow + 1) + ":" + Convert.ToString(row - 1)].Select(); + Ws.Application.Selection.Rows.Group(); + } + lastTableRow = row; + + //Todo: delete (no longer group tables by data source) + //if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource || comparisonObject.ComparisonObjectType == ComparisonObjectType.Table || comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective || comparisonObject.ComparisonObjectType == ComparisonObjectType.Culture || comparisonObject.ComparisonObjectType == ComparisonObjectType.Role || comparisonObject.ComparisonObjectType == ComparisonObjectType.Expression || comparisonObject.ComparisonObjectType == ComparisonObjectType.Action) ///treat perspectives/cultures/roles/expressions like datasources for purpose of grouping + //{ + // // do we need to close a datasource group? + // if (lastDataSourceRow + 1 < row && lastDataSourceRow != -1) + // { + // Ws.Application.Rows[Convert.ToString(lastDataSourceRow + 1) + ":" + Convert.ToString(row - 1)].Select(); + // Ws.Application.Selection.Rows.Group(); + // } + // lastDataSourceRow = row; + //} + } + + //Type column + switch (comparisonObject.ComparisonObjectType) + { + case ComparisonObjectType.DataSource: + Ws.Cells[row, 1].Value = "Data Source"; + break; + case ComparisonObjectType.Table: + Ws.Cells[row, 1].Value = "Table"; + break; + case ComparisonObjectType.Relationship: + Ws.Cells[row, 1].Value = "Relationship"; + Ws.Cells[row, 1].InsertIndent(3); + Ws.Cells[row, 2].InsertIndent(3); + Ws.Cells[row, 4].InsertIndent(3); + break; + case ComparisonObjectType.Measure: + Ws.Cells[row, 1].Value = "Measure"; + Ws.Cells[row, 1].InsertIndent(3); + Ws.Cells[row, 2].InsertIndent(3); + Ws.Cells[row, 4].InsertIndent(3); + break; + case ComparisonObjectType.Kpi: + Ws.Cells[row, 1].Value = "KPI"; + Ws.Cells[row, 1].InsertIndent(3); + Ws.Cells[row, 2].InsertIndent(3); + Ws.Cells[row, 4].InsertIndent(3); + break; + case ComparisonObjectType.Perspective: + Ws.Cells[row, 1].Value = "Perspective"; + break; + case ComparisonObjectType.Culture: + Ws.Cells[row, 1].Value = "Culture"; + break; + case ComparisonObjectType.Role: + Ws.Cells[row, 1].Value = "Role"; + break; + case ComparisonObjectType.Expression: + Ws.Cells[row, 1].Value = "Expression"; + break; + case ComparisonObjectType.Action: + Ws.Cells[row, 1].Value = "Action"; + break; + default: + Ws.Cells[row, 1].Value = comparisonObject.ComparisonObjectType.ToString(); + break; + } + + //Source Obj Name column + if (comparisonObject.SourceObjectName != null && comparisonObject.SourceObjectName != "") + { + Ws.Cells[row, 2].Value = comparisonObject.SourceObjectName; + //source obj def + Ws.Cells[row, 2].AddComment(); + Ws.Cells[row, 2].Comment.Visible = false; + if (comparisonObject.SourceObjectDefinition != null && comparisonObject.SourceObjectDefinition != "") + { + Ws.Cells[row, 2].Comment.Text("Object Definition:\n\n" + comparisonObject.SourceObjectDefinition); + Ws.Cells[row, 2].Comment.Shape.TextFrame.Characters.Font.Bold = false; + Ws.Cells[row, 2].Comment.Shape.Width = 200; + Ws.Cells[row, 2].Comment.Shape.Height = 250; + } + } + else + { + Ws.Cells[row, 2].Interior.Pattern = Excel.Constants.xlSolid; + Ws.Cells[row, 2].Interior.PatternColorIndex = Excel.Constants.xlAutomatic; + Ws.Cells[row, 2].Interior.ThemeColor = Excel.XlThemeColor.xlThemeColorDark1; + Ws.Cells[row, 2].Interior.TintAndShade = -0.149998474074526; + Ws.Cells[row, 2].Interior.PatternTintAndShade = 0; + } + + //status + switch (comparisonObject.Status) + { + case ComparisonObjectStatus.SameDefinition: + Ws.Cells[row, 3].Value = "Same Definition"; + break; + case ComparisonObjectStatus.DifferentDefinitions: + Ws.Cells[row, 3].Value = "Different Definitions"; + break; + case ComparisonObjectStatus.MissingInTarget: + Ws.Cells[row, 3].Value = "Missing in Target"; + break; + case ComparisonObjectStatus.MissingInSource: + Ws.Cells[row, 3].Value = "Missing in Source"; + break; + default: + Ws.Cells[row, 3].Value = comparisonObject.Status.ToString(); + break; + } + + //Target Obj Name column + if (comparisonObject.TargetObjectName != null && comparisonObject.TargetObjectName != "") + { + Ws.Cells[row, 4].Value = comparisonObject.TargetObjectName; + //Target obj def + Ws.Cells[row, 4].AddComment(); + Ws.Cells[row, 4].Comment.Visible = false; + if (comparisonObject.TargetObjectDefinition != null && comparisonObject.TargetObjectDefinition != "") + { + Ws.Cells[row, 4].Comment.Text("Object Definition:\n\n" + comparisonObject.TargetObjectDefinition); + Ws.Cells[row, 4].Comment.Shape.TextFrame.Characters.Font.Bold = false; + Ws.Cells[row, 4].Comment.Shape.Width = 200; + Ws.Cells[row, 4].Comment.Shape.Height = 250; + } + } + else + { + Ws.Cells[row, 4].Interior.Pattern = Excel.Constants.xlSolid; + Ws.Cells[row, 4].Interior.PatternColorIndex = Excel.Constants.xlAutomatic; + Ws.Cells[row, 4].Interior.ThemeColor = Excel.XlThemeColor.xlThemeColorDark1; + Ws.Cells[row, 4].Interior.TintAndShade = -0.149998474074526; + Ws.Cells[row, 4].Interior.PatternTintAndShade = 0; + } + + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + PopulateExcelRow(Ws, ref row, ref lastDataSourceRow, ref lastTableRow, childComparisonObject, progBar); + } + } + + #region Helper functions to execute XMLA + + /// + /// Finds row count for a table to display after processing. + /// + /// + /// + /// + /// Row count. + public static int FindRowCount(Microsoft.AnalysisServices.Core.Server server, string tableName, string databaseName) + { + string dax = String.Format("EVALUATE ROW( \"RowCount\", COUNTROWS('{0}'))", tableName); + XmlNodeList rows = ExecuteXmlaCommand(server, databaseName, dax); + + foreach (XmlNode row in rows) + { + XmlNode rowCountNode = null; + + foreach (XmlNode childNode in row.ChildNodes) + { + if (childNode.Name.Contains("RowCount")) + { + rowCountNode = childNode; + } + } + + int result; + if (rowCountNode != null && int.TryParse(rowCountNode.InnerText, out result)) + { + return result; + } + } + + return 0; + } + + /// + /// Executes an XMLA command on the tabular model for the connection. + /// + /// + /// + /// XmlNodeList containing results of the command execution. + public static XmlNodeList ExecuteXmlaCommand(Microsoft.AnalysisServices.Core.Server server, string catalog, string commandStatement) + { + XmlWriter xmlWriter = server.StartXmlaRequest(XmlaRequestType.Undefined); + WriteSoapEnvelopeWithCommandStatement(xmlWriter, server.SessionID, catalog, commandStatement); + System.Xml.XmlReader xmlReader = server.EndXmlaRequest(); + xmlReader.MoveToContent(); + string fullEnvelopeResponseFromServer = xmlReader.ReadOuterXml(); + xmlReader.Close(); + + XmlDocument documentResponse = new XmlDocument(); + documentResponse.LoadXml(fullEnvelopeResponseFromServer); + XmlNamespaceManager nsmgr = new XmlNamespaceManager(documentResponse.NameTable); + nsmgr.AddNamespace("myns1", "urn:schemas-microsoft-com:xml-analysis"); + nsmgr.AddNamespace("myns2", "urn:schemas-microsoft-com:xml-analysis:rowset"); + XmlNodeList rows = documentResponse.SelectNodes("//myns1:ExecuteResponse/myns1:return/myns2:root/myns2:row", nsmgr); + return rows; + } + + private static void WriteSoapEnvelopeWithCommandStatement(XmlWriter xmlWriter, string sessionId, string catalog, string commandStatement) + { + #region Examples + + //EXAMPLE1 + // + //
+ // + //
+ // + // + // + // + // SystemGetSubdirs 'd:\Program Files\Microsoft SQL Server\MSAS11.MSSQLSERVER\OLAP\Data' + // + // + // + // + // + //
+ + //EXAMPLE2 + // + //
+ // + //
+ // + // + // + // + // EVALUATE ROW( "Result", COUNTROWS('FactInternetSales')) + // + // + // + // + // Tabular1200 V2 + // Tabular + // Data + // + // + // + // + //
+ + #endregion + + xmlWriter.WriteStartElement("Envelope", "http://schemas.xmlsoap.org/soap/envelope/"); + xmlWriter.WriteStartElement("Header"); + if (sessionId != null) + { + xmlWriter.WriteStartElement("Session", "urn:schemas-microsoft-com:xml-analysis"); + xmlWriter.WriteAttributeString("soap", "mustUnderstand", "http://schemas.xmlsoap.org/soap/envelope/", "1"); + xmlWriter.WriteAttributeString("SessionId", sessionId); + xmlWriter.WriteEndElement(); // + } + xmlWriter.WriteEndElement(); // + xmlWriter.WriteStartElement("Body"); + xmlWriter.WriteStartElement("Execute", "urn:schemas-microsoft-com:xml-analysis"); + + xmlWriter.WriteStartElement("Command"); + xmlWriter.WriteElementString("Statement", commandStatement); + xmlWriter.WriteEndElement(); // + + xmlWriter.WriteStartElement("Properties"); + if (!String.IsNullOrEmpty(catalog)) + { + xmlWriter.WriteStartElement("PropertyList"); + xmlWriter.WriteElementString("Catalog", catalog); + xmlWriter.WriteElementString("Format", "Tabular"); + xmlWriter.WriteElementString("Content", "Data"); + xmlWriter.WriteEndElement(); // + } + xmlWriter.WriteEndElement(); // + + xmlWriter.WriteEndElement(); // + xmlWriter.WriteEndElement(); // + xmlWriter.WriteEndElement(); // + } + + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/Core/ComparisonObject.cs b/BismNormalizer/BismNormalizer/TabularCompare/Core/ComparisonObject.cs new file mode 100644 index 0000000..059d2ee --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/Core/ComparisonObject.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare.Core +{ + /// + /// Represents source and target objects for comparison, their type and status. This class is extended by BismNormalizer.TabularCompare.MultidimensionalMetadata.ComparisonObject and BismNormalizer.TabularCompare.TabularMetadata.ComparisonObject depending on SSAS compatibility level of the comparison. + /// + public abstract class ComparisonObject : IComparable + { + #region Protetced/Private Members + + protected ComparisonObjectType _comparisonObjectType; + protected ComparisonObjectStatus _status; + protected MergeAction _mergeAction; + protected List _childComparisonObjects; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the ComparisonObject class using multiple parameters. + /// + /// Type of ComaprisonObject such as Table, Measure, Relationship, etc. + /// Status of ComaprisonObject such as Same Definition, Different Definitions and Missing In Target. + /// Action of ComaprisonObject such as Create, Update, Delete and Skip. + public ComparisonObject( + ComparisonObjectType comparisonObjectType, + ComparisonObjectStatus status, + MergeAction mergeAction) + { + _comparisonObjectType = (comparisonObjectType == ComparisonObjectType.Connection ? ComparisonObjectType.DataSource : comparisonObjectType); //Need to support connection for backwards compatibility when deserializing from xml + _status = status; + _mergeAction = mergeAction; + _childComparisonObjects = new List(); + } + + #endregion + + #region Public Properties + + /// + /// The comparison object type such as Table, Measure, Relationship, etc. + /// + public ComparisonObjectType ComparisonObjectType + { + get { return _comparisonObjectType; } + set + { + _comparisonObjectType = (value == ComparisonObjectType.Connection ? ComparisonObjectType.DataSource : value); //Need to support connection for backwards compatibility when deserializing from xml + } + } + + /// + /// The comparison object status such as Same Definition, Different Definitions, Missing in Target and Missing in Source. + /// + public ComparisonObjectStatus Status + { + get { return _status; } + set { _status = value; } + } + + /// + /// The comparison object merge action such as Create, Update, Delete and Skip. + /// + public MergeAction MergeAction + { + get { return _mergeAction; } + set { _mergeAction = value; } + } + + /// + /// Collection of ComparisonObject. Represents hierarchy shown on the differences grid. + /// + public List ChildComparisonObjects + { + get { return _childComparisonObjects; } + set { _childComparisonObjects = value; } + } + + #endregion + + #region Abstract Members + + /// + /// Gets the name of the source object. + /// + public abstract string SourceObjectName { get; } + + /// + /// Gets the internal name of the source object. For objects instantiated in the BismNormalizer.TabularCompare.TabularMetadata namespace, this is the same as the name of the object with the exception of relationships. Relationships will show the internal GUID created by TOM in this property. + /// + public abstract string SourceObjectInternalName { get; } + + /// + /// Gets the source object definition. For objects instantiated in the BismNormalizer.TabularCompare.TabularMetadata namespace, this is the JSON representation. + /// + public abstract string SourceObjectDefinition { get; } + + + /// + /// Gets the name of the target object. + /// + public abstract string TargetObjectName { get; } + + /// + /// Gets the internal name of the target object. For objects instantiated in the BismNormalizer.TabularCompare.TabularMetadata namespace, this is the same as the name of the object with the exception of relationships. Relationships will show the internal GUID created by TOM in this property. + /// + public abstract string TargetObjectInternalName { get; } + + /// + /// Gets the target object definition. For objects instantiated in the BismNormalizer.TabularCompare.TabularMetadata namespace, this is the JSON representation. + /// + public abstract string TargetObjectDefinition { get; } + + /// + /// Provides key for CompareTo method. + /// + public abstract string SortKey(); + + public abstract int CompareTo(ComparisonObject other); + + #endregion + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/DatabaseDeploymentEventArgs.cs b/BismNormalizer/BismNormalizer/TabularCompare/DatabaseDeploymentEventArgs.cs new file mode 100644 index 0000000..6af7ce0 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/DatabaseDeploymentEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Provides data for the Comparison.DatabaseDeployment event. + /// + public class DatabaseDeploymentEventArgs : EventArgs + { + /// + /// Gets or sets a flag indicating if the database deployment was successful. + /// + public bool DeploymentSuccessful { get; set; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/DeploymentCompleteEventArgs.cs b/BismNormalizer/BismNormalizer/TabularCompare/DeploymentCompleteEventArgs.cs new file mode 100644 index 0000000..9866eb8 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/DeploymentCompleteEventArgs.cs @@ -0,0 +1,31 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Provides data for the Comparison.DeploymentComplete event. + /// + public class DeploymentCompleteEventArgs : EventArgs + { + /// + /// Initializes DeploymentCompleteEventArgs with necessary data for Comparison.DeploymentComplete event. + /// + /// The deployment status. + /// The error message. + public DeploymentCompleteEventArgs(DeploymentStatus deploymentStatus, string errorMessage) + { + DeploymentStatus = deploymentStatus; + ErrorMessage = errorMessage; + } + + /// + /// Gets or sets the deployment status. + /// + public DeploymentStatus DeploymentStatus { get; set; } + + /// + /// Gets or sets the error message. + /// + public string ErrorMessage { get; set; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/DeploymentMessageEventArgs.cs b/BismNormalizer/BismNormalizer/TabularCompare/DeploymentMessageEventArgs.cs new file mode 100644 index 0000000..6765d66 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/DeploymentMessageEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Provides data for the IComparison.DeploymentMessage event. + /// + public class DeploymentMessageEventArgs : EventArgs + { + /// + /// Initializes DeploymentMessageEventArgs with necessary data for IComparison.DeploymentMessage event. + /// + /// The work item that the message relates to. + /// The deployment message. + /// The DeploymentStatus. + public DeploymentMessageEventArgs(string workItem, string message, DeploymentStatus deploymentStatus) + { + WorkItem = workItem; + Message = message; + DeploymentStatus = deploymentStatus; + } + + /// + /// Gets or sets the work item that the message relates to. + /// + public string WorkItem { get; set; } + + /// + /// Gets or set the deployment message. + /// + public string Message { get; set; } + + /// + /// Gets or sets the deployment status. + /// + public DeploymentStatus DeploymentStatus { get; set; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/Enums.cs b/BismNormalizer/BismNormalizer/TabularCompare/Enums.cs new file mode 100644 index 0000000..2fbf385 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/Enums.cs @@ -0,0 +1,43 @@ + +namespace BismNormalizer.TabularCompare +{ + /// + /// Type of object that a validation message relates to. For example, Table, Measure, MeasureCalculationDependency, etc. + /// + public enum ValidationMessageType { DataSource, Table, Relationship, Measure, Kpi, Perspective, Culture, Role, Expression, Action, MeasureCalculationDependency, General }; //General used for command line only + + /// + /// Status for a validation message, such as Informational and Warning. + /// + public enum ValidationMessageStatus { Informational, Warning }; + + /// + /// Type of comparison object. For example, Table, Measure, Relationship, etc. + /// + public enum ComparisonObjectType { DataSource, Table, Relationship, Measure, Kpi, Perspective, Culture, Role, Expression, Action, Connection }; //Need connection for backwards compatibility when deserializing from xml. Set to data source. + + /// + /// Status of comparison object, such as Same Definition, Different Definitions and Missing In Target. + /// + public enum ComparisonObjectStatus { SameDefinition, DifferentDefinitions, MissingInTarget, MissingInSource, Na }; + + /// + /// Action to be taken for a comparison object, such as Create, Update, Delete and Skip. + /// + public enum MergeAction { Create, Update, Delete, Skip }; + + /// + /// Processing option for database deployment. + /// + public enum ProcessingOption { Default, DoNotProcess, Full }; + + /// + /// Status of database deployment. + /// + public enum DeploymentStatus { Deploying = 0, Success = 1, Error = 2, Cancel = 3 }; + + /// + /// Type of dependency. For example, DataSource, Partition, Expression. + /// + public enum CalcDependencyObjectType { DataSource, Partition, Expression }; +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Action.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Action.cs new file mode 100644 index 0000000..31f982e --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Action.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using System.Globalization; +using Microsoft.AnalysisServices; +using Amo=Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + //[Obsolete("This class is obsolete. Left over from BISM Normalizer 2, which supported BIDS Helper actions.")] + + /// + /// Abstraction of a tabular model action with properties and methods for comparison purposes. + /// + public class Action : ITabularObject + { + private TabularModel _parentTabularModel; + private Amo.Action _amoAction; + private string _objectDefinition; + private string _substituteId; + + public Action(TabularModel parentTabularModel, Amo.Action Action) + { + _parentTabularModel = parentTabularModel; + _amoAction = Action; + _objectDefinition = ""; + + if (_amoAction.Caption != null) _objectDefinition += "Caption: " + _amoAction.Caption + "\n"; + _objectDefinition += "Caption is MDX: " + _amoAction.CaptionIsMdx.ToString() + "\n"; + if (_amoAction.Description != null) _objectDefinition += "Description: " + _amoAction.Description + "\n"; + _objectDefinition += "Action Type: " + _amoAction.Type.ToString() + "\n"; + if (_amoAction.Target != null) _objectDefinition += "Target: " + _amoAction.Target + "\n"; + if (_amoAction.Condition != null) _objectDefinition += "Condition: " + _amoAction.Condition + "\n"; + _objectDefinition += "Invocation: " + _amoAction.Invocation.ToString() + "\n\n"; + + switch (_amoAction.Type) + { + case ActionType.DrillThrough: + if (_amoAction is DrillThroughAction) + { + DrillThroughAction drillThroughAction = (DrillThroughAction)_amoAction; + _objectDefinition += "Drillthrough Columns:\n"; + foreach (CubeAttributeBinding column in drillThroughAction.Columns) + { + if (drillThroughAction.Parent.Dimensions.Contains(column.CubeDimensionID) && drillThroughAction.Parent.Dimensions[column.CubeDimensionID].Attributes.Contains(column.AttributeID)) + { + _objectDefinition += "Table: " + drillThroughAction.Parent.Dimensions[column.CubeDimensionID].Name + ", Column: " + drillThroughAction.Parent.Dimensions[column.CubeDimensionID].Attributes[column.AttributeID].Attribute.Name + "\n"; + } + } + if (drillThroughAction.Columns.Count > 0) _objectDefinition += "\n"; + _objectDefinition += "Default: " + drillThroughAction.Default.ToString() + "\n"; + _objectDefinition += "Maximum Rows: " + drillThroughAction.MaximumRows.ToString() + "\n"; + } + break; + case ActionType.Report: + if (_amoAction is ReportAction) + { + ReportAction reportAction = (ReportAction)_amoAction; + _objectDefinition += "Report Parameters:\n"; + foreach (ReportParameter reportParameter in reportAction.ReportParameters) + { + if (reportParameter.Name != null && reportParameter.Value != null) + { + _objectDefinition += "Name: " + reportParameter.Name + ", Value: " + reportParameter.Value + "\n"; + } + } + if (reportAction.ReportParameters.Count > 0 || reportAction.ReportFormatParameters.Count > 0) _objectDefinition += "\n"; + if (reportAction.ReportServer != null) _objectDefinition += "Report Server: " + reportAction.ReportServer + "\n"; + if (reportAction.Path != null) _objectDefinition += "Maximum Path: " + reportAction.Path + "\n"; + } + break; + default: + if (_amoAction is StandardAction) + { + StandardAction standardAction = (StandardAction)_amoAction; + if (standardAction.Expression != null) _objectDefinition += "Expression:\n" + standardAction.Expression + "\n"; + } + break; + } + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += "\nFormat & Visibility:\n"; + + // _objectDefinition += "Action Translations: "; + // if (_amoAction.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation actionTranslation in _amoAction.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(actionTranslation.Language).DisplayName + ": " + actionTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (_amoAction.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation actionDisplayFolderTranslation in _amoAction.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(actionDisplayFolderTranslation.Language).DisplayName + ": " + actionDisplayFolderTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + // _objectDefinition += "\n"; + //} + } + + public TabularModel ParentTabularModel => _parentTabularModel; + + public Amo.Action AmoAction => _amoAction; + + public string Name => _amoAction.Name; + + public string LongName => _amoAction.Name; + + public string Id => _amoAction.ID; + + public string ObjectDefinition => _objectDefinition; + + public string SubstituteId + { + get + { + if (string.IsNullOrEmpty(_substituteId)) + { + return _amoAction.ID; + } + else + { + return _substituteId; + } + } + set + { + _substituteId = value; + } + } + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ActionCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ActionCollection.cs new file mode 100644 index 0000000..3ca1e49 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ActionCollection.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + //[Obsolete("This class is obsolete. Left over from BISM Normalizer 2, which supported BIDS Helper actions.")] + + /// + /// Represents a collection of Action objects. + /// + public class ActionCollection : List + { + public Action FindByName(string name) + { + foreach (Action action in this) + { + if (action.Name == name) + { + return action; + } + } + return null; + } + public bool ContainsName(string name) + { + foreach (Action action in this) + { + if (action.Name == name) + { + return true; + } + } + return false; + } + + public Action FindById(string id) + { + foreach (Action action in this) + { + if (action.Id == id) + { + return action; + } + } + return null; + } + public bool ContainsId(string id) + { + foreach (Action action in this) + { + if (action.Id == id) + { + return true; + } + } + return false; + } + + public bool RemoveById(string id) + { + foreach (Action action in this) + { + if (action.Id == id) + { + this.Remove(action); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Comparison.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Comparison.cs new file mode 100644 index 0000000..4c69f6c --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Comparison.cs @@ -0,0 +1,1295 @@ +using System; +using System.Collections.Generic; +using Microsoft.AnalysisServices; +using Excel = Microsoft.Office.Interop.Excel; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a source vs. target comparison of an SSAS tabular model. This class is for tabular models that use multidimensional metadata with SSAS compatibility level 1100 or 1103. + /// + public class Comparison : Core.Comparison + { + #region Private Variables + + private TabularModel _sourceTabularModel; + private TabularModel _targetTabularModel; + private bool _uncommitedChanges = false; + private DateTime _lastSourceSchemaUpdate = DateTime.MinValue; + private DateTime _lastTargetSchemaUpdate = DateTime.MinValue; + private bool _disposed = false; + + #endregion + + #region Properties + + /// + /// TabularModel object being used as the source for comparison. + /// + public TabularModel SourceTabularModel + { + get { return _sourceTabularModel; } + set { _sourceTabularModel = value; } + } + + /// + /// TabularModel object being used as the target for comparison. + /// + public TabularModel TargetTabularModel + { + get { return _targetTabularModel; } + set { _targetTabularModel = value; } + } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the Comparison class using a ComparisonInfo object. + /// + /// ComparisonInfo object typically deserialized from a BSMN file. + public Comparison(ComparisonInfo comparisonInfo) + : base(comparisonInfo) + { + _sourceTabularModel = new TabularModel(this, comparisonInfo.ConnectionInfoSource, comparisonInfo); + _targetTabularModel = new TabularModel(this, comparisonInfo.ConnectionInfoTarget, comparisonInfo); + } + + #endregion + + /// + /// Connect to source and target tabular models, and instantiate their properties. + /// + public override void Connect() + { + _sourceTabularModel.Connect(); + _targetTabularModel.Connect(); + } + + /// + /// Disconnect from source and target tabular models. + /// + public override void Disconnect() + { + _sourceTabularModel.Disconnect(); + _targetTabularModel.Disconnect(); + } + + public override void CompareTabularModels() + { + _comparisonObjectCount = 0; + + #region Data Sources + + foreach (DataSource dataSourceSource in _sourceTabularModel.DataSources) + { + // check if source is not in target + if (!_targetTabularModel.DataSources.ContainsName(dataSourceSource.Name)) + { + ComparisonObject comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.MissingInTarget, dataSourceSource, dataSourceSource.Name, dataSourceSource.Id, dataSourceSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + + #region Tables for DataSource that is Missing in Target + + foreach (Table tblSource in _sourceTabularModel.Tables.FilterByDataSourceId(dataSourceSource.Id)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInTarget, tblSource, tblSource.Name, tblSource.Id, tblSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for Table that is Missing in Target + + foreach (Relationship relSource in tblSource.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInTarget, relSource, " " + relSource.Name, relSource.Id, relSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures for Table that is Missing in Target + + foreach (Measure measureSource in _sourceTabularModel.Measures.FilterByTableName(tblSource.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInTarget, measureSource, " " + measureSource.Name, measureSource.Id, measureSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + + #region KPIs for Table that is Missing in Target + + foreach (Kpi kpiSource in _sourceTabularModel.Kpis.FilterByTableName(tblSource.Name)) + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInTarget, kpiSource, " " + kpiSource.Name, kpiSource.Id, kpiSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + + #endregion + } + + #endregion + } + else + { + // there is a datasource in the target with the same name at least + DataSource dataSourceTarget = _targetTabularModel.DataSources.FindByName(dataSourceSource.Name); + if (dataSourceSource.Id != dataSourceTarget.Id) + { + dataSourceSource.SubstituteId = dataSourceTarget.Id; + } + ComparisonObject comparisonObjectDataSource; + + // check if datasource object definition is different + if (dataSourceSource.ObjectDefinition != dataSourceTarget.ObjectDefinition) + { + comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.DifferentDefinitions, dataSourceSource, dataSourceSource.Name, dataSourceSource.Id, dataSourceSource.ObjectDefinition, MergeAction.Update, dataSourceTarget, dataSourceTarget.Name, dataSourceTarget.Id, dataSourceTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.SameDefinition, dataSourceSource, dataSourceSource.Name, dataSourceSource.Id, dataSourceSource.ObjectDefinition, MergeAction.Skip, dataSourceTarget, dataSourceTarget.Name, dataSourceTarget.Id, dataSourceTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + + #region Tables where source/target datasources exist + + foreach (Table tblSource in _sourceTabularModel.Tables.FilterByDataSourceId(dataSourceSource.Id)) + { + // check if source is not in target + if (!_targetTabularModel.Tables.FilterByDataSourceId(dataSourceTarget.Id).ContainsName(tblSource.Name)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInTarget, tblSource, tblSource.Name, tblSource.Id, tblSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for table Missing in Target + + // all relationships in source are not in target (the target table doesn't even exist) + foreach (Relationship relSource in tblSource.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInTarget, relSource, " " + relSource.Name, relSource.Id, relSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures for Table that is Missing in Target + + foreach (Measure measureSource in _sourceTabularModel.Measures.FilterByTableName(tblSource.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInTarget, measureSource, " " + measureSource.Name, measureSource.Id, measureSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + + #region Kpis for Table that is Missing in Target + + foreach (Kpi kpiSource in _sourceTabularModel.Kpis.FilterByTableName(tblSource.Name)) + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInTarget, kpiSource, " " + kpiSource.Name, kpiSource.Id, kpiSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + + #endregion + } + else + { + //table name is in source and target + + Table tblTarget = _targetTabularModel.Tables.FindByName(tblSource.Name); + if (tblSource.Id != tblTarget.Id) + { + tblSource.SubstituteId = tblTarget.Id; + } + ComparisonObject comparisonObjectTable; + + if (tblSource.ObjectDefinition == tblTarget.ObjectDefinition) + { + comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.SameDefinition, tblSource, tblSource.Name, tblSource.Id, tblSource.ObjectDefinition, MergeAction.Skip, tblTarget, tblTarget.Name, tblTarget.Id, tblTarget.ObjectDefinition); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.DifferentDefinitions, tblSource, tblSource.Name, tblSource.Id, tblSource.ObjectDefinition, MergeAction.Update, tblTarget, tblTarget.Name, tblTarget.Id, tblTarget.ObjectDefinition); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + } + + #region Relationships (table in source and target) + + // see if matching relationhip in source and target + foreach (Relationship relSource in tblSource.Relationships) + { + bool foundMatch = false; + foreach (Relationship relTarget in tblTarget.Relationships) + { + if (relSource.ObjectDefinition == relTarget.ObjectDefinition) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.SameDefinition, relSource, " " + relSource.Name, relSource.Id, relSource.ObjectDefinition, MergeAction.Skip, relTarget, " " + relTarget.Name, relTarget.Id, relTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + foundMatch = true; + break; + } + } + //the relationship in the source table doesnt' exist in the target table + if (!foundMatch) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInTarget, relSource, " " + relSource.Name, relSource.Id, relSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + } + + // see if relationships in target table that don't exist in source table + foreach (Relationship relTarget in tblTarget.Relationships) + { + bool foundMatch = false; + foreach (Relationship relSource in tblSource.Relationships) + { + if (relSource.ObjectDefinition == relTarget.ObjectDefinition) + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, relTarget, " " + relTarget.Name, relTarget.Id, relTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + } + + #endregion + + #region Measures (table in source and target) + + // see if matching measure in source and target + foreach (Measure measureSource in _sourceTabularModel.Measures.FilterByTableName(tblSource.Name)) + { + if (_targetTabularModel.Measures.FilterByTableName(tblTarget.Name).ContainsName(measureSource.Name)) + { + //Measure in source and target, so check definition + Measure measureTarget = _targetTabularModel.Measures.FilterByTableName(tblTarget.Name).FindByName(measureSource.Name); + if (measureSource.ObjectDefinition == measureTarget.ObjectDefinition) + { + //Measure has same definition + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.SameDefinition, measureSource, " " + measureSource.Name, measureSource.Id, measureSource.ObjectDefinition, MergeAction.Skip, measureTarget, " " + measureTarget.Name, measureTarget.Id, measureTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + else + { + //Measure has different definition + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.DifferentDefinitions, measureSource, " " + measureSource.Name, measureSource.Id, measureSource.ObjectDefinition, MergeAction.Update, measureTarget, " " + measureTarget.Name, measureTarget.Id, measureTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + else + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInTarget, measureSource, " " + measureSource.Name, measureSource.Id, measureSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + //now check if target contains measures Missing in Source + foreach (Measure measureTarget in _targetTabularModel.Measures.FilterByTableName(tblTarget.Name)) + { + if (!_sourceTabularModel.Measures.FilterByTableName(tblSource.Name).ContainsName(measureTarget.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, measureTarget, " " + measureTarget.Name, measureTarget.Id, measureTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + + #endregion + + #region Kpis (table in source and target) + + // see if matching kpi in source and target + foreach (Kpi kpiSource in _sourceTabularModel.Kpis.FilterByTableName(tblSource.Name)) + { + if (_targetTabularModel.Kpis.FilterByTableName(tblTarget.Name).ContainsName(kpiSource.Name)) + { + //Kpi in source and target, so check definition + Kpi kpiTarget = _targetTabularModel.Kpis.FilterByTableName(tblTarget.Name).FindByName(kpiSource.Name); + if (kpiSource.ObjectDefinition == kpiTarget.ObjectDefinition) + { + //Kpi has same definition + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.SameDefinition, kpiSource, " " + kpiSource.Name, kpiSource.Id, kpiSource.ObjectDefinition, MergeAction.Skip, kpiTarget, " " + kpiTarget.Name, kpiTarget.Id, kpiTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + else + { + //Kpi has different definition + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.DifferentDefinitions, kpiSource, " " + kpiSource.Name, kpiSource.Id, kpiSource.ObjectDefinition, MergeAction.Update, kpiTarget, " " + kpiTarget.Name, kpiTarget.Id, kpiTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + } + else + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInTarget, kpiSource, " " + kpiSource.Name, kpiSource.Id, kpiSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + } + //now check if target contains kpis Missing in Source + foreach (Kpi kpiTarget in _targetTabularModel.Kpis.FilterByTableName(tblTarget.Name)) + { + if (!_sourceTabularModel.Kpis.FilterByTableName(tblSource.Name).ContainsName(kpiTarget.Name)) + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, kpiTarget, " " + kpiTarget.Name, kpiTarget.Id, kpiTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + } + + #endregion + } + } + + foreach (Table tblTarget in _targetTabularModel.Tables.FilterByDataSourceId(dataSourceTarget.Id)) + { + // check if target is not in source + if (!_sourceTabularModel.Tables.FilterByDataSourceId(dataSourceSource.Id).ContainsName(tblTarget.Name)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, tblTarget, tblTarget.Name, tblTarget.Id, tblTarget.ObjectDefinition); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for table Missing in Source + + // all relationships in target are not in source (the source table doesn't even exist) + foreach (Relationship relTarget in tblTarget.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, relTarget, " " + relTarget.Name, relTarget.Id, relTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures for Table that is Missing in Source + + foreach (Measure measureTarget in _targetTabularModel.Measures.FilterByTableName(tblTarget.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, measureTarget, " " + measureTarget.Name, measureTarget.Id, measureTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + + #region Kpis for Table that is Missing in Source + + foreach (Kpi kpiTarget in _targetTabularModel.Kpis.FilterByTableName(tblTarget.Name)) + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, kpiTarget, " " + kpiTarget.Name, kpiTarget.Id, kpiTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + + #endregion + } + } + #endregion + } + } + + foreach (DataSource dataSourceTarget in _targetTabularModel.DataSources) + { + // if target datasource is Missing in Source, offer deletion + if (!_sourceTabularModel.DataSources.ContainsName(dataSourceTarget.Name)) + { + ComparisonObject comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, dataSourceTarget, dataSourceTarget.Name, dataSourceTarget.Id, dataSourceTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + + #region Tables for DataSource that is Missing in Source + + foreach (Table tblTarget in _targetTabularModel.Tables.FilterByDataSourceId(dataSourceTarget.Id)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, tblTarget, tblTarget.Name, tblTarget.Id, tblTarget.ObjectDefinition); + comparisonObjectDataSource.ChildComparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for Table that is Missing in Source + + foreach (Relationship relTarget in tblTarget.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, relTarget, " " + relTarget.Name, relTarget.Id, relTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures for Table that is Missing in Source + + foreach (Measure measureTarget in _targetTabularModel.Measures.FilterByTableName(tblTarget.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, measureTarget, " " + measureTarget.Name, measureTarget.Id, measureTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + + #region Kpis for Table that is Missing in Source + + foreach (Kpi kpiTarget in _targetTabularModel.Kpis.FilterByTableName(tblTarget.Name)) + { + ComparisonObject comparisonObjectKpi = new ComparisonObject(ComparisonObjectType.Kpi, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, kpiTarget, " " + kpiTarget.Name, kpiTarget.Id, kpiTarget.ObjectDefinition); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectKpi); + _comparisonObjectCount += 1; + } + + #endregion + } + + #endregion + } + } + + #endregion + + #region Actions + + if (_comparisonInfo.OptionsInfo.OptionActions) + { + foreach (Action actionSource in _sourceTabularModel.Actions) + { + // check if source is not in target + if (!_targetTabularModel.Actions.ContainsName(actionSource.Name)) + { + ComparisonObject comparisonObjectAction = new ComparisonObject(ComparisonObjectType.Action, ComparisonObjectStatus.MissingInTarget, actionSource, actionSource.Name, actionSource.Id, actionSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + _comparisonObjects.Add(comparisonObjectAction); + _comparisonObjectCount += 1; + } + else + { + // there is a Action in the target with the same name at least + Action actionTarget = _targetTabularModel.Actions.FindByName(actionSource.Name); + if (actionSource.Id != actionTarget.Id) + { + actionSource.SubstituteId = actionTarget.Id; + } + ComparisonObject comparisonObjectAction; + + // check if Action object definition is different + if (actionSource.ObjectDefinition != actionTarget.ObjectDefinition) + { + comparisonObjectAction = new ComparisonObject(ComparisonObjectType.Action, ComparisonObjectStatus.DifferentDefinitions, actionSource, actionSource.Name, actionSource.Id, actionSource.ObjectDefinition, MergeAction.Update, actionTarget, actionTarget.Name, actionTarget.Id, actionTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectAction); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectAction = new ComparisonObject(ComparisonObjectType.Action, ComparisonObjectStatus.SameDefinition, actionSource, actionSource.Name, actionSource.Id, actionSource.ObjectDefinition, MergeAction.Skip, actionTarget, actionTarget.Name, actionTarget.Id, actionTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectAction); + _comparisonObjectCount += 1; + } + } + } + + foreach (Action actionTarget in _targetTabularModel.Actions) + { + // if target Action is Missing in Source, offer deletion + if (!_sourceTabularModel.Actions.ContainsName(actionTarget.Name)) + { + ComparisonObject comparisonObjectAction = new ComparisonObject(ComparisonObjectType.Action, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, actionTarget, actionTarget.Name, actionTarget.Id, actionTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectAction); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Perspectives + + if (_comparisonInfo.OptionsInfo.OptionPerspectives) + { + foreach (Perspective perspectiveSource in _sourceTabularModel.Perspectives) + { + // check if source is not in target + if (!_targetTabularModel.Perspectives.ContainsName(perspectiveSource.Name)) + { + ComparisonObject comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.MissingInTarget, perspectiveSource, perspectiveSource.Name, perspectiveSource.Id, perspectiveSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + else + { + // there is a perspective in the target with the same name at least + Perspective perspectiveTarget = _targetTabularModel.Perspectives.FindByName(perspectiveSource.Name); + if (perspectiveSource.Id != perspectiveTarget.Id) + { + perspectiveSource.SubstituteId = perspectiveTarget.Id; + } + ComparisonObject comparisonObjectPerspective; + + // check if perspective object definition is different + //if (perspectiveSource.ObjectDefinition != perspectiveTarget.ObjectDefinition) + if ( (_comparisonInfo.OptionsInfo.OptionMergePerspectives && perspectiveTarget.ContainsOtherPerspectiveSelections(perspectiveSource)) || + (!_comparisonInfo.OptionsInfo.OptionMergePerspectives && perspectiveTarget.ContainsOtherPerspectiveSelections(perspectiveSource) && perspectiveSource.ContainsOtherPerspectiveSelections(perspectiveTarget)) ) + { + // they are equal, ... + comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.SameDefinition, perspectiveSource, perspectiveSource.Name, perspectiveSource.Id, perspectiveSource.ObjectDefinition, MergeAction.Skip, perspectiveTarget, perspectiveTarget.Name, perspectiveTarget.Id, perspectiveTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.DifferentDefinitions, perspectiveSource, perspectiveSource.Name, perspectiveSource.Id, perspectiveSource.ObjectDefinition, MergeAction.Update, perspectiveTarget, perspectiveTarget.Name, perspectiveTarget.Id, perspectiveTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + } + } + + foreach (Perspective perspectiveTarget in _targetTabularModel.Perspectives) + { + // if target perspective is Missing in Source, offer deletion + if (!_sourceTabularModel.Perspectives.ContainsName(perspectiveTarget.Name)) + { + ComparisonObject comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, perspectiveTarget, perspectiveTarget.Name, perspectiveTarget.Id, perspectiveTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Roles + + if (_comparisonInfo.OptionsInfo.OptionRoles) + { + foreach (Role roleSource in _sourceTabularModel.Roles) + { + // check if source is not in target + if (!_targetTabularModel.Roles.ContainsName(roleSource.Name)) + { + ComparisonObject comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.MissingInTarget, roleSource, roleSource.Name, roleSource.Id, roleSource.ObjectDefinition, MergeAction.Create, null, "", "", ""); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + else + { + // there is a role in the target with the same name at least + Role roleTarget = _targetTabularModel.Roles.FindByName(roleSource.Name); + if (roleSource.Id != roleTarget.Id) + { + roleSource.SubstituteId = roleTarget.Id; + } + ComparisonObject comparisonObjectRole; + + // check if role object definition is different + if (roleSource.ObjectDefinition != roleTarget.ObjectDefinition) + { + comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.DifferentDefinitions, roleSource, roleSource.Name, roleSource.Id, roleSource.ObjectDefinition, MergeAction.Update, roleTarget, roleTarget.Name, roleTarget.Id, roleTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.SameDefinition, roleSource, roleSource.Name, roleSource.Id, roleSource.ObjectDefinition, MergeAction.Skip, roleTarget, roleTarget.Name, roleTarget.Id, roleTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + } + } + + foreach (Role roleTarget in _targetTabularModel.Roles) + { + // if target role is Missing in Source, offer deletion + if (!_sourceTabularModel.Roles.ContainsName(roleTarget.Name)) + { + ComparisonObject comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.MissingInSource, null, "", "", "", MergeAction.Delete, roleTarget, roleTarget.Name, roleTarget.Id, roleTarget.ObjectDefinition); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Sorting + + _comparisonObjects.Sort(); + foreach (ComparisonObject childComparisonObject in _comparisonObjects) + { + childComparisonObject.ChildComparisonObjects.Sort(); + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + grandChildComparisonObject.ChildComparisonObjects.Sort(); + } + } + + #endregion + + this.RefreshComparisonObjectsFromSkipSelections(); + + _uncommitedChanges = false; + _lastSourceSchemaUpdate = _sourceTabularModel.AmoDatabase.LastSchemaUpdate; + _lastTargetSchemaUpdate = _targetTabularModel.AmoDatabase.LastSchemaUpdate; + } + + /// + /// Validate selection of actions to perform on target tabular model. Warnings and informational messages are provided by invoking ShowStatusMessageCallBack. + /// + public override void ValidateSelection() + { + #region Refresh/reconnect source and target dbs to check if server definition has changed + + if (_uncommitedChanges) + { + // Reconnect to re-initialize + _sourceTabularModel = new TabularModel(this, _comparisonInfo.ConnectionInfoSource, _comparisonInfo); + _sourceTabularModel.Connect(); + _targetTabularModel = new TabularModel(this, _comparisonInfo.ConnectionInfoTarget, _comparisonInfo); + _targetTabularModel.Connect(); + InitializeSubstituteIds(); + } + else + { + _sourceTabularModel.AmoDatabase.Refresh(); + _targetTabularModel.AmoDatabase.Refresh(); + } + + if (!_sourceTabularModel.ConnectionInfo.UseProject && _sourceTabularModel.AmoDatabase.LastSchemaUpdate > _lastSourceSchemaUpdate) + { + throw new Exception("The definition of the source database has changed since the comparison was run. Please re-run the comparison."); + } + if (!_targetTabularModel.ConnectionInfo.UseProject && _targetTabularModel.AmoDatabase.LastSchemaUpdate > _lastTargetSchemaUpdate) + { + throw new Exception("The definition of the target database has changed since the comparison was run. Please re-run the comparison."); + } + + _uncommitedChanges = true; + + #endregion + + #region DataSources + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteDataSource(comparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Data Source [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Create) + { + _targetTabularModel.CreateDataSource(_sourceTabularModel.DataSources.FindById(comparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create Data Source [" + comparisonObject.SourceObjectName + "].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateDataSource(_sourceTabularModel.DataSources.FindById(comparisonObject.SourceObjectId), _targetTabularModel.DataSources.FindById(comparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update Data Source [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && + (comparisonObject.MergeAction == MergeAction.Skip || comparisonObject.MergeAction == MergeAction.Update) && + (comparisonObject.Status == ComparisonObjectStatus.DifferentDefinitions || comparisonObject.Status == ComparisonObjectStatus.SameDefinition) && + comparisonObject.SourceObjectId != comparisonObject.TargetObjectId) + { + comparisonObject.SourceObjectSubstituteId = comparisonObject.TargetObjectId; + _sourceTabularModel.DataSources.FindById(comparisonObject.SourceObjectId).SubstituteId = comparisonObject.TargetObjectId; + } + } + + #endregion + + #region Flush reference dims + + _sourceTabularModel.FlushReferenceDimensions(); + _targetTabularModel.FlushReferenceDimensions(); + + #endregion + + #region Tables + + //retain partitions depending on option + if (!_comparisonInfo.OptionsInfo.OptionRetainPartitions) + { + OnValidationMessage(new ValidationMessageEventArgs("Option to retain partitions is set, but it is not supported for models with compatibility level 1100 or 1103. It will be ignored.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (childComparisonObject.ComparisonObjectType == ComparisonObjectType.Table && childComparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteTable(childComparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Table '" + childComparisonObject.TargetObjectName + "'.", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (childComparisonObject.ComparisonObjectType == ComparisonObjectType.Table && childComparisonObject.MergeAction == MergeAction.Create) + { + Table tableTarget = _targetTabularModel.Tables.FindByName(childComparisonObject.SourceObjectName); + if (tableTarget == null) + { + string sourceObjectSubstituteId = childComparisonObject.SourceObjectSubstituteId; + bool useSubstituteId = false; + _targetTabularModel.CreateTable(_sourceTabularModel.Tables.FindById(childComparisonObject.SourceObjectId), ref sourceObjectSubstituteId, ref useSubstituteId); + if (useSubstituteId) + { + _sourceTabularModel.UpdateRelationshipsWithSubstituteTableIds(childComparisonObject.SourceObjectSubstituteId, sourceObjectSubstituteId); + childComparisonObject.SourceObjectSubstituteId = sourceObjectSubstituteId; + } + OnValidationMessage(new ValidationMessageEventArgs("Create Table '" + childComparisonObject.SourceObjectName + "'.", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create Table " + childComparisonObject.SourceObjectName + " because another table with the same name (under a different data source) already exists in target model.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + } + } + } + + // before update tables, we need to check tables with Skip action that exist in source and target, to set substitute ids equal to ids from the target table + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (childComparisonObject.ComparisonObjectType == ComparisonObjectType.Table && childComparisonObject.MergeAction == MergeAction.Skip && + (childComparisonObject.Status == ComparisonObjectStatus.DifferentDefinitions || childComparisonObject.Status == ComparisonObjectStatus.SameDefinition) && + childComparisonObject.SourceObjectId != childComparisonObject.TargetObjectId) + { + childComparisonObject.SourceObjectSubstituteId = childComparisonObject.TargetObjectId; + _sourceTabularModel.Tables.FindById(childComparisonObject.SourceObjectId).SubstituteId = childComparisonObject.TargetObjectId; + _sourceTabularModel.UpdateRelationshipsWithSubstituteTableIds(childComparisonObject.SourceObjectId, childComparisonObject.TargetObjectId); + } + } + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (childComparisonObject.ComparisonObjectType == ComparisonObjectType.Table && childComparisonObject.MergeAction == MergeAction.Update) + { + string sourceObjectSubstituteId = childComparisonObject.SourceObjectSubstituteId; + bool useSubstituteId = false; + _targetTabularModel.UpdateTable(_sourceTabularModel.Tables.FindById(childComparisonObject.SourceObjectId), _targetTabularModel.Tables.FindById(childComparisonObject.TargetObjectId), ref sourceObjectSubstituteId, ref useSubstituteId); + if (useSubstituteId) + { + _sourceTabularModel.UpdateRelationshipsWithSubstituteTableIds(childComparisonObject.SourceObjectSubstituteId, sourceObjectSubstituteId); + childComparisonObject.SourceObjectSubstituteId = sourceObjectSubstituteId; + } + OnValidationMessage(new ValidationMessageEventArgs("Update Table '" + childComparisonObject.TargetObjectName + "'.", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + } + } + + // now that we've done table updates, we need to check the child relationships that referred to the updated tables. We have to do this here + // rather than in UpdateTable because need to ensure all the child tables have been updated too (if they happened to have an update action too) + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + if (childComparisonObject.ComparisonObjectType == ComparisonObjectType.Table && childComparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateRelationshipsForChildrenOfUpdatedTables(_targetTabularModel.Tables.FindById(childComparisonObject.SourceObjectSubstituteId)); + } + } + } + + #endregion + + #region Relationships + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Relationship && grandChildComparisonObject.MergeAction == MergeAction.Delete) + { + Table tableTarget = _targetTabularModel.Tables.FindById(childComparisonObject.TargetObjectId); + if (tableTarget != null) + { + // Relationship may have already been deleted if parent table was deleted + tableTarget.DeleteRelationship(grandChildComparisonObject.TargetObjectId); + } + OnValidationMessage(new ValidationMessageEventArgs("Delete Relationship " + grandChildComparisonObject.TargetObjectName.Trim() + ".", ValidationMessageType.Relationship, ValidationMessageStatus.Informational)); + } + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Relationship && grandChildComparisonObject.MergeAction == MergeAction.Create) + { + Table tableSource = _sourceTabularModel.Tables.FindById(childComparisonObject.SourceObjectId); + Table tableTarget = _targetTabularModel.Tables.FindByName(childComparisonObject.SourceObjectName); + Relationship relationshipSource = tableSource.Relationships.FindById(grandChildComparisonObject.SourceObjectId); + Table parentTableSource = _sourceTabularModel.Tables.FindByName(relationshipSource.ParentTableName); + + string warningMessage = "Unable to create Relationship " + grandChildComparisonObject.SourceObjectName.Trim() + " because (considering changes) necessary table/column(s) not found in target model."; + if (tableTarget != null && tableTarget.CreateRelationship(relationshipSource, parentTableSource.AmoDimension, grandChildComparisonObject.SourceObjectName.Trim(), ref warningMessage)) + { + OnValidationMessage(new ValidationMessageEventArgs("Create Relationship " + grandChildComparisonObject.SourceObjectName.Trim() + ".", ValidationMessageType.Relationship, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs(warningMessage, ValidationMessageType.Relationship, ValidationMessageStatus.Warning)); + } + } + } + } + } + + #endregion + + _targetTabularModel.CheckRelationshipValidity(); + _targetTabularModel.PopulateReferenceDimensions(); + + #region Measures / KPIs + + // delete measures + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Measure && grandChildComparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteMeasure(grandChildComparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Measure '" + grandChildComparisonObject.TargetObjectId + "'.", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + } + } + // need to delete KPIs now to minimize chance of conflict - but show the message later + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi && grandChildComparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteKpi(grandChildComparisonObject.TargetObjectId); + //OnValidationMessage(new ValidationMessageEventArgs("Delete KPI '" + grandChildComparisonObject.TargetObjectId + "'.", ValidationMessageType.Kpi, ValidationMessageStatus.MergeActionSuccessful); + } + } + } + } + + // now finish off measures + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Measure && grandChildComparisonObject.MergeAction == MergeAction.Create) + { + if (_targetTabularModel.Measures.ContainsName(_sourceTabularModel.Measures.FindById(grandChildComparisonObject.SourceObjectId).Name) || + _targetTabularModel.Kpis.ContainsName(_sourceTabularModel.Measures.FindById(grandChildComparisonObject.SourceObjectId).Name)) + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create Measure " + grandChildComparisonObject.SourceObjectId + " because measure name already exists in target model.", ValidationMessageType.Measure, ValidationMessageStatus.Warning)); + } + else + { + _targetTabularModel.CreateMeasure(_sourceTabularModel.Measures.FindById(grandChildComparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create Measure " + grandChildComparisonObject.SourceObjectId + ".", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Measure && grandChildComparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateMeasure(_sourceTabularModel.Measures.FindById(grandChildComparisonObject.SourceObjectId), _targetTabularModel.Measures.FindById(grandChildComparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update Measure '" + grandChildComparisonObject.TargetObjectId + "'.", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + } + } + + // now finish off KPIs + // start by showing the messages we didn't when deleted above + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi && grandChildComparisonObject.MergeAction == MergeAction.Delete) + { + OnValidationMessage(new ValidationMessageEventArgs("Delete KPI '" + grandChildComparisonObject.TargetObjectId + "'.", ValidationMessageType.Kpi, ValidationMessageStatus.Informational)); + } + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi && grandChildComparisonObject.MergeAction == MergeAction.Create) + { + if (_targetTabularModel.Kpis.ContainsName(_sourceTabularModel.Kpis.FindById(grandChildComparisonObject.SourceObjectId).Name) || + _targetTabularModel.Measures.ContainsName(_sourceTabularModel.Kpis.FindById(grandChildComparisonObject.SourceObjectId).Name)) + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create KPI " + grandChildComparisonObject.SourceObjectId + " because name already exists in target model as a KPI or measure.", ValidationMessageType.Kpi, ValidationMessageStatus.Warning)); + } + else + { + _targetTabularModel.CreateKpi(_sourceTabularModel.Kpis.FindById(grandChildComparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create KPI " + grandChildComparisonObject.SourceObjectId + ".", ValidationMessageType.Kpi, ValidationMessageStatus.Informational)); + } + } + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + if (grandChildComparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi && grandChildComparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateKpi(_sourceTabularModel.Kpis.FindById(grandChildComparisonObject.SourceObjectId), _targetTabularModel.Kpis.FindById(grandChildComparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update KPI '" + grandChildComparisonObject.TargetObjectId + "'.", ValidationMessageType.Kpi, ValidationMessageStatus.Informational)); + } + } + } + } + + #endregion + + _targetTabularModel.PopulateMdxScript(); + CheckCalcPropsAnnotations(); + + #region Actions + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Action && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteAction(comparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Action [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Action, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Action && comparisonObject.MergeAction == MergeAction.Create) + { + if (_targetTabularModel.AmoDatabase.Cubes.Count > 0) + { + _targetTabularModel.CreateAction(_sourceTabularModel.Actions.FindById(comparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create Action [" + comparisonObject.SourceObjectName + "].", ValidationMessageType.Action, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create Action " + comparisonObject.SourceObjectName + " because public \"cube\" not found in target. There must be at least one data source/table in target, for there to be an public \"cube\".", ValidationMessageType.Action, ValidationMessageStatus.Warning)); + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Action && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.MergeAction(_sourceTabularModel.Actions.FindById(comparisonObject.SourceObjectId), _targetTabularModel.Actions.FindById(comparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update Action [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Action, ValidationMessageStatus.Informational)); + } + } + + #endregion + + #region Perspectives + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeletePerspective(comparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Perspective [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Create) + { + if (_targetTabularModel.AmoDatabase.Cubes.Count > 0) + { + _targetTabularModel.CreatePerspective(_sourceTabularModel.Perspectives.FindById(comparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create Perspective [" + comparisonObject.SourceObjectName + "].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create Perspective " + comparisonObject.SourceObjectName + " because public \"cube\" not found in target. There must be at least one data source/table in target, for there to be an public \"cube\".", ValidationMessageType.Perspective, ValidationMessageStatus.Warning)); + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdatePerspective(_sourceTabularModel.Perspectives.FindById(comparisonObject.SourceObjectId), _targetTabularModel.Perspectives.FindById(comparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update Perspective [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + } + + #endregion + + #region Roles + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteRole(comparisonObject.TargetObjectId); + OnValidationMessage(new ValidationMessageEventArgs("Delete Role [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Create) + { + if (_targetTabularModel.AmoDatabase.Cubes.Count > 0) + { + _targetTabularModel.CreateRole(_sourceTabularModel.Roles.FindById(comparisonObject.SourceObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Create Role [" + comparisonObject.SourceObjectName + "].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs("Unable to create Role " + comparisonObject.SourceObjectName + " because public \"cube\" not found in target. There must be at least one data source/table in target, for there to be an public \"cube\".", ValidationMessageType.Role, ValidationMessageStatus.Warning)); + } + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateRole(_sourceTabularModel.Roles.FindById(comparisonObject.SourceObjectId), _targetTabularModel.Roles.FindById(comparisonObject.TargetObjectId)); + OnValidationMessage(new ValidationMessageEventArgs("Update Role [" + comparisonObject.TargetObjectName + "].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + } + + #endregion + + #region Missing calculation dependencies + + if (_comparisonInfo.OptionsInfo.OptionMeasureDependencies) + { + foreach (Measure measure in _targetTabularModel.Measures) + { + foreach (string missingDependency in measure.FindMissingCalculationDependencies()) + { + OnValidationMessage(new ValidationMessageEventArgs("Measure " + measure.Id + " contains dependency on measure/column [" + missingDependency + "], which (considering changes to target) cannot be found in target model.", ValidationMessageType.MeasureCalculationDependency, ValidationMessageStatus.Warning)); + } + } + + foreach (Kpi kpi in _targetTabularModel.Kpis) + { + foreach (string missingDependency in kpi.FindMissingCalculationDependencies()) + { + OnValidationMessage(new ValidationMessageEventArgs("KPI " + kpi.Id + " contains dependency on measure/column [" + missingDependency + "], which (considering changes to target) cannot be found in target model.", ValidationMessageType.MeasureCalculationDependency, ValidationMessageStatus.Warning)); + } + } + } + + #endregion + + _targetTabularModel.FinalCleanup(); + + OnResizeValidationHeaders(new EventArgs()); + } + + #region Private methods for validation + + private void CheckCalcPropsAnnotations() + { + if (_sourceTabularModel.AmoDatabase.Cubes.Count > 0 && _targetTabularModel.AmoDatabase.Cubes.Count > 0) + { + if ( (_sourceTabularModel.AmoDatabase.Cubes[0].MdxScripts.Contains("MdxScript") && _sourceTabularModel.AmoDatabase.Cubes[0].MdxScripts["MdxScript"].CalculationProperties.Contains("Measures.[__No measures defined]")) && + (_targetTabularModel.AmoDatabase.Cubes[0].MdxScripts.Contains("MdxScript") && !_targetTabularModel.AmoDatabase.Cubes[0].MdxScripts["MdxScript"].CalculationProperties.Contains("Measures.[__No measures defined]")) + ) + { + _targetTabularModel.CreateCalculationProperty(_sourceTabularModel.AmoDatabase.Cubes[0].MdxScripts["MdxScript"].CalculationProperties["Measures.[__No measures defined]"], "Measures.[__No measures defined]"); + } + if (_sourceTabularModel.AmoDatabase.Cubes[0].Annotations.Contains("DefaultMeasure") && !_targetTabularModel.AmoDatabase.Cubes[0].Annotations.Contains("DefaultMeasure")) + { + _targetTabularModel.AmoDatabase.Cubes[0].Annotations.Add(_sourceTabularModel.AmoDatabase.Cubes[0].Annotations["DefaultMeasure"].Clone()); + } + if (_sourceTabularModel.AmoDatabase.Cubes[0].ProactiveCaching != null && _targetTabularModel.AmoDatabase.Cubes[0].ProactiveCaching == null) + { + _targetTabularModel.AmoDatabase.Cubes[0].ProactiveCaching = _sourceTabularModel.AmoDatabase.Cubes[0].ProactiveCaching.Clone(); + } + } + } + + private void InitializeSubstituteIds() + { + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + childComparisonObject.SourceObjectSubstituteId = null; + } + } + } + + #endregion + + /// + /// Update target tabular model with changes defined by actions in ComparisonObject instances. + /// + /// Flag to indicate whether update was successful. + public override bool Update() => _targetTabularModel.Update(); + + /// + /// Gets a collection of ProcessingTable objects depending on Process Affected Tables option. + /// + /// Collection of ProcessingTable objects. + public override ProcessingTableCollection GetTablesToProcess() + { + ProcessingTableCollection tablesToProcess = new ProcessingTableCollection(); + + if (_comparisonInfo.OptionsInfo.OptionProcessingOption != ProcessingOption.DoNotProcess) + { + if (_comparisonInfo.OptionsInfo.OptionAffectedTables) + { + foreach (Core.ComparisonObject comparisonObject in _comparisonObjects) + { + ProcessAffectedTables(comparisonObject, tablesToProcess); + } + } + else + { + foreach (Table table in _targetTabularModel.Tables) + { + tablesToProcess.Add(new ProcessingTable(table.Name, table.Id)); + } + } + } + + tablesToProcess.Sort(); + return tablesToProcess; + } + + private void ProcessAffectedTables(Core.ComparisonObject comparisonObject, ProcessingTableCollection tablesToProcess) + { + //Recursively call for multiple levels to ensure catch calculated tables or those child of data source + + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Table && + (comparisonObject.MergeAction == MergeAction.Create || comparisonObject.MergeAction == MergeAction.Update) + ) + { + tablesToProcess.Add(new ProcessingTable(comparisonObject.SourceObjectName, comparisonObject.SourceObjectInternalName)); + } + + foreach (Core.ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + ProcessAffectedTables(childComparisonObject, tablesToProcess); + } + } + + /// + /// Deploy database to target server and perform processing if required. + /// + /// + public override void DatabaseDeployAndProcess(ProcessingTableCollection tablesToProcess) + { + _targetTabularModel.DatabaseDeployAndProcess(tablesToProcess); + } + + /// + /// Stop processing of deployed database. + /// + public override void StopProcessing() + { + _targetTabularModel.StopProcessing(); + } + + /// + /// Generate script of target database including changes. + /// + /// Script. + public override string ScriptDatabase() => _targetTabularModel.ScriptDatabase(); + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_sourceTabularModel != null) + { + _sourceTabularModel.Dispose(); + } + if (_targetTabularModel != null) + { + _targetTabularModel.Dispose(); + } + } + + _disposed = true; + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ComparisonObject.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ComparisonObject.cs new file mode 100644 index 0000000..b1a86a2 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ComparisonObject.cs @@ -0,0 +1,188 @@ +using System; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents source and target objects for comparison, their type and status. This class is for tabular models that use multidimensional metadata with SSAS compatibility level 1100 or 1103. + /// + public class ComparisonObject : Core.ComparisonObject + { + private ITabularObject _sourceObject; + private string _sourceObjectName; + private string _sourceObjectId; + private string _sourceObjectSubstituteId; + private string _sourceObjectDefinition; + private ITabularObject _targetObject; + private string _targetObjectName; + private string _targetObjectId; + private string _targetObjectDefinition; + + public ComparisonObject( + ComparisonObjectType comparisonObjectType, + ComparisonObjectStatus status, + ITabularObject sourceObject, + string sourceObjectName, + string sourceObjectId, + string sourceObjectDefinition, + MergeAction mergeAction, + ITabularObject targetObject, + string targetObjectName, + string targetObjectId, + string targetObjectDefinition) : base(comparisonObjectType, status, mergeAction) + { + _sourceObject = sourceObject; + _sourceObjectName = sourceObjectName; + _sourceObjectId = sourceObjectId; + _sourceObjectDefinition = sourceObjectDefinition; + _targetObject = targetObject; + _targetObjectName = targetObjectName; + _targetObjectId = targetObjectId; + _targetObjectDefinition = targetObjectDefinition; + } + + /// + /// Source ITabularObject instance for comparison. + /// + public ITabularObject SourceObject + { + get { return _sourceObject; } + set { _sourceObject = value; } + } + + /// + /// Name of source ITabularObject instance. + /// + public override string SourceObjectName + { + get { return _sourceObjectName; } + } + + /// + /// Id of source ITabularObject instance. This is set by the Id property used by multidimensional metadata. + /// + public string SourceObjectId + { + get { return _sourceObjectId; } + set { _sourceObjectId = value; } + } + + /// + /// Id of source ITabularObject instance. This is a replacement Id to avoid conflict with existing Ids in target model. + /// + public string SourceObjectSubstituteId + { + get + { + if (_sourceObjectSubstituteId == null) + { + return _sourceObjectId; + } + else + { + return _sourceObjectSubstituteId; + } + } + set + { + _sourceObjectSubstituteId = value; + } + } + + /// + /// Definition of source ITabularObject instance. + /// + public override string SourceObjectDefinition + { + get { return _sourceObjectDefinition; } + } + + /// + /// Internal name of source ITabularObject instance. + /// + public override string SourceObjectInternalName => SourceObjectSubstituteId; + + /// + /// Target ITabularObject instance for comparison. + /// + public ITabularObject TargetObject + { + get { return _targetObject; } + set { _targetObject = value; } + } + + /// + /// Name of target ITabularObject instance. + /// + public override string TargetObjectName + { + get { return _targetObjectName; } + } + + /// + /// Id of target ITabularObject instance. This is set by the Id property used by multidimensional metadata. + /// + public string TargetObjectId + { + get { return _targetObjectId; } + set { _targetObjectId = value; } + } + + /// + /// Definition of target ITabularObject instance. + /// + public override string TargetObjectDefinition + { + get { return _targetObjectDefinition; } + } + + /// + /// Internal name of source ITabularObject instance. + /// + public override string TargetObjectInternalName => TargetObjectId; + + /// + /// Provides key for CompareTo method. + /// + public override string SortKey() + { + string sortKey = ""; + + switch (this.ComparisonObjectType) + { + //tabular objects + case ComparisonObjectType.DataSource: + sortKey = "A"; + break; + case ComparisonObjectType.Table: + sortKey = "B"; + break; + case ComparisonObjectType.Relationship: + sortKey = "C"; + break; + case ComparisonObjectType.Measure: + sortKey = "D"; + break; + case ComparisonObjectType.Kpi: + sortKey = "E"; + break; + case ComparisonObjectType.Action: + sortKey = "F"; + break; + case ComparisonObjectType.Perspective: + sortKey = "G"; + break; + case ComparisonObjectType.Role: + sortKey = "H"; + break; + + default: + sortKey = "Z"; + break; + } + sortKey += this._sourceObjectName != "" ? this._sourceObjectName : this._targetObjectName; + return sortKey; + } + public override int CompareTo(Core.ComparisonObject other) => string.Compare(this.SortKey(), other.SortKey()); + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSource.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSource.cs new file mode 100644 index 0000000..4d661cf --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSource.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using Microsoft.AnalysisServices; +using Amo=Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model data source with properties and methods for comparison purposes. + /// + public class DataSource : ITabularObject + { + private TabularModel _parentTabularModel; + private Microsoft.AnalysisServices.DataSource _amoDataSource; + private string _substituteId; + + /// + /// Initializes a new instance of the DataSource class using multiple parameters. + /// + /// TabularModel object that the DataSource object belongs to. + /// Analysis Management Objects DataSource object abtstracted by the DataSource object. + public DataSource(TabularModel parentTabularModel, Microsoft.AnalysisServices.DataSource datasource) + { + _parentTabularModel = parentTabularModel; + _amoDataSource = datasource; + } + + /// + /// TabularModel object that the DataSource object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Analysis Management Objects DataSource object abtstracted by the DataSource object. + /// + public Amo.DataSource AmoDataSource => _amoDataSource; + + /// + /// Name of the DataSource object. + /// + public string Name => _amoDataSource.Name; + + /// + /// Long name of the DataSource object. + /// + public string LongName => _amoDataSource.Name; + + /// + /// Id of the DataSource object. + /// + public string Id => _amoDataSource.ID; + + /// + /// Object definition of the DataSource object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public string ObjectDefinition + { + get + { + //the order of items in the connection string is not guaranteed to come out in a consistent order ... + string[] elements = _amoDataSource.ConnectionString.Split(';'); + Array.Sort(elements); + string returnValue = string.Empty; + foreach (string element in elements) + { + returnValue += element + ";"; + } + return returnValue.Substring(0, returnValue.Length - 1) + "\n"; + } + } + + /// + /// Substitute Id of the DataSource object. + /// + public string SubstituteId + { + get + { + if (string.IsNullOrEmpty(_substituteId)) + { + return _amoDataSource.ID; + } + else + { + return _substituteId; + } + } + set + { + _substituteId = value; + } + } + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSourceCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSourceCollection.cs new file mode 100644 index 0000000..5ffa06b --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/DataSourceCollection.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of DataSource objects. + /// + public class DataSourceCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// DataSource object if found. Null if not found. + public DataSource FindByName(string name) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Name == name) + { + return dataSource; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// DataSource object if found. Null if not found. + public DataSource FindById(string id) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Id == id) + { + return dataSource; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Id == id) + { + this.Remove(dataSource); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ITabularObject.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ITabularObject.cs new file mode 100644 index 0000000..7295c32 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/ITabularObject.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a tabular object for comparison. + /// + public interface ITabularObject + { + /// + /// Name of the tabular object. + /// + string Name { get; } + + /// + /// Long name of the tabular object. + /// + string LongName { get; } + + /// + /// Id of the tabular object. + /// + string Id { get; } + + /// + /// Object definition of the tabular object. + /// + string ObjectDefinition { get; } + + /// + /// Substitute Id of the tabular object. + /// + string SubstituteId { get; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Kpi.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Kpi.cs new file mode 100644 index 0000000..d63365c --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Kpi.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model KPI with properties and methods for comparison purposes. + /// + public class Kpi : Measure + { + private Measure _goalMeasure; + private Measure _statusMeasure; + private Measure _trendMeasure; + private string _statusGraphic; + private string _trendGraphic; + + /// + /// Initializes a new instance of the Kpi class using multiple parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Kpi(TabularModel parentTabularModel, string tableName, string measureName, string expression, + Measure goalMeasure, Measure statusMeasure, Measure trendMeasure, string statusGraphic, string trendGraphic) //, Kpi kpi) + : base(parentTabularModel, tableName, measureName, expression) + { + _goalMeasure = goalMeasure; + _statusMeasure = statusMeasure; + _trendMeasure = trendMeasure; + _statusGraphic = statusGraphic; + _trendGraphic = trendGraphic; + //_amoKpi = kpi; + } + + /// + /// Goal measure of the Kpi object. + /// + public Measure GoalMeasure + { + get { return _goalMeasure; } + set { _goalMeasure = value; } + } + + /// + /// Goal calculation reference of the Kpi object. + /// + public string GoalCalculationReference => "[" + _goalMeasure.Name + "]"; + + /// + /// Analysis Management Objects CalculationProperty object for goal abtstracted by the Kpi object. + /// + public CalculationProperty AmoGoalCalculationProperty + { + get + { + if (this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(this.GoalCalculationReference)) + { + return this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties[this.GoalCalculationReference]; + } + else + { + return null; + } + } + } + + /// + /// Status measure of the Kpi object. + /// + public Measure StatusMeasure + { + get { return _statusMeasure; } + set { _statusMeasure = value; } + } + + /// + /// Status calculation reference of the Kpi object. + /// + public string StatusCalculationReference => $"[{_statusMeasure.Name}]"; + + /// + /// Analysis Management Objects CalculationProperty object for status abtstracted by the Kpi object. + /// + public CalculationProperty AmoStatusCalculationProperty + { + get + { + if (this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(this.StatusCalculationReference)) + { + return this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties[this.StatusCalculationReference]; + } + else + { + return null; + } + } + } + + /// + /// Trend measure of the Kpi object. + /// + public Measure TrendMeasure + { + get { return _trendMeasure; } + set { _trendMeasure = value; } + } + + /// + /// Trend calculation reference of the Kpi object. + /// + public string TrendCalculationReference => $"[{_trendMeasure.Name}]"; + + /// + /// Analysis Management Objects CalculationProperty object for trend abtstracted by the Kpi object. + /// + public CalculationProperty AmoTrendCalculationProperty + { + get + { + if (this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(this.TrendCalculationReference)) + { + return this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties[this.TrendCalculationReference]; + } + else + { + return null; + } + } + } + + /// + /// Status graphic of the Kpi object. + /// + public string StatusGraphic + { + get { return _statusGraphic; } + set { _statusGraphic = value; } + } + + /// + /// Trend graphic of the Kpi object. + /// + public string TrendGraphic + { + get { return _trendGraphic; } + set { _trendGraphic = value; } + } + + /// + /// Calculation reference of the Kpi object. + /// + public string KpiCalculationReference => $"KPIs.[{this.Name}]"; + + /// + /// Analysis Management Objects CalculationProperty object for the Kpi object. + /// + public CalculationProperty AmoKpiCalculationProperty + { + get + { + if (this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(this.KpiCalculationReference)) + { + return this.ParentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties[this.KpiCalculationReference]; + } + else + { + //an old version of Tabular Editor didn't use KPI declarations in the MDX script. Instead it used the AMO object model. + //If the KPI happens to be an AMO object model one, will have an issue, so quickly create a calc ref + /* + + + + Type + SupportKpi + + + MainObjectType + Measure + + + MainObjectName + Total Inventory Value Performance + + + KPIs.[Total Inventory Value Performance] + Member + + */ + + CalculationProperty amoKpiCalculationProperty = new CalculationProperty(this.KpiCalculationReference, CalculationType.Member); + amoKpiCalculationProperty.Annotations.Add("Type", "SupportKpi"); + amoKpiCalculationProperty.Annotations.Add("MainObjectType", "Measure"); + amoKpiCalculationProperty.Annotations.Add("MainObjectName", this.Name); + return amoKpiCalculationProperty; + } + } + } + + /// + /// Object definition of the Kpi object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public override string ObjectDefinition => base.ObjectDefinition + + "Goal:\n" + _goalMeasure.Expression + "\n\n" + + "Status:\n" + _statusMeasure.Expression + "\n\n" + + //"Trend:\n" + _trendMeasure.Expression + "\n\n" + + "Status Graphic:\n" + _statusGraphic + "\n"; //\n" + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/KpiCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/KpiCollection.cs new file mode 100644 index 0000000..f66f80d --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/KpiCollection.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of Kpi objects. + /// + public class KpiCollection : List + { + /// + /// Find an object in the collection by Id. + /// + /// + /// Kpi object if found. Null if not found. + public Kpi FindById(string id) + { + foreach (Kpi kpi in this) + { + if (kpi.Id == id) + { + return kpi; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Kpi kpi in this) + { + if (kpi.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by name. + /// + /// + /// Kpi object if found. Null if not found. + public Kpi FindByName(string name) + { + foreach (Kpi kpi in this) + { + if (kpi.Name == name) + { + return kpi; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Kpi BismKpi in this) + { + if (BismKpi.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Kpi objects filtered by the parent table's name. + /// + /// + /// KpiCollection + public KpiCollection FilterByTableName(string tableName) + { + KpiCollection returnMeasures = new KpiCollection(); + foreach (Kpi kpi in this) + { + if (kpi.TableName == tableName) + { + returnMeasures.Add(kpi); + } + } + return returnMeasures; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Kpi kpi in this) + { + if (kpi.Id == id) + { + this.Remove(kpi); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Measure.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Measure.cs new file mode 100644 index 0000000..6d6fc69 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Measure.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; +using Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model measure with properties and methods for comparison purposes. + /// + public class Measure : ITabularObject + { + private TabularModel _parentTabularModel; + private string _tableName; + private string _name; + private string _expression; + private string _objectDefinition; + private bool _IsKpiReferenceMeasure; + + /// + /// Initializes a new instance of the Measure class using multiple parameters. + /// + /// TabularModel object that the measure belongs to. + /// Name of the table that the Measure belongs to. + /// Name of the measure. + /// DAX expression + public Measure(TabularModel parentTabularModel, string tableName, string measureName, string expression) + { + _parentTabularModel = parentTabularModel; + _tableName = tableName; + _name = measureName; + _expression = expression; + _IsKpiReferenceMeasure = false; + + PopulateObjectDefinition(); + } + + private void PopulateObjectDefinition() + { + _objectDefinition = "Expression:\n" + _expression + "\n\n"; + + if (this.AmoCalculationProperty != null) + { + if (this.AmoCalculationProperty.Annotations.Contains("Format") && this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Format"] != null) + { + _objectDefinition += "Format & Visibility:\n"; + switch (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Format"].Value) + { + case "General": + _objectDefinition += "Format: General"; + break; + case "NumberDecimal": + _objectDefinition += "Format: Decimal Number" + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "NumberWhole": + _objectDefinition += "Format: Whole Number" + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "Percentage": + _objectDefinition += "Format: Percentage" + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "Scientific": + _objectDefinition += "Format: Scientific" + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"].Value : ""); + break; + case "Currency": + _objectDefinition += "Format: Currency" + + (this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + this.AmoCalculationProperty.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (this.AmoCalculationProperty.Annotations["Format"].Value.HasChildNodes && + this.AmoCalculationProperty.Annotations["Format"].Value.ChildNodes[0].Attributes["DisplayName"] != null + ? ", Currency Symbol: " + this.AmoCalculationProperty.Annotations["Format"].Value.ChildNodes[0].Attributes["DisplayName"].Value : ""); + break; + case "DateTimeCustom": + _objectDefinition += "Format: Date" + + (this.AmoCalculationProperty.Annotations["Format"].Value.HasChildNodes && + this.AmoCalculationProperty.Annotations["Format"].Value.ChildNodes[0].HasChildNodes && + this.AmoCalculationProperty.Annotations["Format"].Value.ChildNodes[0].ChildNodes[0].Attributes["FormatString"] != null + ? ", Date Format: " + this.AmoCalculationProperty.Annotations["Format"].Value.ChildNodes[0].ChildNodes[0].Attributes["FormatString"].Value : ""); + break; + case "Boolean": + _objectDefinition += "Format: TRUE/FALSE"; + break; + default: + break; + } + if (this.AmoCalculationProperty.Annotations.Contains("IsPrivate") && this.AmoCalculationProperty.Annotations["IsPrivate"].Value != null) + { + _objectDefinition += ", Hidden: " + this.AmoCalculationProperty.Annotations["IsPrivate"].Value.Value; + } + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + //{ + // _objectDefinition += ", Display Folder: " + (this.AmoCalculationProperty.DisplayFolder == null ? "" : this.AmoCalculationProperty.DisplayFolder); + //} + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += ", Measure Translations: "; + // if (this.AmoCalculationProperty.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation measureTranslation in this.AmoCalculationProperty.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(measureTranslation.Language).DisplayName + ": " + measureTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (this.AmoCalculationProperty.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation measureDisplayFolderTranslation in this.AmoCalculationProperty.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(measureDisplayFolderTranslation.Language).DisplayName + ": " + measureDisplayFolderTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + //} + _objectDefinition += "\n\n"; + } + } + } + + /// + /// TabularModel object that the Measure object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Name of the table that the Measure oject belongs to. + /// + public string TableName + { + get { return _tableName; } + set { _tableName = value; } + } + + /// + /// Name of the Measure object. + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Long name of the Measure object. + /// + public string LongName => _name; + + /// + /// Id of the Measure object. + /// + public string Id => $"'{_tableName}'[{_name}]"; + + /// + /// Substitute Id of the Measure object. + /// + public string SubstituteId => this.Id; + + /// + /// DAX expression of the Measure object. + /// + public string Expression + { + get { return _expression; } + set { _expression = value; } + } + + /// + /// Boolean indicating if the Measure object is a KPI reference measure. + /// + public bool IsKpiReferenceMeasure + { + get { return _IsKpiReferenceMeasure; } + set { _IsKpiReferenceMeasure = value; } + } + + /// + /// Object definition of the Measure object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public virtual string ObjectDefinition => _objectDefinition; + + /// + /// Calculation reference of the Measure object. + /// + public string CalculationReference => $"[{_name}]"; + + /// + /// Analysis Management Objects CalculationProperty object for the Measure object. + /// + public CalculationProperty AmoCalculationProperty + { + get + { + if (_parentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(this.CalculationReference)) + { + return _parentTabularModel.AmoDatabase.Cubes[0].MdxScripts[0].CalculationProperties[this.CalculationReference]; + } + else + { + return null; + } + } + } + + public override string ToString() => this.GetType().FullName; + + /// + /// Find missing dependencies. + /// + /// List of dependencies. + public List FindMissingCalculationDependencies() + { + List dependencies = new List(); + + using (StringReader lines = new StringReader(_expression)) + { + string line = string.Empty; + while ((line = lines.ReadLine()) != null) + { + string whatsLeftOfLine = line; + + while (whatsLeftOfLine.Contains('[') && whatsLeftOfLine.Contains(']')) + { + int openSquareBracketPosition = whatsLeftOfLine.IndexOf('[', 0); + //brilliant person at microsoft has ]] instead of ] + int closeSquareBracketPosition = whatsLeftOfLine.Replace("]]", " ").IndexOf(']', openSquareBracketPosition + 1); + + if (openSquareBracketPosition < closeSquareBracketPosition - 1) + { + string potentialDependency = whatsLeftOfLine.Substring(openSquareBracketPosition + 1, closeSquareBracketPosition - openSquareBracketPosition - 1); + if (!potentialDependency.Contains('"') && !dependencies.Contains(potentialDependency)) + { + //unbelievable: some genius at m$ did a replace on ] with ]] + dependencies.Add(potentialDependency); + } + } + + whatsLeftOfLine = whatsLeftOfLine.Substring(closeSquareBracketPosition + 1); + } + } + } + + List missingDependencies = new List(); + foreach (string dependency in dependencies) + { + bool foundDependency = false; + + //need to check internal measures because of references to KPI goals in measures + //// check if it is another measure + //foreach (Measure measure in _parentTabularModel.Measures) + //{ + // if (measure.Name == dependency) + // { + // foundDependency = true; + // break; + // } + //} + foreach (Measure measure in _parentTabularModel.MeasuresFull) + { + if (measure.Name == dependency) + { + foundDependency = true; + break; + } + } + + if (!foundDependency) + { + // check if it is a kpi + foreach (Measure kpi in _parentTabularModel.Kpis) + { + if (kpi.Name == dependency) + { + foundDependency = true; + break; + } + } + } + + if (!foundDependency) + { + // check if it is a column + foreach (Table table in _parentTabularModel.Tables) + { + foreach (DimensionAttribute column in table.AmoDimension.Attributes) + { + if (column.Name == dependency) + { + foundDependency = true; + break; + } + } + if (foundDependency) + { + break; + } + } + } + + if (!foundDependency) + { + missingDependencies.Add(dependency); + } + } + + return missingDependencies; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/MeasureCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/MeasureCollection.cs new file mode 100644 index 0000000..3b3806c --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/MeasureCollection.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of Measure objects. + /// + public class MeasureCollection : List + { + /// + /// Find an object in the collection by Id. + /// + /// + /// Measure object if found. Null if not found. + public Measure FindById(string id) + { + foreach (Measure measure in this) + { + if (measure.Id == id) + { + return measure; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Measure measure in this) + { + if (measure.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by name. + /// + /// + /// Measure object if found. Null if not found. + public Measure FindByName(string name) + { + foreach (Measure measure in this) + { + if (measure.Name == name) + { + return measure; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Measure measure in this) + { + if (measure.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Measure objects filtered by the parent table's name. + /// + /// + /// MeasureCollection + public MeasureCollection FilterByTableName(string tableName) + { + MeasureCollection returnMeasures = new MeasureCollection(); + foreach (Measure measure in this) + { + if (measure.TableName == tableName) + { + returnMeasures.Add(measure); + } + } + return returnMeasures; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Measure measure in this) + { + if (measure.Id == id) + { + this.Remove(measure); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Perspective.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Perspective.cs new file mode 100644 index 0000000..47e987e --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Perspective.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using Microsoft.AnalysisServices; +using Amo=Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model perspective with properties and methods for comparison purposes. + /// + public class Perspective : ITabularObject + { + private TabularModel _parentTabularModel; + private Microsoft.AnalysisServices.Perspective _amoPerspective; + private string _objectDefinition; + private string _substituteId; + + /// + /// Initializes a new instance of the Perspective class using multiple parameters. + /// + /// TabularModel object that the perspective belongs to. + /// Analysis Management Objects Perspective object abtstracted by the Perspective class. + public Perspective(TabularModel parentTabularModel, Amo.Perspective amoPerspective) + { + _parentTabularModel = parentTabularModel; + _amoPerspective = amoPerspective; + _objectDefinition = ""; + + List perspectiveTableNames = new List(); // put in here to sort + foreach (PerspectiveDimension perspectiveDimension in _amoPerspective.Dimensions) + { + perspectiveTableNames.Add(perspectiveDimension.Dimension.Name); + } + // now need to check if there are measures/KPIs where no other table objects are selected in the perspective - because if so won't have a .Dimension value + foreach (PerspectiveCalculation perspectiveCalculation in _amoPerspective.Calculations) + { + string measureName = perspectiveCalculation.Name.Replace("[Measures].[", "").Replace("]", ""); + if (_parentTabularModel.Measures.ContainsName(measureName) && + !perspectiveTableNames.Contains(_parentTabularModel.Measures.FindByName(measureName).TableName)) + { + perspectiveTableNames.Add(_parentTabularModel.Measures.FindByName(measureName).TableName); + } + } + foreach (PerspectiveKpi perspectiveKpi in _amoPerspective.Kpis) + { + string kpiName = perspectiveKpi.ToString(); + if (_parentTabularModel.Kpis.ContainsName(kpiName) && + !perspectiveTableNames.Contains(_parentTabularModel.Kpis.FindByName(kpiName).TableName)) + { + perspectiveTableNames.Add(_parentTabularModel.Kpis.FindByName(kpiName).TableName); + } + } + perspectiveTableNames.Sort(); + + foreach (string perspectiveTableName in perspectiveTableNames) + { + _objectDefinition += perspectiveTableName + "\n"; + + // find the PerspectiveDimension again + foreach (PerspectiveDimension perspectiveDimension in _amoPerspective.Dimensions) + { + if (perspectiveDimension.Dimension.Name == perspectiveTableName) + { + //Attributes + List perspectiveAttributeNames = new List(); // put in here to sort + foreach (PerspectiveAttribute perspectiveAttribute in perspectiveDimension.Attributes) + { + perspectiveAttributeNames.Add(perspectiveAttribute.Attribute.Name); + } + perspectiveAttributeNames.Sort(); + foreach (string perspectiveAttributeName in perspectiveAttributeNames) + { + _objectDefinition += " " + perspectiveAttributeName + "\n"; + } + + //Hierarchies + List perspectiveHierarchyNames = new List(); // put in here to sort + foreach (PerspectiveHierarchy perspectiveHierarchy in perspectiveDimension.Hierarchies) + { + perspectiveHierarchyNames.Add(perspectiveHierarchy.Hierarchy.Name); + } + perspectiveHierarchyNames.Sort(); + foreach (string perspectiveHierarchyName in perspectiveHierarchyNames) + { + _objectDefinition += " " + perspectiveHierarchyName + "\n"; + } + } + } + + //Measures + List perspectiveMeasureNames = new List(); // put in here to sort + foreach (PerspectiveCalculation perspectiveCalculation in _amoPerspective.Calculations) + { + string measureName = perspectiveCalculation.Name.Replace("[Measures].[", "").Replace("]", ""); + if (_parentTabularModel.Measures.ContainsName(measureName) && + _parentTabularModel.Measures.FindByName(measureName).TableName == perspectiveTableName) + { + perspectiveMeasureNames.Add(measureName); + } + } + perspectiveMeasureNames.Sort(); + foreach (string perspectiveCalculationName in perspectiveMeasureNames) + { + _objectDefinition += " " + perspectiveCalculationName + "\n"; + } + + //KPIs + List perspectiveKpiNames = new List(); // put in here to sort + foreach (PerspectiveKpi perspectiveKpi in _amoPerspective.Kpis) + { + string kpiName = perspectiveKpi.ToString(); + if (_parentTabularModel.Kpis.ContainsName(kpiName) && + _parentTabularModel.Kpis.FindByName(kpiName).TableName == perspectiveTableName && + !perspectiveMeasureNames.Contains(kpiName)) //last check in case already added as a measure + { + perspectiveKpiNames.Add(kpiName); + } + } + perspectiveKpiNames.Sort(); + foreach (string perspectiveCalculationName in perspectiveKpiNames) + { + _objectDefinition += " " + perspectiveCalculationName + "\n"; + } + } + + //Actions + List perspectiveActionNames = new List(); // put in here to sort + foreach (PerspectiveAction perspectiveAction in _amoPerspective.Actions) + { + if (perspectiveAction.ParentCube.Actions.Contains(perspectiveAction.ActionID)) //need this check or .Action returns error + { + string actionName = perspectiveAction.Action.Name; + if (_parentTabularModel.Actions.ContainsName(actionName)) + { + perspectiveActionNames.Add(actionName); + } + } + } + perspectiveActionNames.Sort(); + foreach (string perspectiveActionName in perspectiveActionNames) + { + _objectDefinition += "Action: " + perspectiveActionName + "\n"; + } + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += "\nFormat & Visibility:\nPerspective Translations: "; + // if (_amoPerspective.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation perspectiveTranslation in _amoPerspective.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(perspectiveTranslation.Language).DisplayName + ": " + perspectiveTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (_amoPerspective.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation perspectiveDisplayFolderTranslation in _amoPerspective.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(perspectiveDisplayFolderTranslation.Language).DisplayName + ": " + perspectiveDisplayFolderTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + // _objectDefinition += "\n"; + //} + + _objectDefinition += "\n"; + } + + /// + /// TabularModel object that the Perspective object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Analysis Management Objects Perspective object abtstracted by the Perspective class. + /// + public Amo.Perspective AmoPerspective => _amoPerspective; + + /// + /// Name of the Perspective object. + /// + public string Name => _amoPerspective.Name; + + /// + /// Long name of the Perspective object. + /// + public string LongName => _amoPerspective.Name; + + /// + /// Id of the Perspective object. + /// + public string Id => _amoPerspective.ID; + + /// + /// Object definition of the Perspective object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public string ObjectDefinition => _objectDefinition; + + /// + /// Substitute id of the Perspective object. + /// + public string SubstituteId + { + get + { + if (string.IsNullOrEmpty(_substituteId)) + { + return _amoPerspective.ID; + } + else + { + return _substituteId; + } + } + set + { + _substituteId = value; + } + } + + public override string ToString() => this.GetType().FullName; + + /// + /// Verifies whether another Perspective object's selections are included in this Perspective object. + /// + /// The other Perspective object to be verified. + /// True if otherPerspective matches. False if does not match. + public bool ContainsOtherPerspectiveSelections(Perspective otherPerspective) + { + bool everythingMatches = true; + + //Tables + foreach (PerspectiveDimension otherDimension in otherPerspective.AmoPerspective.Dimensions) + { + bool foundDimensionMatch = false; + foreach (PerspectiveDimension perspectiveDimension in _amoPerspective.Dimensions) + { + if (perspectiveDimension.Dimension.Name == otherDimension.Dimension.Name) + { + foundDimensionMatch = true; + + //Columns + foreach (PerspectiveAttribute otherAttribute in otherDimension.Attributes) + { + bool foundAttributeMatch = false; + foreach (PerspectiveAttribute perspectiveAttribute in perspectiveDimension.Attributes) + { + if (perspectiveAttribute.Attribute.Name == otherAttribute.Attribute.Name) + { + foundAttributeMatch = true; + break; + } + } + if (!foundAttributeMatch) + { + everythingMatches = false; + break; + } + } + + //Hierarchies + foreach (PerspectiveHierarchy otherHierarchy in otherDimension.Hierarchies) + { + bool foundHierarchyMatch = false; + foreach (PerspectiveHierarchy perspectiveHierarchy in perspectiveDimension.Hierarchies) + { + if (perspectiveHierarchy.Hierarchy.Name == otherHierarchy.Hierarchy.Name) + { + foundHierarchyMatch = true; + break; + } + } + if (!foundHierarchyMatch) + { + everythingMatches = false; + break; + } + } + + } + if (!everythingMatches) break; + } + + if (!foundDimensionMatch) + { + everythingMatches = false; + break; + } + } + + if (everythingMatches) + { + //Measures + foreach (PerspectiveCalculation otherCalculation in otherPerspective.AmoPerspective.Calculations) + { + string measureName = otherCalculation.Name.Replace("[Measures].[", "").Replace("]", ""); + if (otherPerspective.ParentTabularModel.Measures.ContainsName(measureName)) // this if clause shouldn't be necessary, but it is + { + bool foundCalculationMatch = false; + foreach (PerspectiveCalculation perspectiveCalculation in _amoPerspective.Calculations) + { + if (perspectiveCalculation.Name == otherCalculation.Name) + { + foundCalculationMatch = true; + break; + } + } + if (!foundCalculationMatch) + { + everythingMatches = false; + break; + } + } + } + } + + if (everythingMatches) + { + //Kpis + foreach (PerspectiveKpi otherKpi in otherPerspective.AmoPerspective.Kpis) + { + string KpiName = otherKpi.ToString(); + if (otherPerspective.ParentTabularModel.Kpis.ContainsName(KpiName)) + { + bool foundKpiMatch = false; + foreach (PerspectiveKpi perspectiveKpi in _amoPerspective.Kpis) + { + if (perspectiveKpi.ToString() == otherKpi.ToString()) + { + foundKpiMatch = true; + break; + } + } + if (!foundKpiMatch) + { + everythingMatches = false; + break; + } + } + } + } + + if (everythingMatches) + { + //Actions + foreach (PerspectiveAction otherAction in otherPerspective.AmoPerspective.Actions) + { + bool foundActionMatch = false; + foreach (PerspectiveAction perspectiveAction in _amoPerspective.Actions) + { + if (perspectiveAction.ParentCube.Actions.Contains(perspectiveAction.ActionID) && otherAction.ParentCube.Actions.Contains(otherAction.ActionID) && //need this check or .Action returns error + perspectiveAction.Action.Name == otherAction.Action.Name) + { + foundActionMatch = true; + break; + } + } + if (!foundActionMatch) + { + everythingMatches = false; + break; + } + } + } + + if (everythingMatches) + { + //Translations + foreach (Translation otherTranslation in otherPerspective.AmoPerspective.Translations) + { + bool foundActionMatch = false; + foreach (Translation perspectiveTranslation in _amoPerspective.Translations) + { + if (perspectiveTranslation.Language == otherTranslation.Language && + perspectiveTranslation.Caption == otherTranslation.Caption) + { + foundActionMatch = true; + break; + } + } + if (!foundActionMatch) + { + everythingMatches = false; + break; + } + } + } + + return everythingMatches; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/PerspectiveCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/PerspectiveCollection.cs new file mode 100644 index 0000000..2a3995f --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/PerspectiveCollection.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of Perspective objects. + /// + public class PerspectiveCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Perspective object if found. Null if not found. + public Perspective FindByName(string name) + { + foreach (Perspective perspective in this) + { + if (perspective.Name == name) + { + return perspective; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Perspective perspective in this) + { + if (perspective.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Perspective object if found. Null if not found. + public Perspective FindById(string id) + { + foreach (Perspective perspective in this) + { + if (perspective.Id == id) + { + return perspective; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Perspective perspective in this) + { + if (perspective.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Perspective perspective in this) + { + if (perspective.Id == id) + { + this.Remove(perspective); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Relationship.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Relationship.cs new file mode 100644 index 0000000..cbedff2 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Relationship.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AnalysisServices; +using Amo=Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model relationship with properties and methods for comparison purposes. + /// + public class Relationship : ITabularObject + { + private Table _table; + private Amo.Relationship _amoRelationship; + private string _name; + private string _longName; + private string _childTableName; + private string _childColumnName; + private string _parentTableName; + private string _parentColumnName; + private string _objectDefinition; + private bool _copiedFromSource; + + /// + /// Initializes a new instance of the Relationship class using multiple parameters. + /// + /// Table object that the Relationship belongs to. + /// Analysis Management Objects Relationship object abtstracted by the Relationship class. + /// Boolean indicating whether the relationship was copied from the source TabularModel object. + public Relationship(Table table, Amo.Relationship amoRelationship, bool copiedFromSource = false) + { + _table = table; + _amoRelationship = amoRelationship; + + // parentTable is actually the FK (child) table in the relationship + Dimension dimPK = table.TabularModel.AmoDatabase.Dimensions.Find(amoRelationship.ToRelationshipEnd.DimensionID); + + _childTableName = table.Name; + _childColumnName = table.AmoDimension.Attributes.Find(amoRelationship.FromRelationshipEnd.Attributes[0].AttributeID).Name; + _parentTableName = dimPK.Name; + _parentColumnName = dimPK.Attributes.Find(amoRelationship.ToRelationshipEnd.Attributes[0].AttributeID).Name; + + //_name = "'" + _childTableName + "'[" + _childColumnName + "]" + "' -> '" + _parentTableName + "'[" + _parentColumnName + "]"; + _name = "'" + _childTableName + "' -> '" + _parentTableName + "'"; + _longName = _childTableName + "'[" + _childColumnName + "] => '" + _parentTableName + "'[" + _parentColumnName + "]"; + _objectDefinition = "Foreign Key Column: '" + _childTableName + "'[" + _childColumnName + "]\n" + + "Primary Key Column: '" + _parentTableName + "'[" + _parentColumnName + "]\n"; + + _copiedFromSource = copiedFromSource; + } + + /// + /// Table object that the Relationship oject belongs to. + /// + public Table Table => _table; + + /// + /// Analysis Management Objects Relationship object abtstracted by the Relationship class. + /// + public Amo.Relationship AmoRelationship + { + get { return _amoRelationship; } + set { _amoRelationship = value; } + } + + /// + /// Name of the Relationship object. + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Long name of the Relationship object. + /// + public string LongName => _longName; + + /// + /// Id of the Relationship object. + /// + public string Id => _amoRelationship.ID; + + /// + /// Substitute Id of the Relationship object. + /// + public string SubstituteId => _amoRelationship.ID; + + /// + /// Name of the child table for the Relationship object. + /// + public string ChildTableName => _childTableName; + + /// + /// Name of the child column for the Relationship object. + /// + public string ChildColumnName => _childColumnName; + + /// + /// Name of the parent table for the Relationship object. + /// + public string ParentTableName => _parentTableName; + + /// + /// Name of the parent column for the Relationship object. + /// + public string ParentColumnName => _parentColumnName; + + /// + /// Indicates whether the Relationship object is active in the tabular model. + /// + public bool IsActive => _table.TabularModel.ActiveRelationshipIds.Contains(_amoRelationship.ID); + + /// + /// Indicates whether the relationship was copied from the source tabular model. + /// + public bool CopiedFromSource => _copiedFromSource; + + /// + /// Object definition of the Relationship object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public string ObjectDefinition => _objectDefinition; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RelationshipCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RelationshipCollection.cs new file mode 100644 index 0000000..951dbf8 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RelationshipCollection.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of Relationship objects. + /// + public class RelationshipCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Relationship object if found. Null if not found. + public Relationship FindByName(string name) + { + foreach (Relationship relationship in this) + { + if (relationship.Name == name) + { + return relationship; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Relationship relationship in this) + { + if (relationship.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Relationship object if found. Null if not found. + public Relationship FindById(string id) + { + foreach (Relationship relationship in this) + { + if (relationship.Id == id) + { + return relationship; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Relationship relationship in this) + { + if (relationship.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Relationship objects filtered by the parent table's name. + /// + /// + /// RelationshipCollection + public RelationshipCollection FilterByTableId(string tableId) + { + RelationshipCollection returnTables = new RelationshipCollection(); + foreach (Relationship relationship in this) + { + if (relationship.Table.Id == tableId) + { + returnTables.Add(relationship); + } + } + return returnTables; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Relationship relationship in this) + { + if (relationship.Id == id) + { + this.Remove(relationship); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Role.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Role.cs new file mode 100644 index 0000000..8e47857 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Role.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using Microsoft.AnalysisServices; +using Amo=Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model role with properties and methods for comparison purposes. + /// + public class Role : ITabularObject + { + private TabularModel _parentTabularModel; + private Amo.Role _amoRole; + private string _objectDefinition; + private string _substituteId; + + /// + /// Initializes a new instance of the Role class using multiple parameters. + /// + /// TabularModel object that the Role object belongs to. + /// Analysis Management Objects Role object abtstracted by the Role class. + public Role(TabularModel parentTabularModel, Amo.Role role) + { + _parentTabularModel = parentTabularModel; + _amoRole = role; + + _objectDefinition = "Permissions:\n"; + foreach (DatabasePermission dbPermission in _parentTabularModel.AmoDatabase.DatabasePermissions) + { + if (dbPermission.RoleID == _amoRole.ID) + { + if (dbPermission.Administer) + { + _objectDefinition += "Administrator\n"; + } + else if (dbPermission.Read == ReadAccess.Allowed && dbPermission.Process) + { + _objectDefinition += "Read and Process\n"; + } + else if (dbPermission.Read == ReadAccess.Allowed) + { + _objectDefinition += "Read\n"; + } + else if (dbPermission.Process) + { + _objectDefinition += "Process\n"; + } + else + { + _objectDefinition += "None\n"; + } + } + } + + _objectDefinition += "\nRow Filters:\n"; + List rowFilters = new List(); //need to be added in alphabetical order to allow comparison if obj definitions + foreach (Dimension dimension in _parentTabularModel.AmoDatabase.Dimensions) + { + foreach (DimensionPermission dimPermission in dimension.DimensionPermissions) + { + if (dimPermission.RoleID == _amoRole.ID) + { + //_objectDefinition += "[" + dimension.Name + "]=" + dimPermission.AllowedRowsExpression + "\n"; + rowFilters.Add("[" + dimension.Name + "]=" + dimPermission.AllowedRowsExpression + "\n"); + } + } + } + rowFilters.Sort(); + foreach (string rowFilter in rowFilters) + { + _objectDefinition += rowFilter; + } + + _objectDefinition += "\nMembers:\n"; + List roleMembers = new List(); //need to be added in alphabetical order to allow comparison if obj definitions + foreach (RoleMember roleMember in _amoRole.Members) + { + //_objectDefinition += roleMember.Name + "\n"; + roleMembers.Add(roleMember.Name + "\n"); + } + roleMembers.Sort(); + foreach (string roleMember in roleMembers) + { + _objectDefinition += roleMember; + } + } + + /// + /// TabularModel object that the Role object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Analysis Management Objects Role object abtstracted by the Role class. + /// + public Amo.Role AmoRole => _amoRole; + + /// + /// Name of the Role object. + /// + public string Name => _amoRole.Name; + + /// + /// Long name of the Role object. + /// + public string LongName => _amoRole.Name; + + /// + /// Id of the Role object. + /// + public string Id => _amoRole.ID; + + /// + /// Object definition of the Role object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public string ObjectDefinition => _objectDefinition; + + /// + /// Substitute Id of the Role object. + /// + public string SubstituteId + { + get + { + if (string.IsNullOrEmpty(_substituteId)) + { + return _amoRole.ID; + } + else + { + return _substituteId; + } + } + set + { + _substituteId = value; + } + } + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RoleCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RoleCollection.cs new file mode 100644 index 0000000..a992a44 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/RoleCollection.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of Role objects. + /// + public class RoleCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Role object if found. Null if not found. + public Role FindByName(string name) + { + foreach (Role role in this) + { + if (role.Name == name) + { + return role; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Role role in this) + { + if (role.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Role object if found. Null if not found. + public Role FindById(string id) + { + foreach (Role role in this) + { + if (role.Id == id) + { + return role; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Role role in this) + { + if (role.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Role role in this) + { + if (role.Id == id) + { + this.Remove(role); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Table.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Table.cs new file mode 100644 index 0000000..52332e4 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/Table.cs @@ -0,0 +1,782 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using Microsoft.AnalysisServices; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model table with properties and methods for comparison purposes. + /// + public class Table : ITabularObject + { + #region Private Members + + private TabularModel _parentTabularModel; + private Dimension _amoDimension; + private DataTable _amoTable; + private CubeDimension _amoCubeDimension; + private MeasureGroup _amoMeasureGroup; + private RelationshipCollection _relationships = new RelationshipCollection(); + private string _datasourceId; + private string _objectDefinition; + private string _substituteId; + private Dimension _amoDimensionBackup = null; + private const int _spacing = 31; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the Table class using multiple parameters. + /// + /// TabularModel object that the Table object belongs to. + /// Analysis Management Objects Dimension object abtstracted by the Table class. + public Table(TabularModel parentTabularModel, Dimension dimension) + { + _parentTabularModel = parentTabularModel; + _amoDimension = dimension; + + PopulateProperties(); + } + + #endregion + + #region Public Properties + + /// + /// TabularModel object that the Table object belongs to. + /// + public TabularModel TabularModel => _parentTabularModel; + + /// + /// Name of the Table object. + /// + public string Name => _amoDimension.Name; + + /// + /// Long name of the Table object. + /// + public string LongName => _amoDimension.Name; + + /// + /// Id of the Table object. + /// + public string Id => _amoDimension.ID; + + /// + /// Data source id of the Table object. + /// + public string DataSourceID => _datasourceId; + + /// + /// Object definition of the Table object. This is a simplified list of relevant attribute values for comparison; not the XMLA definition of the abstracted AMO object. + /// + public string ObjectDefinition => _objectDefinition; + + /// + /// Collection of relationships for the Table object. + /// + public RelationshipCollection Relationships => _relationships; + + /// + /// DataTable object abtstracted by the Table class. + /// + public DataTable AmoTable => _amoTable; + + /// + /// Analysis Management Objects CubeDimension object abtstracted by the Table class. + /// + public CubeDimension AmoCubeDimension => _amoCubeDimension; + + /// + /// Analysis Management Objects MeasureGroup object abtstracted by the Table class. + /// + public MeasureGroup AmoMeasureGroup => _amoMeasureGroup; + + /// + /// Analysis Management Objects Dimension object abtstracted by the Table class. + /// + public Dimension AmoDimension => _amoDimension; + + /// + /// Backed up version of the AMO Dimension object. Used for when updating a table. + /// + public Dimension AmoOldDimensionBackup + { + get + { + return _amoDimensionBackup; + } + set + { + _amoDimensionBackup = value; + } + } + + /// + /// Substitute Id of the Table object. + /// + public string SubstituteId + { + get + { + if (string.IsNullOrEmpty(_substituteId)) + { + return _amoDimension.ID; + } + else + { + return _substituteId; + } + } + set + { + _substituteId = value; + } + } + + #endregion + + private void PopulateProperties() + { + // find the datasourceid for the Table - and also the datasourceid + foreach (DataSourceView dsv in _parentTabularModel.AmoDatabase.DataSourceViews) + { + _datasourceId = dsv.DataSourceID; + foreach (DataTable tbl in dsv.Schema.Tables) + { + if ((tbl.TableName == _amoDimension.ID) || + (_amoDimension.Source is DsvTableBinding && tbl.TableName == ((DsvTableBinding)_amoDimension.Source).TableID) + ) + { + _amoTable = tbl; + + //_tableInDsv = tbl; + if (tbl.ExtendedProperties["DataSourceID"] != null) + { + _datasourceId = tbl.ExtendedProperties["DataSourceID"].ToString(); + break; + } + } + } + } + + foreach (CubeDimension cd in _parentTabularModel.AmoDatabase.Cubes[0].Dimensions) + { + if (cd.ID == _amoDimension.ID) + { + _amoCubeDimension = cd; + break; + } + } + + foreach (MeasureGroup mg in _parentTabularModel.AmoDatabase.Cubes[0].MeasureGroups) + { + if (mg.ID == _amoDimension.ID) + { + _amoMeasureGroup = mg; + break; + } + } + + string baseColumns = ""; + string calculatedColumns = ""; + foreach (DimensionAttribute attribute in _amoDimension.Attributes) + { + // ignore key attribute - which is internal built in "RowNumber" + //if (attribute.ID != _amoDimension.KeyAttribute.ID) + if (attribute.ID != "__XL_RowNumber") //if (attribute.ID != "RowNumber") //Before SQL 2016, was just "RowNumber" + { + // if calculated column, show expression + if (attribute.NameColumn.Source is ExpressionBinding) + { + string expression = ((ExpressionBinding)attribute.NameColumn.Source).Expression; + calculatedColumns += "[" + attribute.Name + "]:=" + expression + "; " + SetColumnFormatAndVisibility(attribute) + "\n"; + } + else + { + string baseColumn = "[" + attribute.Name + "]"; + + /* DATA TYPE MAPPING + Text: WChar + Whole Number: BigInt + Decimal Number: Double + True/False: Boolean + Currency: Currency + Date: Date + Binary: Binary + */ + // insert spaces to line up data types nicely + if (baseColumn.Length < _spacing) + { + baseColumn += new String(' ', _spacing - baseColumn.Length); + } + + switch (attribute.KeyColumns[0].DataType) + { + case System.Data.OleDb.OleDbType.WChar: + baseColumn += " Data Type: Text, "; + break; + case System.Data.OleDb.OleDbType.BigInt: + case System.Data.OleDb.OleDbType.Integer: + case System.Data.OleDb.OleDbType.SmallInt: + baseColumn += " Data Type: Whole Number, "; + break; + case System.Data.OleDb.OleDbType.Double: + baseColumn += " Data Type: Decimal Number, "; + break; + case System.Data.OleDb.OleDbType.Boolean: + baseColumn += " Data Type: True/False, "; + break; + case System.Data.OleDb.OleDbType.Currency: + baseColumn += " Data Type: Currency, "; + break; + case System.Data.OleDb.OleDbType.Date: + baseColumn += " Data Type: Date, "; + break; + case System.Data.OleDb.OleDbType.Binary: + baseColumn += " Data Type: Binary, "; + break; + default: + break; + } + + // Format & visibility + baseColumn += SetColumnFormatAndVisibility(attribute); + + baseColumns += baseColumn + "\n"; + } + } + } + + _objectDefinition += "Base Columns:\n" + baseColumns + "\n"; + _objectDefinition += "Calculated Columns:\n" + calculatedColumns + "\n"; + + _objectDefinition += "Hierarchies:\n"; + if (_amoDimension.Hierarchies.Count == 0) + { + _objectDefinition += "\n"; + } + else + { + foreach (Hierarchy hierarchy in _amoDimension.Hierarchies) + { + _objectDefinition += "[" + hierarchy.Name + "] "; + //if ((_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders || _parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) && hierarchy.Name.Length + 2 < _spacing) + //{ + // _objectDefinition += new String(' ', _spacing - hierarchy.Name.Length - 2); + //} + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + //{ + // _objectDefinition += "Display Folder: "; + // if (hierarchy.DisplayFolder != null) + // { + // _objectDefinition += hierarchy.DisplayFolder; + // } + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) _objectDefinition += ", "; + //} + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += "Hierarchy Translations: "; + // if (hierarchy.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation hierarchyTranslation in hierarchy.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(hierarchyTranslation.Language).DisplayName + ": " + hierarchyTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (hierarchy.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation hierarchyTranslation in hierarchy.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(hierarchyTranslation.Language).DisplayName + ": " + hierarchyTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + //} + + _objectDefinition += "\nLevels:\n"; + foreach (Level level in hierarchy.Levels) + { + _objectDefinition += " [" + level.Name + "]"; + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations && level.Name.Length + 4 < _spacing) + //{ + // _objectDefinition += new String(' ', _spacing - level.Name.Length - 4); + //} + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += "Level Translations: "; + // if (level.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation levelTranslation in level.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(levelTranslation.Language).DisplayName + ": " + levelTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (level.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation levelTranslation in level.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(levelTranslation.Language).DisplayName + ": " + levelTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + //} + _objectDefinition += "\n"; + } + _objectDefinition += "\n"; + } + } + + if (_amoCubeDimension != null) + { + _objectDefinition += "Format & Visibility:\nHidden:" + (!_amoCubeDimension.Visible).ToString(); + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // _objectDefinition += ", Table Translations: "; + // if (_amoCubeDimension.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation tableTranslation in _amoCubeDimension.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(tableTranslation.Language).DisplayName + ": " + tableTranslation.Caption + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // _objectDefinition += ", Display Folder Translations: "; + // if (_amoCubeDimension.Translations.Count > 0) + // { + // _objectDefinition += "["; + // foreach (Translation tableDisplayFolderTranslation in _amoCubeDimension.Translations) + // { + // _objectDefinition += CultureInfo.GetCultureInfo(tableDisplayFolderTranslation.Language).DisplayName + ": " + tableDisplayFolderTranslation.DisplayFolder + ", "; + // } + // _objectDefinition = _objectDefinition.Substring(0, _objectDefinition.Length - 2) + "]"; + // } + // } + //} + + _objectDefinition += "\n"; + } + + if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionPartitions && _amoMeasureGroup != null) + { + _objectDefinition += "\nPartitions:\n"; + + List partitionNames = new List(); // put in here to sort + foreach (Partition partition in _amoMeasureGroup.Partitions) + { + partitionNames.Add(partition.Name); + } + partitionNames.Sort(); + + foreach (string partitionName in partitionNames) + { + foreach (Partition partition in _amoMeasureGroup.Partitions) + { + if (partition.Name == partitionName) + { + _objectDefinition += "Name: [" + partition.Name + "]\nSQL:\n" + ((QueryBinding)partition.Source).QueryDefinition + "\n"; + break; + } + } + } + } + + foreach (Microsoft.AnalysisServices.Relationship relationship in _amoDimension.Relationships) + { + _relationships.Add(new Relationship(this, relationship)); + } + } + + private string SetColumnFormatAndVisibility(DimensionAttribute attribute) + { + string columnDef = ""; + + if (attribute != null && attribute.Annotations != null && attribute.Annotations.Contains("Format") && attribute.Annotations["Format"].Value.Attributes["Format"] != null) + { + switch (attribute.Annotations["Format"].Value.Attributes["Format"].Value) + { + case "General": + columnDef += "Data Format: General"; + break; + case "NumberDecimal": + columnDef += "Data Format: Decimal Number" + + (attribute.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + attribute.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "NumberWhole": + columnDef += "Data Format: Whole Number" + + (attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "Percentage": + columnDef += "Data Format: Percentage" + + (attribute.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + attribute.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"] != null ? ", Show Thousand Separator: " + attribute.Annotations["Format"].Value.Attributes["ThousandSeparator"].Value : ""); + break; + case "Scientific": + columnDef += "Data Format: Scientific" + + (attribute.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + attribute.Annotations["Format"].Value.Attributes["Accuracy"].Value : ""); + break; + case "Currency": + columnDef += "Data Format: Currency" + + (attribute.Annotations["Format"].Value.Attributes["Accuracy"] != null ? ", Decimal Places: " + attribute.Annotations["Format"].Value.Attributes["Accuracy"].Value : "") + + (attribute.Annotations["Format"].Value.HasChildNodes && + attribute.Annotations["Format"].Value.ChildNodes[0].Attributes["DisplayName"] != null + ? ", Currency Symbol: " + attribute.Annotations["Format"].Value.ChildNodes[0].Attributes["DisplayName"].Value : ""); + break; + case "DateTimeCustom": + columnDef += "Data Format: Date" + + (attribute.Annotations["Format"].Value.HasChildNodes && + attribute.Annotations["Format"].Value.ChildNodes[0].HasChildNodes && + attribute.Annotations["Format"].Value.ChildNodes[0].ChildNodes[0].Attributes["FormatString"] != null + ? ", Date Format: " + attribute.Annotations["Format"].Value.ChildNodes[0].ChildNodes[0].Attributes["FormatString"].Value : ""); + break; + case "DateTimeGeneral": + columnDef += "Data Format: General"; + break; + case "Text": + columnDef += "Data Format: Text"; + break; + case "Boolean": + columnDef += "Data Format: TRUE/FALSE"; + break; + default: + break; + } + } + else + { + // Sometimes annotations are not populated, so just show the default formats (which are text/general) + switch (attribute.KeyColumns[0].DataType) + { + case System.Data.OleDb.OleDbType.WChar: + columnDef += "Data Format: Text"; + break; + case System.Data.OleDb.OleDbType.BigInt: + case System.Data.OleDb.OleDbType.Integer: + case System.Data.OleDb.OleDbType.SmallInt: + case System.Data.OleDb.OleDbType.Double: + case System.Data.OleDb.OleDbType.Currency: + case System.Data.OleDb.OleDbType.Date: + case System.Data.OleDb.OleDbType.Binary: + columnDef += "Data Format: General"; + break; + case System.Data.OleDb.OleDbType.Boolean: + columnDef += "Data Format: TRUE/FALSE"; + break; + default: + break; + } + } + + columnDef += ", Hidden: " + (!attribute.AttributeHierarchyVisible).ToString(); + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + //{ + // columnDef += ", Display Folder: "; + // if (attribute.AttributeHierarchyDisplayFolder != null) + // { + // columnDef += attribute.AttributeHierarchyDisplayFolder; + // } + //} + + //if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionTranslations) + //{ + // columnDef += ", Column Translations: "; + // if (attribute.Translations.Count > 0) + // { + // columnDef += "["; + // foreach (AttributeTranslation attributeTranslation in attribute.Translations) + // { + // columnDef += CultureInfo.GetCultureInfo(attributeTranslation.Language).DisplayName + ": " + attributeTranslation.Caption + ", "; + // } + // columnDef = columnDef.Substring(0, columnDef.Length - 2) + "]"; + // } + + // if (_parentTabularModel.ComparisonInfo.OptionsInfo.OptionDisplayFolders) + // { + // columnDef += ", Display Folder Translations: "; + // if (attribute.Translations.Count > 0) + // { + // columnDef += "["; + // foreach (AttributeTranslation attributeTranslation in attribute.Translations) + // { + // columnDef += CultureInfo.GetCultureInfo(attributeTranslation.Language).DisplayName + ": " + attributeTranslation.DisplayFolder + ", "; + // } + // columnDef = columnDef.Substring(0, columnDef.Length - 2) + "]"; + // } + // } + //} + + return columnDef; + } + + #region Actions: Relationships + + /// + /// Delete relationship associated with the Table object. + /// + /// The Id of the relationship to be deleted. + public void DeleteRelationship(string relationshipId) + { + if (_amoDimension.Relationships.Contains(relationshipId)) + { + _amoDimension.Relationships.Remove(relationshipId); + } + + // shell model + if (_relationships.ContainsId(relationshipId)) + { + _relationships.RemoveById(relationshipId); + } + } + + /// + /// Clear all relationships for the Table object. + /// + public void ClearRelationships() + { + _amoDimension.Relationships.Clear(); + + // shell model + _relationships.Clear(); + } + + /// + /// Create a relationship for the Table object. + /// + /// + /// + /// + /// + /// + public bool CreateRelationship(Relationship relationshipSource, Dimension parentDimSource, string relationshipName, ref string warningMessage) + => this.CreateRelationship(relationshipSource.AmoRelationship, parentDimSource, relationshipName, ref warningMessage, relationshipSource.IsActive); + + /// + /// Create a relationship for the Table object. + /// + /// + /// + /// + /// + /// + /// + public bool CreateRelationship(Microsoft.AnalysisServices.Relationship amoRelationshipSource, Dimension parentDimSource, string relationshipName, ref string warningMessage, bool active) + { + // Do the required tables exist? + + // Child table is guaranteed to exist - we are in an instance of it. Just need to ensure the FromRelationshipEnd.DimensionID is correct + amoRelationshipSource.FromRelationshipEnd.DimensionID = _amoDimension.ID; + + // Parent table need to check ... + if (_parentTabularModel.Tables.ContainsName(parentDimSource.Name)) + { + //plug in the substitute id for the parent and move on with my life (I have a life you know) + amoRelationshipSource.ToRelationshipEnd.DimensionID = _parentTabularModel.Tables.FindByName(parentDimSource.Name).Id; + } + else + { + warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) parent table not found in target model."; + return false; + } + + Dimension childDimSource = (Dimension)amoRelationshipSource.Parent; + Dimension childDimTarget = _amoDimension; + //Dimension parentDimSource = ... //had to pass in as parameter + Dimension parentDimTarget = _parentTabularModel.Tables.FindById(amoRelationshipSource.ToRelationshipEnd.DimensionID).AmoDimension; + + // do the required columns exist? + Microsoft.AnalysisServices.Relationship amoRelationshipSourceTemp = null; // can't modify attribute values while in dim collection, so (might) need a temporary holder + + foreach (RelationshipEndAttribute childDimAttributeSource in amoRelationshipSource.FromRelationshipEnd.Attributes) + { + DimensionAttribute childDimAttributeTarget = childDimTarget.Attributes.FindByName(childDimSource.Attributes[childDimAttributeSource.AttributeID].Name); + if (childDimAttributeTarget == null) + { + warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) child column not found in target model."; + return false; + } + + // just in case the attribute ids are not the same between source/target (though obviously the names are the same), then set the correct id (below) in the source relationship and everything will just work + if (childDimAttributeSource.AttributeID != childDimAttributeTarget.ID) + { + amoRelationshipSourceTemp = amoRelationshipSource.Clone(); + RelationshipEndAttribute childDimAttributeSourceTemp = childDimAttributeSource.Clone(); + childDimAttributeSourceTemp.AttributeID = childDimAttributeTarget.ID; + amoRelationshipSourceTemp.FromRelationshipEnd.Attributes.Remove(childDimAttributeSource.AttributeID); + amoRelationshipSourceTemp.FromRelationshipEnd.Attributes.Add(childDimAttributeSourceTemp); + } + } + + // now check parent columns + foreach (RelationshipEndAttribute parentDimAttributeSource in amoRelationshipSource.ToRelationshipEnd.Attributes) + { + if (!parentDimSource.Attributes.Contains(parentDimAttributeSource.AttributeID)) + { + warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) parent column not found in target model."; + return false; + } + + DimensionAttribute parentDimAttributeTarget = parentDimTarget.Attributes.FindByName(parentDimSource.Attributes[parentDimAttributeSource.AttributeID].Name); + if (parentDimAttributeTarget == null) + { + warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) parent column not found in target model."; + return false; + } + + ////does the parent column allow non-unique values? (if so, won't work as a parent in the relationship) + //if (!( parentDimTarget.Attributes.Contains("RowNumber") && + // parentDimTarget.Attributes["RowNumber"].AttributeRelationships.Contains(parentDimAttributeTarget.ID) && + // parentDimTarget.Attributes["RowNumber"].AttributeRelationships[parentDimAttributeTarget.ID].Cardinality == Cardinality.One )) + //{ + // warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) parent column allows non-unique values."; + // return false; + //} + + // just in case the attribute ids are not the same between source/target (though obviously the names are the same), then set the correct id (below) in the source relationship and everything will just work + if (parentDimAttributeSource.AttributeID != parentDimAttributeTarget.ID) + { + if (amoRelationshipSourceTemp == null) + { + amoRelationshipSourceTemp = amoRelationshipSource.Clone(); + } + RelationshipEndAttribute parentDimAttributeSourceTemp = parentDimAttributeSource.Clone(); + parentDimAttributeSourceTemp.AttributeID = parentDimAttributeTarget.ID; + amoRelationshipSourceTemp.ToRelationshipEnd.Attributes.Remove(parentDimAttributeSource.AttributeID); + amoRelationshipSourceTemp.ToRelationshipEnd.Attributes.Add(parentDimAttributeSourceTemp); + } + } + + if (amoRelationshipSourceTemp != null) //i.e. we had to replace at least one attribute id + { + childDimSource.Relationships.Remove(amoRelationshipSource.ID); + childDimSource.Relationships.Add(amoRelationshipSourceTemp); + amoRelationshipSource = amoRelationshipSourceTemp; + } + + // is there already a relationship with the same tables/columns? + bool foundMatch = false; + foreach (Relationship relationshipTarget in _relationships) + { + // has same parent table? + if (relationshipTarget.AmoRelationship.ToRelationshipEnd.DimensionID == amoRelationshipSource.ToRelationshipEnd.DimensionID) + { + // check columns + bool columnsMatch = true; + + foreach (RelationshipEndAttribute attribute in amoRelationshipSource.FromRelationshipEnd.Attributes) + { + if (!relationshipTarget.AmoRelationship.FromRelationshipEnd.Attributes.Contains(attribute.AttributeID)) + { + columnsMatch = false; + } + } + foreach (RelationshipEndAttribute attribute in amoRelationshipSource.ToRelationshipEnd.Attributes) + { + if (!relationshipTarget.AmoRelationship.ToRelationshipEnd.Attributes.Contains(attribute.AttributeID)) + { + columnsMatch = false; + } + } + + if (columnsMatch) foundMatch = true; + } + } + if (foundMatch) + { + warningMessage = "Unable to create Relationship " + relationshipName + " because (considering changes) relationship already exists in target model."; + return false; + } + + // at this point we know we will add the relationship, but need to check that parent column only allows unique values. If not, change it. + foreach (RelationshipEndAttribute parentDimAttributeSource in amoRelationshipSource.ToRelationshipEnd.Attributes) + { + DimensionAttribute parentDimAttributeTarget = parentDimTarget.Attributes.FindByName(parentDimSource.Attributes[parentDimAttributeSource.AttributeID].Name); + //(already checked for existence of parentDimAttributeTarget above) + if (parentDimTarget.Attributes.Contains("RowNumber") && + parentDimTarget.Attributes["RowNumber"].AttributeRelationships.Contains(parentDimAttributeTarget.ID) && + parentDimTarget.Attributes["RowNumber"].AttributeRelationships[parentDimAttributeTarget.ID].Cardinality != Cardinality.One) + { + parentDimTarget.Attributes["RowNumber"].AttributeRelationships[parentDimAttributeTarget.ID].Cardinality = Cardinality.One; + foreach (DataItem di in parentDimAttributeTarget.KeyColumns) + { + di.NullProcessing = NullProcessing.Error; + } + if (_parentTabularModel.AmoDatabase.Cubes.Count > 0) + { + foreach (MeasureGroup mg in _parentTabularModel.AmoDatabase.Cubes[0].MeasureGroups) + { + if (mg.ID == parentDimTarget.ID) + { + foreach (MeasureGroupDimension mgd in mg.Dimensions) + { + if (mgd.CubeDimensionID == parentDimTarget.ID && mgd is DegenerateMeasureGroupDimension) + { + foreach (MeasureGroupAttribute mga in ((DegenerateMeasureGroupDimension)mgd).Attributes) + { + if (mga.AttributeID == parentDimAttributeTarget.ID) + { + mga.KeyColumns[0].NullProcessing = NullProcessing.Error; + } + } + } + } + } + } + } + } + } + + // at this point we know we will add the relationship + Microsoft.AnalysisServices.Relationship relationshipClone = amoRelationshipSource.Clone(); + + // but first check if there is an existing relationship with same id + if (_parentTabularModel.ContainsRelationship(relationshipClone.ID)) + { + //Id already exists, but still need to add because different definition - this is due to clever clog users changing table names that were originially in both source and target + string oldRelationshipId = relationshipClone.ID; + relationshipClone.ID = Convert.ToString(Guid.NewGuid()); + } + + if (active && !_parentTabularModel.ActiveRelationshipIds.Contains(relationshipClone.ID)) + { + _parentTabularModel.ActiveRelationshipIds.Add(relationshipClone.ID); + } + _amoDimension.Relationships.Add(relationshipClone); + _relationships.Add(new Relationship(this, relationshipClone, copiedFromSource: true)); + + return true; + } + + #endregion + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TableCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TableCollection.cs new file mode 100644 index 0000000..763d5d3 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TableCollection.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Represents a collection of DataSource objects. + /// + public class TableCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// DataSource object if found. Null if not found. + public Table FindByName(string name) + { + foreach (Table table in this) + { + if (table.Name == name) + { + return table; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Table table in this) + { + if (table.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// DataSource object if found. Null if not found. + public Table FindById(string id) + { + foreach (Table table in this) + { + if (table.Id == id) + { + return table; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (Table table in this) + { + if (table.Id == id) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Table objects filtered by the parent datasource's Id. + /// + /// + /// TableCollection + public TableCollection FilterByDataSourceId(string dataSourceId) + { + TableCollection returnTables = new TableCollection(); + foreach (Table table in this) + { + if (table.DataSourceID == dataSourceId) + { + returnTables.Add(table); + } + } + return returnTables; + } + + /// + /// Removes an object from the collection by its Id. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveById(string id) + { + foreach (Table table in this) + { + if (table.Id == id) + { + this.Remove(table); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TabularModel.cs b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TabularModel.cs new file mode 100644 index 0000000..e957d4f --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/MultidimensionalMetadata/TabularModel.cs @@ -0,0 +1,2935 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using System.IO; +using System.Xml; +using System.Threading; +using System.Security.Principal; +using Microsoft.AnalysisServices; +using EnvDTE; +using System.Windows.Forms; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare.MultidimensionalMetadata +{ + /// + /// Abstraction of a tabular model table with properties and methods for comparison purposes. This class can represent a database on a server, or a project in Visual Studio. + /// + public class TabularModel : IDisposable + { + #region Private Members + + private Comparison _parentComparison; + private ConnectionInfo _connectionInfo; + private ComparisonInfo _comparisonInfo; + private Server _amoServer; + private Database _amoDatabase; + private DataSourceCollection _dataSources = new DataSourceCollection(); + private TableCollection _tables = new TableCollection(); + private MeasureCollection _measures = new MeasureCollection(); + private KpiCollection _kpis = new KpiCollection(); + private PerspectiveCollection _perspectives = new PerspectiveCollection(); + private RoleCollection _roles = new RoleCollection(); + private ActionCollection _actions = new ActionCollection(); + private List _activeRelationshipIds = new List(); + private bool _disposed = false; + + #endregion + + /// + /// Initializes a new instance of the TabularModel class using multiple parameters. + /// + /// Comparison object that the tabular model belongs to. + /// ConnectionInfo object for the tabular model. + /// ComparisonInfo object for the tabular model. + public TabularModel(Comparison parentComparison, ConnectionInfo connectionInfo, ComparisonInfo comparisonInfo) + { + _parentComparison = parentComparison; + _connectionInfo = connectionInfo; + _comparisonInfo = comparisonInfo; + } + + /// + /// Connect to SSAS server and instantiate properties of the TabularModel object. + /// + public void Connect() + { + this.Disconnect(); + + _amoServer = new Server(); + _amoServer.Connect("Provider=MSOLAP;Data Source=" + _connectionInfo.ServerName); + + _amoDatabase = _amoServer.Databases.FindByName(_connectionInfo.DatabaseName); + if (_amoDatabase == null) + { + //We don't need try to load from project here as will already be done before instantiated Comparison + throw new Microsoft.AnalysisServices.ConnectionException("Could not connect to database " + _connectionInfo.DatabaseName); + } + + //direct query check + if (_amoDatabase.DirectQueryMode == DirectQueryMode.DirectQuery || _amoDatabase.DirectQueryMode == DirectQueryMode.InMemoryWithDirectQuery || _amoDatabase.DirectQueryMode == DirectQueryMode.DirectQueryWithInMemory) + { + throw new ConnectionException((_connectionInfo.UseProject ? "Project " + _connectionInfo.ProjectName : "Database " + _amoDatabase.Name) + " has DirectQuery Mode property set to " + Convert.ToString(_amoDatabase.DirectQueryMode) + ", which is not supported for Compatibility Level " + Convert.ToString(_connectionInfo.CompatibilityLevel) + "."); + } + + // shell model + foreach (Microsoft.AnalysisServices.DataSource datasource in _amoDatabase.DataSources) + { + _dataSources.Add(new DataSource(this, datasource)); + } + foreach (Dimension dimension in _amoDatabase.Dimensions) + { + _tables.Add(new Table(this, dimension)); + } + foreach (Microsoft.AnalysisServices.Role role in _amoDatabase.Roles) + { + _roles.Add(new Role(this, role)); + } + if (_amoDatabase.Cubes.Count > 0) + { + foreach (Microsoft.AnalysisServices.Action action in _amoDatabase.Cubes[0].Actions) + { + _actions.Add(new Action(this, action)); + } + } + PopulateMeasures(); + //need to populate perspectives after measures because obj def refers to measures collection + if (_amoDatabase.Cubes.Count > 0) + { + foreach (Microsoft.AnalysisServices.Perspective perspective in _amoDatabase.Cubes[0].Perspectives) + { + _perspectives.Add(new Perspective(this, perspective)); + } + } + } + + /// + /// Disconnect from the SSAS server. + /// + public void Disconnect() + { + if (_amoServer != null) _amoServer.Disconnect(); + } + + #region Private Methods + + private struct ParsedCommand + { + public ParsedCommand(IEnumerable expressions, string fullName, string table) + { + Expressions = new List(); + Expressions.AddRange(expressions); + FullName = fullName; + Table = table; + } + public List Expressions; + public string FullName; + public string Table; + } + private void PopulateMeasures() + { + if (_amoDatabase.Cubes.Count > 0) + { + MdxScript mdxScript = _amoDatabase.Cubes[0].MdxScripts["MdxScript"]; + if (mdxScript.Commands.Count > 0) + { + List parsedCommands = new List(); + + //Review all commands in MdxScripts and extract all MDX expressions found there + for (int i = 0; i < mdxScript.Commands.Count; i++) + { + if (mdxScript.Commands[i].Text != null) + { + List expressions = new List(); + expressions.AddRange(ParseMdxScript(mdxScript.Commands[i].Text)); + if (expressions.Count > 0) + { + string fullName = ""; + if (mdxScript.Commands[i].Annotations.Contains("FullName")) fullName = mdxScript.Commands[i].Annotations["FullName"].Value.InnerText; + ////yet another microsoft fudge + //if (fullName.Length > 1 && fullName.Substring(fullName.Length - 2, 2) == "]]") fullName = fullName.Substring(0, fullName.Length - 1); + string table = ""; + if (mdxScript.Commands[i].Annotations.Contains("Table")) table = mdxScript.Commands[i].Annotations["Table"].Value.InnerText; + parsedCommands.Add(new ParsedCommand(expressions, fullName, table)); + } + } + } + + MeasureCollection kpiReferenceMeasures = new MeasureCollection(); + //KPIs declared in MDX script + List kpisDeclaredInScript = new List(); + + if (_amoDatabase.CompatibilityLevel < 1103) + { + //This block only applies pre SP1 + //Populate the KPI reference measures first - they start with "CREATE MEMBER CURRENTCUBE.Measures" (and use MDX) rather than "CREATE MEASURE" + + foreach (ParsedCommand parsedCommand in parsedCommands) + { + foreach (string statement in parsedCommand.Expressions) + { + if ((statement.Length >= 35) && (statement.Substring(0, 35) == "CREATE MEMBER CURRENTCUBE.Measures.")) + { + //Find table name/measure/expression + int openSquareBracketPosition = statement.IndexOf('[', 0); + int closeSquareBracketPosition = statement.IndexOf(']', openSquareBracketPosition + 1); + string statementMeasureName = statement.Substring(openSquareBracketPosition + 1, closeSquareBracketPosition - openSquareBracketPosition - 1); + + int openExpressionQuotePosition = statement.IndexOf('\'', 0); + if (openExpressionQuotePosition != -1) + { + int closeExpressionQuotePosition = statement.IndexOf('\'', openExpressionQuotePosition + 1); + string statementMeasureExpression = statement.Substring(openExpressionQuotePosition + 1, closeExpressionQuotePosition - openExpressionQuotePosition - 1); + + int associatedMeasureGroupEndPosition = statement.IndexOf("ASSOCIATED_MEASURE_GROUP", closeExpressionQuotePosition) + 24; + int openTableQuotePosition = statement.IndexOf('\'', associatedMeasureGroupEndPosition + 1); + int closeTableQuotePosition = statement.IndexOf('\'', openTableQuotePosition + 1); + string statementTableName = statement.Substring(openTableQuotePosition + 1, closeTableQuotePosition - openTableQuotePosition - 1); + + kpiReferenceMeasures.Add(new Measure(this, statementTableName, statementMeasureName, statementMeasureExpression)); + } + } + } + } + } + + /* KPIs can be created in 2 different ways: either declared in the MDX script as "CREATE KPI" (in which case won't be populated + in _amoDatabase.Cubes[0].Kpis), or using the object model (in which case it will be in the AMO KPI collection). + -PRE -SP1 CAN BE EITHER! + -POST-SP1 ALWAYS IN SCRIPT + So we need to check both. + BUT WAIT, THERE'S MORE ... if created in script pre-sp1 will be MDX syntax, if created in script post sp1, will be dax syntax + AND ... post-sp1 replaces ']' characters with ']]' + Thank you Microsoft. You are the best. + */ + + foreach (ParsedCommand parsedCommand in parsedCommands) + { + foreach (string statement in parsedCommand.Expressions) + { + if ((statement.Length >= 23) && (statement.Substring(0, 23) == "CREATE KPI CURRENTCUBE.")) + { + int lastCharacterPosition = 0; + string kpiName; + string kpiExpression; + string goalName; + string goalExpression; + string statusName; + string statusExpression; + string trendName; + string trendExpression; + + if (_amoDatabase.CompatibilityLevel < 1103) + { + int openSquareBracketPosition = statement.IndexOf('[', 0); + int closeSquareBracketPosition = statement.IndexOf(']', openSquareBracketPosition + 1); + kpiName = statement.Substring(openSquareBracketPosition + 1, closeSquareBracketPosition - openSquareBracketPosition - 1); + + int goalEndPosition = statement.IndexOf("GOAL", closeSquareBracketPosition) + "GOAL".Length; + int goalOpenPosition = statement.IndexOf("Measures.[", goalEndPosition + 1) + "Measures.[".Length; + int goalClosePosition = statement.IndexOf("]", goalOpenPosition) - 1; + goalName = statement.Substring(goalOpenPosition, goalClosePosition - goalOpenPosition + 1); + + int statusEndPosition = statement.IndexOf("STATUS", closeSquareBracketPosition) + "STATUS".Length; + int statusOpenPosition = statement.IndexOf("Measures.[", statusEndPosition + 1) + "Measures.[".Length; + int statusClosePosition = statement.IndexOf("]", statusOpenPosition) - 1; + statusName = statement.Substring(statusOpenPosition, statusClosePosition - statusOpenPosition + 1); + + int trendEndPosition = statement.IndexOf("TREND", closeSquareBracketPosition) + "TREND".Length; + int trendOpenPosition = statement.IndexOf("Measures.[", trendEndPosition + 1) + "Measures.[".Length; + int trendClosePosition = statement.IndexOf("]", trendOpenPosition) - 1; + trendName = statement.Substring(trendOpenPosition, trendClosePosition - trendOpenPosition + 1); + } + else + { + ParseMeasureAndExpression(parsedCommand.FullName, statement, out kpiName, out kpiExpression, ref lastCharacterPosition); + + lastCharacterPosition = statement.IndexOf("GOAL", lastCharacterPosition) + "GOAL".Length; + ParseMeasureAndExpression(parsedCommand.FullName, statement, out goalName, out goalExpression, ref lastCharacterPosition); + + lastCharacterPosition = statement.IndexOf("STATUS", lastCharacterPosition) + "STATUS".Length; + ParseMeasureAndExpression(parsedCommand.FullName, statement, out statusName, out statusExpression, ref lastCharacterPosition); + + lastCharacterPosition = statement.IndexOf("TREND", lastCharacterPosition) + "TREND".Length; + ParseMeasureAndExpression(parsedCommand.FullName, statement, out trendName, out trendExpression, ref lastCharacterPosition); + } + + int statusGraphicEndPosition = statement.IndexOf("STATUS_GRAPHIC", lastCharacterPosition) + "STATUS_GRAPHIC".Length; + int statusGraphicOpenQuotePosition = statement.IndexOf('\'', statusGraphicEndPosition + 1); + int statusGraphicCloseQuotePosition = statement.IndexOf('\'', statusGraphicOpenQuotePosition + 1); + string mdxStmtStatusGraphic = statement.Substring(statusGraphicOpenQuotePosition + 1, statusGraphicCloseQuotePosition - statusGraphicOpenQuotePosition - 1); + + int trendGraphicEndPosition = statement.IndexOf("TREND_GRAPHIC", lastCharacterPosition) + "TREND_GRAPHIC".Length; + int trendGraphicOpenQuotePosition = statement.IndexOf('\'', trendGraphicEndPosition + 1); + int trendGraphicCloseQuotePosition = statement.IndexOf('\'', trendGraphicOpenQuotePosition + 1); + string mdxStmtTrendGraphic = statement.Substring(trendGraphicOpenQuotePosition + 1, trendGraphicCloseQuotePosition - trendGraphicOpenQuotePosition - 1); + + //Kpi kpiDeclaredInScript = new Kpi(mdxStmtKpiName, mdxStmtKpiName); //ok to use a guid as id because this is just a temporary store of the KPI in the kpisDeclaredInScript variable. It is referred to below without using the id/amo instance. This resolves issue where special characters are in the name (e.g. (, %, etc), which causes error if in the ID. + Microsoft.AnalysisServices.Kpi kpiDeclaredInScript = new Microsoft.AnalysisServices.Kpi(kpiName, Convert.ToString(Guid.NewGuid())); + kpiDeclaredInScript.Goal = goalName; + kpiDeclaredInScript.Status = statusName; + kpiDeclaredInScript.Trend = trendName; + kpiDeclaredInScript.StatusGraphic = mdxStmtStatusGraphic; + kpiDeclaredInScript.TrendGraphic = mdxStmtTrendGraphic; + kpisDeclaredInScript.Add(kpiDeclaredInScript); + } + } + } + + //Note: here we are making the assumption that Measures are created in a FIXED way + // This routine will only find measures that have been created by following fixed pattern + // string.Format("CREATE MEASURE '{0}'[{1}]={2};", cmTableName, cmName, newCalculatedMeasureExpression.Text) + _measures.Clear(); + _kpis.Clear(); + + foreach (ParsedCommand parsedCommand in parsedCommands) + { + foreach (string statement in parsedCommand.Expressions) + { + if ((statement.Length >= 14) && (statement.Substring(0, 14) == "CREATE MEASURE")) + { + //Find table name/measure/expression + string statementTableName = ""; + if (parsedCommand.Table == "") //post SP1, there should always be a table/fullname and one single statement, so this check SHOULD be redundant + { + int openQuotePosition = statement.IndexOf('\'', 0); + int closeQuotePosition = statement.IndexOf('\'', openQuotePosition + 1); + statementTableName = statement.Substring(openQuotePosition + 1, closeQuotePosition - openQuotePosition - 1); + } + else + { + statementTableName = parsedCommand.Table; + } + + //int closeSquareBracketPosition = statement.IndexOf(']', openSquareBracketPosition + 1); + int lastCharacterPosition = 0; + string statementMeasureName; + string statementMeasureExpression; + ParseMeasureAndExpression(parsedCommand.FullName, statement, out statementMeasureName, out statementMeasureExpression, ref lastCharacterPosition); + + //check if it's a kpi measure + Microsoft.AnalysisServices.Kpi kpi = null; + if (_amoDatabase.CompatibilityLevel < 1103) + { + + kpi = _amoDatabase.Cubes[0].Kpis.FindByName(statementMeasureName); //these are populated using the object model (pre SP1 only SOMETIMES) + } + + // Could be declared in script (post-SP1, it should always be, pre SP1 SOMETIMES) + if (kpi == null) + { + //check if declared in MDX script instead (it normally would be) + foreach (Microsoft.AnalysisServices.Kpi kpiDeclaredInScript in kpisDeclaredInScript) + { + if (kpiDeclaredInScript.Name == statementMeasureName) + { + kpi = kpiDeclaredInScript; + break; + } + } + } + + if (kpi == null) + { + // it's really a measure (not a KPI) + _measures.Add(new Measure(this, statementTableName, statementMeasureName, statementMeasureExpression.Trim())); + } + else + { + // it's really a KPI + + //note: the kpiReferenceMeasures will be empty post SP1, but will fix it below. Can't fix it here because might not have all the measures populated yet (post sp1, kpi reference measures will be populated in _measures) + _kpis.Add(new Kpi(this, + statementTableName, + statementMeasureName, + statementMeasureExpression, + kpiReferenceMeasures.FindByName(kpi.Goal), + kpiReferenceMeasures.FindByName(kpi.Status), + kpiReferenceMeasures.FindByName(kpi.Trend), + kpi.StatusGraphic, + kpi.TrendGraphic + //,kpi + )); + } + } + } + } + + //post SP1, fix kpi reference measures + if (_amoDatabase.CompatibilityLevel >= 1103) + { + foreach (Kpi kpi in _kpis) + { + //get the AMO KPI + Microsoft.AnalysisServices.Kpi amoKpi = _amoDatabase.Cubes[0].Kpis.FindByName(kpi.Name); //these are populated using the object model - can also be declared as CREATE KPI in the MDX script, which will not be in this Kpi collection + if (amoKpi == null) + { + //check if declared in MDX script instead (it normally would be) + foreach (Microsoft.AnalysisServices.Kpi kpiDeclaredInScript in kpisDeclaredInScript) + { + if (kpiDeclaredInScript.Name == kpi.Name) + { + amoKpi = kpiDeclaredInScript; + break; + } + } + } + if (amoKpi != null) + { + // Now check _measures to get the kpi reference measures. Flag them as KPI reference measures and populate KPI reference measure properties + Measure kpiGoalReferenceMeasure = _measures.FindByName(amoKpi.Goal); + Measure kpiStatusReferenceMeasure = _measures.FindByName(amoKpi.Status); + Measure kpiTrendReferenceMeasure = _measures.FindByName(amoKpi.Trend); + + //Flag the public KPI measures (like Measures.[M Goal] as IsKpiReferenceMeasure so doesn't show up on grid) + if (kpiGoalReferenceMeasure != null) + { + kpiGoalReferenceMeasure.IsKpiReferenceMeasure = true; + kpi.GoalMeasure = kpiGoalReferenceMeasure; + } + if (kpiStatusReferenceMeasure != null) + { + kpiStatusReferenceMeasure.IsKpiReferenceMeasure = true; + kpi.StatusMeasure = kpiStatusReferenceMeasure; + } + if (kpiTrendReferenceMeasure != null) + { + kpiTrendReferenceMeasure.IsKpiReferenceMeasure = true; + kpi.TrendMeasure = kpiTrendReferenceMeasure; + } + } + } + } + } + } + } + + private void ParseMeasureAndExpression(string measureNameFromAnnotations, string statement, out string statementMeasureName, out string statementMeasureExpression, ref int lastCharacterPosition) + { + //There is some major Microsoft hacking going on here. ] chars are replaced with ]], dependent on all sorts of formatting to identify KPI reference measures (e.g. Measures.[_xxx Goal]), ... the list goes on. + int openSquareBracketPosition = statement.IndexOf('[', lastCharacterPosition); + // following line doesn't work for MS hacked measures like "Measures.[_M1 Goal]" becuase of the hacked underscore + //int closeSquareBracketPosition = statement.IndexOf(']', tableName.Replace("]", " ").Length + openSquareBracketPosition + 1); + // so have to do this instead + string msHackedMeasureFullName = measureNameFromAnnotations.Replace("]", "]]"); + int closeMeasurePosition = statement.IndexOf(msHackedMeasureFullName, openSquareBracketPosition + 1) + msHackedMeasureFullName.Length - 1; + lastCharacterPosition = statement.IndexOf(']', closeMeasurePosition + 1); ; + statementMeasureName = statement.Substring(openSquareBracketPosition + 1, lastCharacterPosition - openSquareBracketPosition - 1); + + int equalSigPosition = statement.IndexOf('=', lastCharacterPosition); + statementMeasureExpression = statement.Substring(equalSigPosition + 1); + } + + private string[] ParseMdxScript(string commandText) + { + List mdxExpressions = new List(); + List subLines = new List(); + using (StringReader lines = new StringReader(commandText)) + { + string line = string.Empty; + Boolean continuedLine = false; + Boolean partialSubLine = false; + Boolean inCommentBlock = false; + StringBuilder mdxExpression = new StringBuilder(); + + while ((line = lines.ReadLine()) != null) + { + line = line.TrimEnd(); + + if (inCommentBlock) + { + if (line.Contains("*/")) + { + inCommentBlock = false; + int closeCommentBlockPosition = line.IndexOf("*/") + 2; + if (line.Length > closeCommentBlockPosition) + { + // check if text after comment block + line = line.Substring(closeCommentBlockPosition, line.Length - closeCommentBlockPosition); + } + else + continue; + } + else + continue; + } + + if (line.Contains("/*")) //start of comment + { + if (!line.Contains("*/")) // does not complete comment in one line + { + inCommentBlock = true; + + //check if there is text before comment block + line = line.Substring(0, line.IndexOf("/*")); + } + else + { //does complete comment in one line - so check if text following comment block + + int closeCommentBlockPosition = line.IndexOf("*/") + 2; + if (line.Length > closeCommentBlockPosition) + { + line = line.Substring(closeCommentBlockPosition, line.Length - closeCommentBlockPosition); + } + else + continue; + } + } + + if (IsBlankLine(line)) + continue; // Ignore comment lines or empty line + + if (line.Contains(';')) + { + subLines.Clear(); + + //8/22/14 commented out following sections. No longer support multiple statements on 1 line. Can have a problem if ';' in measure name and also '"' in measure name. + //// Check the semi-colon is not part a string literal before spliting the line + //if (line.Contains('"')) + //{ + // //Have to do manual split... to avoid spliting a string literal, just in case there is a semi-colon in the string + // int pk = 0; + // int npk = 0; + // do + // { + // int pq = line.IndexOf('"', pk); + // int npq = ((pq + 1) < line.Length) ? line.IndexOf('"', pq + 1) : -1; + // while (((npk = line.IndexOf(';', pk)) != -1) && (npk < pq)) + // { + // if (GetExpressionFromLineFlagEOL(subLines, line, ref pk, ref partialSubLine)) + // break; + // } + // if (npk > pq) + // { + // //if ((npk = line.IndexOf(';', npq)) != -1) + // if ((npk = line.LastIndexOf(';')) != -1) + // { + // if (GetExpressionFromLineFlagEOL(subLines, line, ref pk, ref partialSubLine)) + // break; + // } + // } + + // } while ((npk != -1)); + + //} + //else + //{ + int pk = 0; + while (!GetExpressionFromLineFlagEOL(subLines, line, ref pk, ref partialSubLine)) ; + //} + + if (continuedLine) + { + //subLines[0] = string.Concat(mdxExpression.ToString(), '\n', subLines[0]); + //---------- + if (!mdxExpression.ToString().TrimStart().StartsWith("//")) + { + subLines[0] = string.Concat(mdxExpression.ToString(), subLines[0]); + } + mdxExpression.Clear(); + //---------- + continuedLine = false; + } + for (int i = 0; i < subLines.Count - 1; i++) + { + mdxExpressions.Add(subLines[i].TrimStart()); + } + if (!partialSubLine) + { + mdxExpressions.Add(subLines[subLines.Count - 1].TrimStart()); + } + else + { + mdxExpression.Clear(); + mdxExpression.AppendLine(subLines[subLines.Count - 1]); + continuedLine = true; + } + + } + else + { + continuedLine = true; + mdxExpression.AppendLine(line); + } + + } + } + return mdxExpressions.ToArray(); + } + + private Boolean IsBlankLine(string line) => + (string.IsNullOrWhiteSpace(line) || + (line.TrimStart().Length >= 2 && line.TrimStart().Substring(0, 2) == "--") || + (line.TrimStart().Length >= 2 && line.TrimStart().Substring(0, 2) == "//") + ); + + private Boolean GetExpressionFromLineFlagEOL(List SubLines, string Line, ref int pk, ref bool partialSubLine) + { + //changed following line 8/19/14 + //this won't work because can have ';' in measure names post SP1: int npk = Line.IndexOf(';', pk); + //this won't work because can have ';' in measure names AND not be the end of line: int npk = Line.LastIndexOf(';'); + //so have to use this instead, which does not allow multiple statements on a single line separated by ';': + int npk = Line.IndexOf(';', Line.TrimEnd().Length - 1); + if (npk != -1) + { + SubLines.Add(Line.Substring(pk, (npk - pk) + 1)); // to include both endpoints + pk = npk + 1; + partialSubLine = false; + if (pk >= Line.Length) + { + return true; + } + else + { + return false; + } + } + else + { + if (!IsBlankLine(Line.Substring(pk))) + { + SubLines.Add(Line.Substring(pk)); + partialSubLine = true; + } + return true; //EOL reached + } + } + + #endregion + + #region Properties + + /// + /// Analysis Management Objects Database object abtstracted by the TabularModel class. + /// + public Database AmoDatabase + { + get { return _amoDatabase; } + set { _amoDatabase = value; } + } + + /// + /// Collection of data sources for the TabularModel object. + /// + public DataSourceCollection DataSources => _dataSources; + + /// + /// Collection of tables for the TabularModel object. + /// + public TableCollection Tables => _tables; + + /// + /// Collection of measures for the TabularModel object, excluding those that are KPI references. + /// + public MeasureCollection Measures + { + get + { + //exclude the measures that are kpi reference measures, which are internal + MeasureCollection _returnMeasures = new MeasureCollection(); + foreach (Measure measure in _measures) + { + if (measure.IsKpiReferenceMeasure == false) + { + _returnMeasures.Add(measure); + } + } + return _returnMeasures; + } + } + + /// + /// Collection of measures for the TabularModel object, including those that are KPI references. + /// + public MeasureCollection MeasuresFull => _measures; + + /// + /// Collection of KPIs for the TabularModel object. + /// + public KpiCollection Kpis => _kpis; + + /// + /// Collection of perspectives for the TabularModel object. + /// + public PerspectiveCollection Perspectives => _perspectives; + + /// + /// Collection of roles for the TabularModel object. + /// + public RoleCollection Roles => _roles; + + /// + /// Collection of actions for the TabularModel object. + /// + public ActionCollection Actions => _actions; + + /// + /// List of active relationship ids for the TabularModel object. + /// + public List ActiveRelationshipIds => _activeRelationshipIds; + + /// + /// ConnectionInfo object for the tabular model. + /// + public ConnectionInfo ConnectionInfo => _connectionInfo; + + /// + /// ComparisonInfo object for the tabular model. + /// + public ComparisonInfo ComparisonInfo => _comparisonInfo; + + #endregion + + #region Actions + + /// + /// Remove all reference dimensions from the AMO tabular model when starting to validate actions. They are added back dynamically at the end of validation. + /// + public void FlushReferenceDimensions() + { + if (_amoDatabase.Cubes.Count == 0) return; + + // Clear out reference dimensions - will recreate them later according to new model + foreach (MeasureGroup measureGroup in _amoDatabase.Cubes[0].MeasureGroups) + { + List measureGroupDimensionIdsToDelete = new List(); + foreach (MeasureGroupDimension measureGroupDimension in measureGroup.Dimensions) + { + if (measureGroupDimension is ReferenceMeasureGroupDimension) + { + ReferenceMeasureGroupDimension referenceMeasureGroupDimension = ((ReferenceMeasureGroupDimension)measureGroupDimension); + + // If there is a reference dimension for the relationship, then it is active + //_activeRelationshipIds.Add(referenceMeasureGroupDimension.RelationshipID); //unfortunately, the RelationshipID property is not always populated - only if the intermediate dim is the fact dim + foreach (Table table in _tables) + { + bool foundRelationship = false; + foreach (Relationship relationship in table.Relationships) + { + if (referenceMeasureGroupDimension.IntermediateCubeDimensionID == relationship.AmoRelationship.FromRelationshipEnd.DimensionID && + referenceMeasureGroupDimension.CubeDimensionID == relationship.AmoRelationship.ToRelationshipEnd.DimensionID) + { + if (!_activeRelationshipIds.Contains(relationship.Id)) + { + _activeRelationshipIds.Add(relationship.Id); + } + foundRelationship = true; + break; + } + } + if (foundRelationship) + { + break; + } + } + + referenceMeasureGroupDimension.IntermediateCubeDimensionID = null; + measureGroupDimensionIdsToDelete.Add(measureGroupDimension.CubeDimensionID); + } + } + foreach (string dimensionId in measureGroupDimensionIdsToDelete) + { + measureGroup.Dimensions.Remove(dimensionId); + } + } + } + + /// + /// Dynamically add back reference dimensions when finishing validation of actions. This includes checking for ambigious paths and setting to inactive as required. + /// + public void PopulateReferenceDimensions() + { + //7. Repopulate reference dims based on relationships. When iterating the tables, keep a record (string array) of + // relationships added (or having ref dim implemented) that are active relationships. If come across a 2nd one to the same + // table, the one that was already in the target wins. + + if (_amoDatabase.Cubes.Count > 0) + { + foreach (MeasureGroup measureGroup in _amoDatabase.Cubes[0].MeasureGroups) + { + // If have any relationships, the measure group will have a fact dimension that acts as the intermediate dimension + if (measureGroup.Dimensions.Count == 1) + { + string degenerateDimensionId = measureGroup.Dimensions[0].CubeDimensionID; + Table measureGroupTable = _tables.FindById(measureGroup.ID); + + foreach (Relationship relationship in measureGroupTable.Relationships) + { + PopulateReferenceDimension(measureGroup, degenerateDimensionId, relationship); + } + } + } + } + } + + private void PopulateReferenceDimension(MeasureGroup measureGroup, string degenerateDimensionId, Relationship relationship) + { + if (relationship.IsActive) + { + Dimension referencedDimension = _tables.FindById(relationship.AmoRelationship.ToRelationshipEnd.DimensionID).AmoDimension; + bool willAddReference = false; + + if (measureGroup.Dimensions.Contains(referencedDimension.ID)) + { + // If we are here, we have identified 2 paths to get to the same reference dimension - because when combining source/target models would result in 2 active relationship paths to the same table. So, the one that was already there in the target should win. + if (relationship.CopiedFromSource) + { + // So we just ignore [relationship] (don't populate it). But we also need to call DeleteAlternateActiveRelationship to ensure it's not flagged as active anymore - and also flush any reference dims that might have already been populated with it ... + DeleteAlternateActiveRelationship(relationship); + + _parentComparison.OnValidationMessage(new ValidationMessageEventArgs( + "Relationship " + relationship.Name.Trim() + " (which is active in the source) has been created in the target, but it is set to inactive because there is already an active set of relationships between '" + measureGroup.Name + "' and '" + referencedDimension.Name + "'.", + ValidationMessageType.Relationship, + ValidationMessageStatus.Warning)); + willAddReference = false; + } + else + { + // [relationship] is the one that was already in the target. So need to remove the existing one (which was copied from source) + ReferenceMeasureGroupDimension referenceDimToDelete = (ReferenceMeasureGroupDimension)measureGroup.Dimensions[referencedDimension.ID]; + bool foundRelationship = false; + + //We need to delete the relationship that has already been populated - which was copied from source + foreach (Table table in _tables) + { + foreach (Relationship potentiallyRelationshipToDelete in table.Relationships) + { + if (potentiallyRelationshipToDelete.Id != relationship.Id && + potentiallyRelationshipToDelete.AmoRelationship.FromRelationshipEnd.DimensionID == referenceDimToDelete.IntermediateCubeDimensionID && + potentiallyRelationshipToDelete.AmoRelationship.ToRelationshipEnd.DimensionID == referenceDimToDelete.CubeDimensionID) + { + DeleteAlternateActiveRelationship(potentiallyRelationshipToDelete); + _parentComparison.OnValidationMessage(new ValidationMessageEventArgs( + "Relationship " + potentiallyRelationshipToDelete.Name.Trim() + " (which is active in the source) has been created in the target, but it is set to inactive because there is already an active set of relationships between '" + measureGroup.Name + "' and '" + referencedDimension.Name + "'.", + ValidationMessageType.Relationship, + ValidationMessageStatus.Warning)); + foundRelationship = true; + break; + } + } + if (foundRelationship) + { + break; + } + } + willAddReference = true; + } + } + else + { + willAddReference = true; + } + + if (willAddReference) + { + ReferenceMeasureGroupDimension referenceMeasuregroupDimension = new ReferenceMeasureGroupDimension(); + referenceMeasuregroupDimension.CubeDimensionID = referencedDimension.ID; + foreach (DimensionAttribute attribute in referencedDimension.Attributes) + { + MeasureGroupAttribute mgAttr = referenceMeasuregroupDimension.Attributes.Add(attribute.ID); + if (relationship.AmoRelationship.ToRelationshipEnd.Attributes.Contains(attribute.ID)) + { + mgAttr.Type = MeasureGroupAttributeType.Granularity; + } + foreach (DataItem di in attribute.KeyColumns) + { + mgAttr.KeyColumns.Add(di.Clone()); + } + } + + Dimension intermediateDimension = _tables.FindById(relationship.AmoRelationship.FromRelationshipEnd.DimensionID).AmoDimension; + referenceMeasuregroupDimension.IntermediateCubeDimensionID = intermediateDimension.ID; + referenceMeasuregroupDimension.IntermediateGranularityAttributeID = relationship.AmoRelationship.FromRelationshipEnd.Attributes[0].AttributeID; + + // these last properties are only set if the intermediate dimension is the fact dimension + if (intermediateDimension.ID == degenerateDimensionId) + { + referenceMeasuregroupDimension.Materialization = ReferenceDimensionMaterialization.Regular; + referenceMeasuregroupDimension.RelationshipID = relationship.Id; + } + + measureGroup.Dimensions.Add(referenceMeasuregroupDimension); + + foreach (Relationship referenceChainRelationship in _tables.FindById(relationship.AmoRelationship.ToRelationshipEnd.DimensionID).Relationships) + { + PopulateReferenceDimension(measureGroup, degenerateDimensionId, referenceChainRelationship); + } + } + } + } + + private void DeleteAlternateActiveRelationship(Relationship relationship) + { + // remove from db's active relationshps collection + relationship.Table.TabularModel.ActiveRelationshipIds.Remove(relationship.Id); + + // We also need to check all the existing reference relationships (it's possible it's in there) + foreach (MeasureGroup measureGroup in _amoDatabase.Cubes[0].MeasureGroups) + { + List measureGroupDimensionIdsToDelete = new List(); + foreach (MeasureGroupDimension measureGroupDimension in measureGroup.Dimensions) + { + if (measureGroupDimension is ReferenceMeasureGroupDimension) + { + ReferenceMeasureGroupDimension referenceMeasureGroupDimension = ((ReferenceMeasureGroupDimension)measureGroupDimension); + + if (referenceMeasureGroupDimension.IntermediateCubeDimensionID == relationship.AmoRelationship.FromRelationshipEnd.DimensionID && + referenceMeasureGroupDimension.CubeDimensionID == relationship.AmoRelationship.ToRelationshipEnd.DimensionID && + relationship.AmoRelationship.FromRelationshipEnd.Attributes.Contains(referenceMeasureGroupDimension.IntermediateGranularityAttributeID)) + { + referenceMeasureGroupDimension.IntermediateCubeDimensionID = null; + measureGroupDimensionIdsToDelete.Add(measureGroupDimension.CubeDimensionID); + } + } + } + foreach (string dimensionId in measureGroupDimensionIdsToDelete) + { + measureGroup.Dimensions.Remove(dimensionId); + } + } + } + + /// + /// Check whether the TabularModel object contains a relationship. + /// + /// The id of the relationship. + /// True if found; false if not. + public bool ContainsRelationship(string relationshipId) + { + bool foundRelationship = false; + + foreach (Table table in _tables) + { + foreach (Relationship relationship in table.Relationships) + { + if (relationship.Id == relationshipId) + { + foundRelationship = true; + break; + } + } + if (foundRelationship) + { + break; + } + } + + return foundRelationship; + } + + /// + /// Find a relationship by its id. + /// + /// The id of the relationship. + /// Relationship if found; null if not. + public Relationship FindRelationshipById(string relationshipId) + { + Relationship returnRelationship = null; + + foreach (Table table in _tables) + { + foreach (Relationship relationship in table.Relationships) + { + if (relationship.Id == relationshipId) + { + returnRelationship = relationship; + break; + } + } + if (returnRelationship != null) + { + break; + } + } + + return returnRelationship; + } + + + #region DataSources + + /// + /// Delete datasource associated with the TabularModel object. + /// + /// The id of the datasource to be deleted. + public void DeleteDataSource(string id) + { + if (_amoDatabase.DataSources.Contains(id)) + { + _amoDatabase.DataSources.Remove(id); + } + + //check if DataSourceViews[0].DataSourceID refers to the datasource to be deleted + if (_amoDatabase.DataSourceViews.Count > 0 && _amoDatabase.DataSourceViews[0].DataSourceID == id) + { + //set it to the first data source in the cube (should be fine because all the existing tables that use this datasource will also be deleted) + if (_amoDatabase.DataSources.Count > 0) + { + _amoDatabase.DataSourceViews[0].DataSourceID = _amoDatabase.DataSources[0].ID; + } + else + { + _amoDatabase.DataSourceViews[0].DataSourceID = null; + } + } + + // shell model + if (_dataSources.ContainsId(id)) + { + _dataSources.RemoveById(id); + } + } + + /// + /// Create data source associated with the TabularModel object. + /// + /// DataSource object from the source tabular model to be created in the target. + public void CreateDataSource(DataSource dataSourceSource) + { + Microsoft.AnalysisServices.DataSource amoDataSourceTarget = dataSourceSource.AmoDataSource.Clone(); + + // Need to check if there is an existing datasource with same ID (some clever clogs might have renamed the object in source and kept same ID). If so, replace it with a new one and store it as substitute ID in source. + if (_amoDatabase.DataSources.Contains(dataSourceSource.Id)) + { + amoDataSourceTarget.ID = Convert.ToString(Guid.NewGuid()); + dataSourceSource.SubstituteId = amoDataSourceTarget.ID; + } + + _amoDatabase.DataSources.Add(amoDataSourceTarget); + + // in the event we deleted the only datasource in the DeleteDataSource method above, ... + if (_amoDatabase.DataSourceViews.Count > 0 && _amoDatabase.DataSourceViews[0].DataSourceID == null) + { + _amoDatabase.DataSourceViews[0].DataSourceID = amoDataSourceTarget.ID; + } + + // shell model + _dataSources.Add(new DataSource(this, amoDataSourceTarget)); + } + + /// + /// Update datasource associated with the TabularModel object. + /// + /// DataSource object from the source tabular model to be updated in the target. + /// DataSource object in the target tabular model to be updated. + public void UpdateDataSource(DataSource dataSourceSource, DataSource dataSourceTarget) + { + dataSourceTarget.AmoDataSource.ConnectionString = dataSourceSource.AmoDataSource.ConnectionString; + + if (dataSourceSource.Id != dataSourceTarget.Id) + { + // If the names are the same, but the IDs are different, need to store the ID from the target in the source connection so that when Create/Update subsequent tables (partitions, DSVs and special dimension properties), we know to substitute the Connection ID + dataSourceSource.SubstituteId = dataSourceTarget.Id; + } + } + + #endregion + + #region Tables + + /// + /// Delete table associated with the TabularModel object. + /// + /// Id of the table to be deleted. + /// Flag indicatign whether to delete child relationships of the table. + public void DeleteTable(string id, bool deleteChildRelatoinships = true) + { + if (deleteChildRelatoinships) + { + // Check if any other tables refer to the one about to be deleted - if so, delete relationship + foreach (Table table in _tables) + { + List relationshipIdsToDelete = new List(); //can't remove from collection whilst iterating it + foreach (Relationship relationship in table.Relationships) + { + if (relationship.AmoRelationship.ToRelationshipEnd.DimensionID == id) + { + relationshipIdsToDelete.Add(relationship.Id); + } + } + foreach (string relationshipId in relationshipIdsToDelete) + { + table.DeleteRelationship(relationshipId); + } + } + } + + // DSV table + if (_amoDatabase.DataSourceViews[0].Schema.Tables.Contains(id)) + { + _amoDatabase.DataSourceViews[0].Schema.Tables.Remove(id); + } + + // Dim/Measure group + if (_amoDatabase.Cubes[0].Dimensions.Contains(id)) + { + if (_amoDatabase.Cubes[0].MeasureGroups.Contains(id)) + { + _amoDatabase.Cubes[0].MeasureGroups[id].Measures.Clear(); + _amoDatabase.Cubes[0].MeasureGroups[id].Partitions.Clear(); + _amoDatabase.Cubes[0].MeasureGroups.Remove(id, true); + } + _amoDatabase.Cubes[0].Dimensions.Remove(id, true); + _amoDatabase.Dimensions.Remove(id); + } + + // shell model + if (_tables.ContainsId(id)) + { + _tables.RemoveById(id); + } + } + + /// + /// Create table associated with the TabularModel object. + /// + /// Table object from the source tabular model to be created in the target. + /// Substitute id from the source table. + /// Flag indicating whether use of the substitute id is required. + public void CreateTable(Table tableSource, ref string sourceObjectSubstituteId, ref bool useSubstituteId) + { + #region If blank db, need to create dsv/cube/mdx script + + if (_amoDatabase.Cubes.Count == 0) + { + string newDataSourceViewName = tableSource.TabularModel.AmoDatabase.DataSourceViews[0].Name; + DataSet newDataSourceViewDataSet = new DataSet(newDataSourceViewName); + DataSourceView newDatasourceView = _amoDatabase.DataSourceViews.AddNew(newDataSourceViewName, newDataSourceViewName); + newDatasourceView.DataSourceID = tableSource.TabularModel.DataSources.FindById(tableSource.DataSourceID).SubstituteId; + newDatasourceView.Schema = newDataSourceViewDataSet; + + Cube sandboxCube = _amoDatabase.Cubes.Add(tableSource.TabularModel.AmoDatabase.Cubes[0].Name, tableSource.TabularModel.AmoDatabase.Cubes[0].ID); + sandboxCube.Source = new DataSourceViewBinding(newDatasourceView.ID); + sandboxCube.StorageMode = StorageMode.InMemory; + sandboxCube.Language = tableSource.TabularModel.AmoDatabase.Language; + sandboxCube.Collation = tableSource.TabularModel.AmoDatabase.Collation; + + //Create initial MdxScript + MdxScript mdxScript = sandboxCube.MdxScripts.Add(tableSource.TabularModel.AmoDatabase.Cubes[0].MdxScripts[0].Name, tableSource.TabularModel.AmoDatabase.Cubes[0].MdxScripts[0].ID); + mdxScript.Commands.Add(new Microsoft.AnalysisServices.Command(tableSource.TabularModel.AmoDatabase.Cubes[0].MdxScripts[0].Commands[0].Text)); + } + // check to add 2nd command here just in case get a cube with only the first default command populated + if (_amoDatabase.Cubes[0].MdxScripts[0].Commands.Count == 1) + { + _amoDatabase.Cubes[0].MdxScripts[0].Commands.Add(new Microsoft.AnalysisServices.Command("")); //blank 2nd command to hold measures + } + + #endregion + + #region Need to check if there is an existing table with same ID (some clever clogs might have renamed the object in source and kept same ID). If so, replace it with a new one and store it as substitute ID in source. + + if (_amoDatabase.Dimensions.Contains(tableSource.Id)) + { + tableSource.SubstituteId = tableSource.Name + "_" + Convert.ToString(Guid.NewGuid()); + sourceObjectSubstituteId = tableSource.SubstituteId; + useSubstituteId = true; + } + + string substituteDataSourceId = tableSource.TabularModel.DataSources.FindById(tableSource.DataSourceID).SubstituteId; + + #endregion + + #region DSV Table + + if (tableSource.AmoTable != null) + { + //DataTable tableTarget = tableSource.AmoTable.Clone(); + DataTable tableTarget = tableSource.AmoTable.Copy(); + tableTarget.ExtendedProperties["DataSourceID"] = substituteDataSourceId; + if (useSubstituteId) tableTarget.TableName = tableSource.SubstituteId; + + if (_amoDatabase.DataSourceViews[0].Schema.Tables.Contains(tableTarget.TableName)) + { + _amoDatabase.DataSourceViews[0].Schema.Tables.Remove(tableTarget.TableName); + } + + _amoDatabase.DataSourceViews[0].Schema.Tables.Add(tableTarget); + } + #endregion + + #region Dimension / Relationships + + Dimension dimensionTarget = tableSource.AmoDimension.Clone(); + if (tableSource.AmoDimension.Source is DataSourceViewBinding) + { + dimensionTarget.Source = new DataSourceViewBinding(_amoDatabase.DataSourceViews[0].ID); + } + + if (useSubstituteId) + { + dimensionTarget.ID = tableSource.SubstituteId; + + foreach (DimensionAttribute attribute in dimensionTarget.Attributes) + { + foreach (DataItem keyColumn in attribute.KeyColumns) + { + if (keyColumn.Source is ColumnBinding && ((ColumnBinding)keyColumn.Source).TableID != tableSource.SubstituteId) + { + ((ColumnBinding)keyColumn.Source).TableID = tableSource.SubstituteId; + } + } + if (attribute.NameColumn.Source is ColumnBinding && ((ColumnBinding)attribute.NameColumn.Source).TableID != tableSource.SubstituteId) + { + ((ColumnBinding)attribute.NameColumn.Source).TableID = tableSource.SubstituteId; + } + } + } + + // clear all relationships inherited from source table; they will be added back later only if required + dimensionTarget.Relationships.Clear(); + _amoDatabase.Dimensions.Add(dimensionTarget); + if (useSubstituteId) _amoDatabase.Cubes[0].Dimensions.Add(tableSource.SubstituteId); + + if (!_amoDatabase.Cubes[0].Dimensions.Contains(tableSource.SubstituteId)) + { + _amoDatabase.Cubes[0].Dimensions.Add(tableSource.SubstituteId); + } + + if (tableSource.AmoCubeDimension != null && tableSource.AmoCubeDimension.Visible == false) + { + _amoDatabase.Cubes[0].Dimensions[tableSource.SubstituteId].Visible = false; + } + + #endregion + + #region Measure Group + + MeasureGroup measureGroupTarget = tableSource.AmoMeasureGroup.Clone(); + + if (useSubstituteId) + { + measureGroupTarget.ID = tableSource.SubstituteId; + + string measureGroupDimensionIdToRename = ""; //can't rename it while in MeasureGroupDimension (.Dimensions) collection + foreach (MeasureGroupDimension measureGroupDimension in measureGroupTarget.Dimensions) + { + if (measureGroupDimension is DegenerateMeasureGroupDimension) + { + measureGroupDimensionIdToRename = measureGroupDimension.CubeDimensionID; + break; + } + } + if (measureGroupDimensionIdToRename != "") + { + DegenerateMeasureGroupDimension measureGroupDimensionClone = (DegenerateMeasureGroupDimension)measureGroupTarget.Dimensions[measureGroupDimensionIdToRename].Clone(); + measureGroupDimensionClone.CubeDimensionID = tableSource.SubstituteId; + foreach (MeasureGroupAttribute attribute in measureGroupDimensionClone.Attributes) + { + foreach (DataItem keyColumn in attribute.KeyColumns) + { + if (keyColumn.Source is ColumnBinding && ((ColumnBinding)keyColumn.Source).TableID != tableSource.SubstituteId) + { + ((ColumnBinding)keyColumn.Source).TableID = tableSource.SubstituteId; + } + } + } + + if (measureGroupTarget.Measures.Contains(measureGroupDimensionIdToRename)) + { + Microsoft.AnalysisServices.Measure measureClone = measureGroupTarget.Measures[measureGroupDimensionIdToRename].Clone(); + measureClone.ID = tableSource.SubstituteId; + if (measureClone.Source.Source is RowBinding && ((RowBinding)measureClone.Source.Source).TableID != tableSource.SubstituteId) + { + ((RowBinding)measureClone.Source.Source).TableID = tableSource.SubstituteId; + } + + measureGroupTarget.Measures.Remove(measureGroupDimensionIdToRename); + measureGroupTarget.Measures.Add(measureClone); + } + + measureGroupTarget.Dimensions.Remove(measureGroupDimensionIdToRename); + measureGroupTarget.Dimensions.Add(measureGroupDimensionClone); + } + } + + //Now make sure all partitions are hooked up + List partitionIdsToRename = new List(); + foreach (Partition partition in measureGroupTarget.Partitions) + { + if (partition.Source is QueryBinding) partition.Source = new QueryBinding(substituteDataSourceId, ((QueryBinding)partition.Source).QueryDefinition); + if (useSubstituteId && partition.ID == tableSource.Id) partitionIdsToRename.Add(partition.ID); + } + foreach (string partitionIdToRename in partitionIdsToRename) + { + Partition partition = measureGroupTarget.Partitions[partitionIdToRename]; + Partition partitionClone = partition.Clone(); + partitionClone.ID = tableSource.SubstituteId; + measureGroupTarget.Partitions.Remove(partition.ID); + measureGroupTarget.Partitions.Add(partitionClone); + } + + //And finally add it to the target cube + _amoDatabase.Cubes[0].MeasureGroups.Add(measureGroupTarget); + + #endregion + + #region Shell model + + _tables.Add(new Table(this, dimensionTarget)); + + #endregion + } + + /// + /// Update relationships with substitute ids to avoid unique id conflict. + /// + /// Old table id. + /// New table substitute id. + public void UpdateRelationshipsWithSubstituteTableIds(string oldTableId, string newTableSubstituteId) + { + foreach (Table table in _tables) + { + foreach (Relationship relationship in table.Relationships) + { + if (relationship.AmoRelationship.FromRelationshipEnd.DimensionID == oldTableId) + { + relationship.AmoRelationship.FromRelationshipEnd.DimensionID = newTableSubstituteId; + } + if (relationship.AmoRelationship.ToRelationshipEnd.DimensionID == oldTableId) + { + relationship.AmoRelationship.ToRelationshipEnd.DimensionID = newTableSubstituteId; + } + } + } + } + + /// + /// Update tablre associated with the TabularModel object. + /// + /// Table object from the source tabular model to be updated in the target. + /// Table object in the target tabular model to be updated. + /// Substitute id of the source object. + /// Flag indicating whether it is required to use the substitute id. + public void UpdateTable(Table tableSource, Table tableTarget, ref string sourceObjectSubstituteId, ref bool useSubstituteId) + { + if (tableSource.Id != tableTarget.Id) + { + // If the names are the same, but the IDs are different, need to store the ID from the target in the source Table so that maintains existing object relationships + tableSource.SubstituteId = tableTarget.Id; + sourceObjectSubstituteId = tableSource.SubstituteId; + useSubstituteId = true; + } + + #region Backup the target db for reference (otherwise perspectives can't get at dim attribute names) + Database dbTargetBackup = _amoDatabase.Clone(); + #endregion + + Dimension dimensionTargetBackup = tableTarget.AmoDimension.Clone(); + DeleteTable(tableTarget.Id, deleteChildRelatoinships: false); + CreateTable(tableSource, ref sourceObjectSubstituteId, ref useSubstituteId); + //get back the newly created table + tableTarget = _tables.FindById(tableSource.SubstituteId); + tableTarget.AmoOldDimensionBackup = dimensionTargetBackup; + + #region Add back table/columns to perspectives if required + foreach (Microsoft.AnalysisServices.Perspective perspective in _amoDatabase.Cubes[0].Perspectives) + { + if (dbTargetBackup.Cubes[0].Perspectives.Contains(perspective.ID)) + { + Microsoft.AnalysisServices.Perspective perspectiveBackup = dbTargetBackup.Cubes[0].Perspectives.Find(perspective.ID); + if (perspectiveBackup.Dimensions.Contains(tableTarget.Id)) + { + PerspectiveDimension perspectiveDimensionBackup = perspectiveBackup.Dimensions.Find(tableTarget.Id); + PerspectiveDimension perspectiveDimension = perspective.Dimensions.Find(tableTarget.Id); + + //table + if (perspectiveDimension == null) + { + perspectiveDimension = perspective.Dimensions.Add(tableTarget.Id); + } + + //attributes + foreach (PerspectiveAttribute attributeBackup in perspectiveDimensionBackup.Attributes) + { + bool foundMatch = false; + foreach (PerspectiveAttribute attribute in perspectiveDimension.Attributes) + { + if (attributeBackup.Attribute.Name == attribute.Attribute.Name) + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + //we know the attribute is not already in the dim perspective. Now see if it's in the actual dim. + DimensionAttribute dimAttribute = tableTarget.AmoDimension.Attributes.FindByName(attributeBackup.Attribute.Name); + if (dimAttribute != null) + { + perspectiveDimension.Attributes.Add(dimAttribute.ID); + } + } + } + + //hierarchies + foreach (PerspectiveHierarchy hierarchyBackup in perspectiveDimensionBackup.Hierarchies) + { + bool foundMatch = false; + foreach (PerspectiveHierarchy hierarchy in perspectiveDimension.Hierarchies) + { + if (hierarchyBackup.Hierarchy.Name == hierarchy.Hierarchy.Name) + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + //we know the hierarchy is not already in the dim perspective. Now see if it's in the actual dim. + Hierarchy dimHierarchy = tableTarget.AmoDimension.Hierarchies.FindByName(hierarchyBackup.Hierarchy.Name); + if (dimHierarchy != null) + { + perspectiveDimension.Hierarchies.Add(dimHierarchy.ID); + } + } + } + } + } + } + #endregion + + //Add back parent relationships from target clone (assuming necessary tables/columns exist). + List relationshipIdsToAddBack = new List(); + foreach (Microsoft.AnalysisServices.Relationship amoRelationship in dimensionTargetBackup.Relationships) + { + if (_tables.ContainsId(amoRelationship.ToRelationshipEnd.DimensionID)) + { + relationshipIdsToAddBack.Add(amoRelationship.ID); + } + } + foreach (string relationshipIdToAddBack in relationshipIdsToAddBack) + { + Microsoft.AnalysisServices.Relationship amoRelationship = dimensionTargetBackup.Relationships.Find(relationshipIdToAddBack); + Table parentTable = _tables.FindById(amoRelationship.ToRelationshipEnd.DimensionID); + + // it is possible that the parent table's relationship column's ID has changed (if table been updated) ... + if (!parentTable.AmoDimension.Attributes.Contains(amoRelationship.ToRelationshipEnd.Attributes[0].AttributeID)) + { + // get the Name from the backup + string nameOfAttributeWithWrongId = parentTable.AmoOldDimensionBackup.Attributes.Find(amoRelationship.ToRelationshipEnd.Attributes[0].AttributeID).Name; + RelationshipEndAttribute relationshipEndAttributeClone = amoRelationship.ToRelationshipEnd.Attributes[0].Clone(); + DimensionAttribute parentAttribute = parentTable.AmoDimension.Attributes.FindByName(nameOfAttributeWithWrongId); + if (parentAttribute != null) + { + relationshipEndAttributeClone.AttributeID = parentAttribute.ID; + amoRelationship.ToRelationshipEnd.Attributes.Remove(amoRelationship.ToRelationshipEnd.Attributes[0].AttributeID); + amoRelationship.ToRelationshipEnd.Attributes.Add(relationshipEndAttributeClone); + } + } + // note: in this case we can pass in [parentTable.AmoDimension] as the 2nd parameter [parentDimSource] because we are just copying back the relationships that were already there in the target + string warningMessage = ""; + tableTarget.CreateRelationship(amoRelationship, parentTable.AmoDimension, "", ref warningMessage, tableTarget.TabularModel.ActiveRelationshipIds.Contains(amoRelationship.ID)); + } + } + + /// + /// Update the relationships for children of updated tables to maintain referential integrity in the model. + /// + /// Target Table object. + public void UpdateRelationshipsForChildrenOfUpdatedTables(Table tableTarget) + { + //Now we have to check child relationships that referred to this table. They might need to be deleted if the parent column is no longer there. + //Or they might even use an AttributeId that is not valid, but the column is actually there (same name, different attributeId) + foreach (Table table in _tables) + { + // might need to delete relationships, or modify the attributeids, but can't do while in collection + List relationshipIdsToDelete = new List(); + + foreach (Relationship relationship in table.Relationships) + { + if (relationship.AmoRelationship.ToRelationshipEnd.DimensionID == tableTarget.Id) + { + Microsoft.AnalysisServices.Relationship amoRelationshipTemp = null; + + foreach (RelationshipEndAttribute attribute in relationship.AmoRelationship.ToRelationshipEnd.Attributes) + { + DimensionAttribute dimAttributeParentBackup = tableTarget.AmoOldDimensionBackup.Attributes.Find(attribute.AttributeID); + if (dimAttributeParentBackup != null) // will only be null if changed parent attribute id in UpdateTable - in which case don't need to worry about it + { + DimensionAttribute dimAttributeParent = tableTarget.AmoDimension.Attributes.FindByName(dimAttributeParentBackup.Name); + if (dimAttributeParent == null) + { + //parent attribute is definitely not there (not even with a different id), so need to delete this relationship + if (!relationshipIdsToDelete.Contains(relationship.Id)) + { + relationshipIdsToDelete.Add(relationship.Id); + } + break; + } + else + { + //parent attribute is there. If has same Id, we are good. Othersise, need to change to new Id + if (dimAttributeParentBackup.ID != dimAttributeParent.ID) + { + amoRelationshipTemp = relationship.AmoRelationship.Clone(); + RelationshipEndAttribute parentDimAttributeTemp = attribute.Clone(); + parentDimAttributeTemp.AttributeID = dimAttributeParent.ID; + amoRelationshipTemp.ToRelationshipEnd.Attributes.Remove(attribute.AttributeID); + amoRelationshipTemp.ToRelationshipEnd.Attributes.Add(parentDimAttributeTemp); + } + + //check that the parent attribute allows only unique values + if (dimAttributeParent.Parent.Attributes.Contains("RowNumber") && + dimAttributeParent.Parent.Attributes["RowNumber"].AttributeRelationships.Contains(dimAttributeParent.ID) && + dimAttributeParent.Parent.Attributes["RowNumber"].AttributeRelationships[dimAttributeParent.ID].Cardinality != Cardinality.One) + { + dimAttributeParent.Parent.Attributes["RowNumber"].AttributeRelationships[dimAttributeParent.ID].Cardinality = Cardinality.One; + foreach (DataItem di in dimAttributeParent.KeyColumns) + { + di.NullProcessing = NullProcessing.Error; + } + if (_amoDatabase.Cubes.Count > 0) + { + foreach (MeasureGroup mg in _amoDatabase.Cubes[0].MeasureGroups) + { + if (mg.ID == dimAttributeParent.Parent.ID) + { + foreach (MeasureGroupDimension mgd in mg.Dimensions) + { + if (mgd.CubeDimensionID == dimAttributeParent.Parent.ID && mgd is DegenerateMeasureGroupDimension) + { + foreach (MeasureGroupAttribute mga in ((DegenerateMeasureGroupDimension)mgd).Attributes) + { + if (mga.AttributeID == dimAttributeParent.ID) + { + mga.KeyColumns[0].NullProcessing = NullProcessing.Error; + } + } + } + } + } + } + } + } + } + } + } + if (amoRelationshipTemp != null) //i.e. we had to replace at least one attribute id + { + table.AmoDimension.Relationships.Remove(relationship.AmoRelationship.ID); + table.AmoDimension.Relationships.Add(amoRelationshipTemp); + relationship.AmoRelationship = amoRelationshipTemp; + } + } + } + foreach (string relationshipIdToDelete in relationshipIdsToDelete) + { + table.DeleteRelationship(relationshipIdToDelete); + } + } + } + + /// + /// Check relationship validity to maintain referential integrity in the model. + /// + public void CheckRelationshipValidity() + { + //in rare cases where tables updated and old table had relationship to updated table, renamed tables, etc., need this safety net + foreach (Table table in _tables) + { + List relationshipIdsToDelete = new List(); + foreach (Relationship relationship in table.Relationships) + { + bool deleteRelationship = false; + + foreach (RelationshipEndAttribute attribute in relationship.AmoRelationship.FromRelationshipEnd.Attributes) + { + if (!table.AmoDimension.Attributes.Contains(attribute.AttributeID)) + { + if (!relationshipIdsToDelete.Contains(relationship.Id)) + { + relationshipIdsToDelete.Add(relationship.Id); + } + deleteRelationship = true; + break; + } + } + + if (!deleteRelationship) + { + Table parentTable = _tables.FindById(relationship.AmoRelationship.ToRelationshipEnd.DimensionID); + if (parentTable == null) + { + if (!relationshipIdsToDelete.Contains(relationship.Id)) + { + relationshipIdsToDelete.Add(relationship.Id); + } + } + else + { + foreach (RelationshipEndAttribute attribute in relationship.AmoRelationship.ToRelationshipEnd.Attributes) + { + if (!parentTable.AmoDimension.Attributes.Contains(attribute.AttributeID)) + { + if (!relationshipIdsToDelete.Contains(relationship.Id)) + { + relationshipIdsToDelete.Add(relationship.Id); + } + break; + } + } + } + } + } + foreach (string relationshipIdToDelete in relationshipIdsToDelete) + { + table.DeleteRelationship(relationshipIdToDelete); + } + } + } + + #endregion + + #region Measures + + /// + /// Delete measure associated with the TabularModel object. + /// + /// The id of the measure to be deleted. + public void DeleteMeasure(string id) + { + DeleteCalculationProperty(_measures.FindById(id).CalculationReference); + + // shell model + if (_measures.ContainsId(id)) + { + _measures.RemoveById(id); + } + } + + /// + /// Create measure associated with the TabularModel object. + /// + /// Measure object from the source tabular model to be created in the target. + public void CreateMeasure(Measure measureSource) + { + CreateCalculationProperty(measureSource.AmoCalculationProperty, measureSource.CalculationReference); + + // shell model + _measures.Add(new Measure(this, measureSource.TableName, measureSource.Name, measureSource.Expression)); + } + + /// + /// Update measure associated with the TabularModel object. + /// + /// Measure object from the source tabular model to be updated in the target. + /// Measure object in the target tabular model to be updated. + public void UpdateMeasure(Measure measureSource, Measure measureTarget) + { + DeleteCalculationProperty(measureTarget.CalculationReference); + CreateCalculationProperty(measureSource.AmoCalculationProperty, measureSource.CalculationReference); + + measureTarget.Expression = measureSource.Expression; + } + + #endregion + + #region KPIs + + /// + /// Delete KPI associated with the TabularModel object. + /// + /// The id of the KPI to be deleted. + public void DeleteKpi(string id) + { + Kpi kpiToDelete = _kpis.FindById(id); + + DeleteCalculationProperty(kpiToDelete.CalculationReference); + DeleteCalculationProperty(kpiToDelete.GoalCalculationReference); + DeleteCalculationProperty(kpiToDelete.StatusCalculationReference); + if (_amoDatabase.CompatibilityLevel < 1103) DeleteCalculationProperty(kpiToDelete.TrendCalculationReference); + DeleteCalculationProperty(kpiToDelete.KpiCalculationReference); + + //_amoDatabase.Cubes[0].Kpis.Remove(kpiToDelete.AmoKpi.ID); + + // shell model + _kpis.RemoveById(id); + } + + /// + /// Create KPI associated with the TabularModel object. + /// + /// Kpi object from the source tabular model to be created in the target. + public void CreateKpi(Kpi kpiSource) + { + /* No longer using AMO KPI model since latest version of Tabular Editor has switched to use MDX script declarations instead */ + //if (kpiSource.AmoKpi != null) + //{ + // Kpi amoKpiTarget = kpiSource.AmoKpi.Clone(); + + // // Need to check if there is an existing KPI with same ID (some clever clogs might have renamed the object in source and kept same ID). If so, replace it with a new one and store it as substitute ID in source. + // if (_amoDatabase.Cubes[0].Kpis.Contains(kpiSource.Id)) + // { + // amoKpiTarget.ID = Convert.ToString(Guid.NewGuid()); + // kpiSource.SubstituteId = amoKpiTarget.ID; + // } + + // _amoDatabase.Cubes[0].Kpis.Add(amoKpiTarget); + //} + + CreateCalculationProperty(kpiSource.AmoCalculationProperty, kpiSource.CalculationReference); + CreateCalculationProperty(kpiSource.AmoGoalCalculationProperty, kpiSource.GoalCalculationReference); + CreateCalculationProperty(kpiSource.AmoStatusCalculationProperty, kpiSource.StatusCalculationReference); + //CreateCalculationProperty(kpiSource.AmoTrendCalculationProperty, kpiSource.TrendCalculationReference); + CreateCalculationProperty(kpiSource.AmoKpiCalculationProperty, kpiSource.KpiCalculationReference); + + // shell model + _kpis.Add(new Kpi(this, kpiSource.TableName, kpiSource.Name, kpiSource.Expression, kpiSource.GoalMeasure, kpiSource.StatusMeasure, kpiSource.TrendMeasure, kpiSource.StatusGraphic, kpiSource.TrendGraphic)); //, amoKpiTarget)); + } + + /// + /// Update KPI associated with the TabularModel object. + /// + /// KPI object from the source tabular model to be updated in the target. + /// KPI object in the target tabular model to be updated. + public void UpdateKpi(Kpi kpiSource, Kpi kpiTarget) + { + //base measure + DeleteCalculationProperty(kpiTarget.CalculationReference); + CreateCalculationProperty(kpiSource.AmoCalculationProperty, kpiSource.CalculationReference); + kpiTarget.Expression = kpiSource.Expression; + + //goal + DeleteCalculationProperty(kpiTarget.GoalCalculationReference); + CreateCalculationProperty(kpiSource.AmoGoalCalculationProperty, kpiSource.GoalCalculationReference); + kpiTarget.GoalMeasure.Expression = kpiSource.GoalMeasure.Expression; + + //status + DeleteCalculationProperty(kpiTarget.StatusCalculationReference); + CreateCalculationProperty(kpiSource.AmoStatusCalculationProperty, kpiSource.StatusCalculationReference); + kpiTarget.StatusMeasure.Expression = kpiSource.StatusMeasure.Expression; + + if (_amoDatabase.CompatibilityLevel < 1103) + { + //trend + DeleteCalculationProperty(kpiTarget.TrendCalculationReference); + CreateCalculationProperty(kpiSource.AmoTrendCalculationProperty, kpiSource.TrendCalculationReference); + kpiTarget.TrendMeasure.Expression = kpiSource.TrendMeasure.Expression; + } + + //kpi calc ref + DeleteCalculationProperty(kpiTarget.KpiCalculationReference); + CreateCalculationProperty(kpiSource.AmoKpiCalculationProperty, kpiSource.KpiCalculationReference); + } + + #endregion + + #region Actions + + /// + /// Delete action associated with the TabularModel object. + /// + /// The id of the action to be deleted. + public void DeleteAction(string id) + { + if (_amoDatabase.Cubes.Count > 0) + { + if (_amoDatabase.Cubes[0].Actions.Contains(id)) + { + _amoDatabase.Cubes[0].Actions.Remove(id); + } + } + + // shell model + if (_actions.ContainsId(id)) + { + _actions.RemoveById(id); + } + } + + /// + /// Create action associated with the TabularModel object. + /// + /// Action object from the source tabular model to be created in the target. + public void CreateAction(Action actionSource) + { + if (_amoDatabase.Cubes.Count > 0) + { + + Microsoft.AnalysisServices.Action amoActionTarget = actionSource.AmoAction.Clone(); + + // Need to check if there is an existing Action with same ID (some clever clogs might have renamed the object in source and kept same ID). If so, replace it with a new one and store it as substitute ID in source. + if (_amoDatabase.Cubes[0].Actions.Contains(actionSource.Id)) + { + amoActionTarget.ID = Convert.ToString(Guid.NewGuid()); + actionSource.SubstituteId = amoActionTarget.ID; + } + + _amoDatabase.Cubes[0].Actions.Add(amoActionTarget); + + // shell model + _actions.Add(new Action(this, amoActionTarget)); + } + } + + /// + /// Update action associated with the TabularModel object. + /// + /// Action object from the source tabular model to be updated in the target. + /// Action object in the target tabular model to be updated. + public void MergeAction(Action ActionSource, Action ActionTarget) + { + if (ActionSource.Id != ActionTarget.Id) + { + // If the names are the same, but the IDs are different, need to store the ID from the target in the source Action so that when Create/Update subsequent tables (partitions, DSVs and special dimension properties), we know to substitute the Action ID + ActionSource.SubstituteId = ActionTarget.Id; + } + + DeleteAction(ActionTarget.Id); + CreateAction(ActionSource); + } + + #endregion + + #region Perspectives + + /// + /// Delete perspective associated with the TabularModel object. + /// + /// The id of the perspective to be deleted. + public void DeletePerspective(string id) + { + if (_amoDatabase.Cubes[0].Perspectives.Contains(id)) + { + _amoDatabase.Cubes[0].Perspectives.Remove(id); + } + + // shell model + if (_perspectives.ContainsId(id)) + { + _perspectives.RemoveById(id); + } + } + + /// + /// Create perspective associated with the TabularModel object. + /// + /// Perspective object from the source tabular model to be created in the target. + public void CreatePerspective(Perspective perspectiveSource) + { + if (_amoDatabase.Cubes.Count > 0) + { + //easier to just create a copy rather than clone ... + + Microsoft.AnalysisServices.Perspective amoPerspectiveTarget = _amoDatabase.Cubes[0].Perspectives.Add(perspectiveSource.Name); + + //Tables + foreach (PerspectiveDimension perspectiveDimensionSource in perspectiveSource.AmoPerspective.Dimensions) + { + Table tableTarget = _tables.FindByName(perspectiveDimensionSource.Dimension.Name); + + if (tableTarget != null) + { + PerspectiveDimension perspectiveDimensionTarget = amoPerspectiveTarget.Dimensions.Add(tableTarget.AmoDimension.ID); + + //Columns + foreach (PerspectiveAttribute perspectiveAttributeSource in perspectiveDimensionSource.Attributes) + { + DimensionAttribute dimensionAttributeTarget = tableTarget.AmoDimension.Attributes.FindByName(perspectiveAttributeSource.Attribute.Name); + + if (dimensionAttributeTarget != null) + { + perspectiveDimensionTarget.Attributes.Add(dimensionAttributeTarget.ID); + } + } + + //Hierarchies + foreach (PerspectiveHierarchy perspectiveHierarchySource in perspectiveDimensionSource.Hierarchies) + { + Hierarchy hierarchyTarget = tableTarget.AmoDimension.Hierarchies.FindByName(perspectiveHierarchySource.Hierarchy.Name); + + if (hierarchyTarget != null) + { + perspectiveDimensionTarget.Hierarchies.Add(hierarchyTarget.ID); + } + } + } + } + + //Measures + foreach (PerspectiveCalculation perspectiveCalculationSource in perspectiveSource.AmoPerspective.Calculations) + { + string measureName = perspectiveCalculationSource.Name.Replace("[Measures].[", "").Replace("]", ""); + + if (perspectiveSource.ParentTabularModel.Measures.ContainsName(measureName)) // this if clause shouldn't be necessary, but it is + { + Measure measureTarget = _measures.FindByName(measureName); + + if (measureTarget != null) + { + amoPerspectiveTarget.Calculations.Add(perspectiveCalculationSource.Name); + } + } + } + + //KPIs + foreach (PerspectiveKpi perspectiveKpiSource in perspectiveSource.AmoPerspective.Kpis) + { + string KpiName = perspectiveKpiSource.ToString(); + + if (perspectiveSource.ParentTabularModel.Kpis.ContainsName(KpiName)) + { + Kpi kpiTarget = _kpis.FindByName(KpiName); + + if (kpiTarget != null) + { + amoPerspectiveTarget.Kpis.Add(perspectiveKpiSource.ToString()); + } + } + } + + //Actions + foreach (PerspectiveAction perspectiveActionSource in perspectiveSource.AmoPerspective.Actions) + { + if (perspectiveActionSource.ParentCube.Actions.Contains(perspectiveActionSource.ActionID)) //need this check or .Action returns error + { + string actionName = perspectiveActionSource.Action.Name; + + if (perspectiveSource.ParentTabularModel.Actions.ContainsName(actionName)) + { + Action actionTarget = _actions.FindByName(actionName); + + if (actionTarget != null) + { + amoPerspectiveTarget.Actions.Add(actionTarget.Id); + } + } + } + } + + //Translations + foreach (Translation perspectiveTranslationSource in perspectiveSource.AmoPerspective.Translations) + { + Translation perspectiveTranslationTarget = perspectiveTranslationSource.Clone(); + amoPerspectiveTarget.Translations.Add(perspectiveTranslationTarget); + } + + // shell model + _perspectives.Add(new Perspective(this, amoPerspectiveTarget)); + } + } + + /// + /// Update perspective associated with the TabularModel object. + /// + /// Perspective object from the source tabular model to be updated in the target. + /// Perspective object in the target tabular model to be updated. + public void UpdatePerspective(Perspective perspectiveSource, Perspective perspectiveTarget) + { + if (_comparisonInfo.OptionsInfo.OptionMergePerspectives) + { + //Tables + foreach (PerspectiveDimension perspectiveDimensionSource in perspectiveSource.AmoPerspective.Dimensions) + { + PerspectiveDimension perspectiveDimensionTarget = null; + + //is this table selected in the target perspective? + foreach (PerspectiveDimension perspectiveDimensionTarget2 in perspectiveTarget.AmoPerspective.Dimensions) + { + if (perspectiveDimensionTarget2.Dimension.Name == perspectiveDimensionSource.Dimension.Name) + { + perspectiveDimensionTarget = perspectiveDimensionTarget2; + break; + } + } + + //If perspectiveDimensionTarget == null, then this table is not selected in the target perspective. But does it exist in the target db? if so, we should select it. + if (perspectiveDimensionTarget == null && _amoDatabase.Dimensions.ContainsName(perspectiveDimensionSource.Dimension.Name)) + { + perspectiveDimensionTarget = perspectiveTarget.AmoPerspective.Dimensions.Add(_amoDatabase.Dimensions.FindByName(perspectiveDimensionSource.Dimension.Name).ID); + } + + //if perspectiveDimensionTarget is still null here then we don't have a matching table in the target perspective at all, and we can move onto the next table + if (perspectiveDimensionTarget != null) + { + //Columns + foreach (PerspectiveAttribute perspectiveAttributeSource in perspectiveDimensionSource.Attributes) + { + PerspectiveAttribute perspectiveAttributeTarget = null; + + foreach (PerspectiveAttribute perspectiveAttributeTarget2 in perspectiveDimensionTarget.Attributes) + { + if (perspectiveAttributeTarget2.Attribute.Name == perspectiveAttributeSource.Attribute.Name) + { + perspectiveAttributeTarget = perspectiveAttributeTarget2; + break; + } + } + + if (perspectiveAttributeTarget == null) + { + //There is no selection in the target dim for this attribute. Is there an attribute in the target dim with the same name? + if (perspectiveDimensionTarget.Dimension.Attributes.ContainsName(perspectiveAttributeSource.Attribute.Name)) + { + perspectiveAttributeTarget = perspectiveDimensionTarget.Attributes.Add(perspectiveDimensionTarget.Dimension.Attributes.FindByName(perspectiveAttributeSource.Attribute.Name).ID); + } + else break; //attribute doesn't exist in target dim, so move onto the next attribute + } + } + + //Hierarchies + foreach (PerspectiveHierarchy perspectiveHierarchySource in perspectiveDimensionSource.Hierarchies) + { + PerspectiveHierarchy perspectiveHierarchyTarget = null; + + foreach (PerspectiveHierarchy perspectiveHierarchyTarget2 in perspectiveDimensionTarget.Hierarchies) + { + if (perspectiveHierarchyTarget2.Hierarchy.Name == perspectiveHierarchySource.Hierarchy.Name) + { + perspectiveHierarchyTarget = perspectiveHierarchyTarget2; + break; + } + } + + if (perspectiveHierarchyTarget == null) + { + //There is no selection in the target dim for this hierarchy. Is there a hierarchy in the target dim with the same name? + if (perspectiveDimensionTarget.Dimension.Hierarchies.ContainsName(perspectiveHierarchySource.Hierarchy.Name)) + { + perspectiveHierarchyTarget = perspectiveDimensionTarget.Hierarchies.Add(perspectiveDimensionTarget.Dimension.Hierarchies.FindByName(perspectiveHierarchySource.Hierarchy.Name).ID); + } + else break; //hierarchy doesn't exist in target dim, so move onto the next hierarchy + } + } + } + } + + //Measures + foreach (PerspectiveCalculation perspectiveCalculationSource in perspectiveSource.AmoPerspective.Calculations) + { + PerspectiveCalculation perspectiveCalculationTarget = null; + + foreach (PerspectiveCalculation perspectiveCalculationTarget2 in perspectiveTarget.AmoPerspective.Calculations) + { + if (perspectiveCalculationTarget2.Name == perspectiveCalculationSource.Name) + { + perspectiveCalculationTarget = perspectiveCalculationTarget2; + break; + } + } + + if (perspectiveCalculationTarget == null) + { + //There is no selection in the target db for this calculation. Is there a calculation in the target db with the same name? + if (perspectiveTarget.ParentTabularModel.Measures.ContainsName(perspectiveCalculationSource.Name.Replace("[Measures].[", "").Replace("]", ""))) + { + perspectiveCalculationTarget = perspectiveTarget.AmoPerspective.Calculations.Add(perspectiveCalculationSource.Name); + } + } + } + + //Kpis + foreach (PerspectiveKpi perspectiveKpiSource in perspectiveSource.AmoPerspective.Kpis) + { + PerspectiveKpi perspectiveKpiTarget = null; + + foreach (PerspectiveKpi perspectiveKpiTarget2 in perspectiveTarget.AmoPerspective.Kpis) + { + if (perspectiveKpiTarget2.ToString() == perspectiveKpiSource.ToString()) + { + perspectiveKpiTarget = perspectiveKpiTarget2; + break; + } + } + + if (perspectiveKpiTarget == null) + { + //There is no selection in the target db for this Kpi. Is there a Kpi in the target db with the same name? + if (perspectiveTarget.ParentTabularModel.Kpis.ContainsName(perspectiveKpiSource.ToString())) + { + perspectiveKpiTarget = perspectiveTarget.AmoPerspective.Kpis.Add(perspectiveKpiSource.ToString()); + } + } + } + + //Actions + foreach (PerspectiveAction perspectiveActionSource in perspectiveSource.AmoPerspective.Actions) + { + if (perspectiveActionSource.ParentCube.Actions.Contains(perspectiveActionSource.ActionID)) //need this check or .Action returns error + { + PerspectiveAction perspectiveActionTarget = null; + + foreach (PerspectiveAction perspectiveActionTarget2 in perspectiveTarget.AmoPerspective.Actions) + { + if (perspectiveActionTarget2.ParentCube.Actions.Contains(perspectiveActionTarget2.ActionID) && //need this check or .Action returns error + perspectiveActionTarget2.Action.Name == perspectiveActionSource.Action.Name) + { + perspectiveActionTarget = perspectiveActionTarget2; + break; + } + } + + if (perspectiveActionTarget == null) + { + //There is no selection in the target db for this Action. Is there an action in the target db with the same name? + if (perspectiveTarget.ParentTabularModel.Actions.ContainsName(perspectiveActionSource.Action.Name)) + { + perspectiveActionTarget = perspectiveTarget.AmoPerspective.Actions.Add(_amoDatabase.Cubes[0].Actions.FindByName(perspectiveActionSource.Action.Name).ID); + } + } + } + } + + //Translations + foreach (Translation perspectiveTranslationSource in perspectiveSource.AmoPerspective.Translations) + { + if (perspectiveTarget.AmoPerspective.Translations.Contains(perspectiveTranslationSource.Language)) + { + perspectiveTarget.AmoPerspective.Translations.FindByLanguage(perspectiveTranslationSource.Language).Caption = perspectiveTranslationSource.Caption; + } + else + { + Translation perspectiveTranslationTarget = perspectiveTranslationSource.Clone(); + perspectiveTarget.AmoPerspective.Translations.Add(perspectiveTranslationTarget); + } + } + } + else + { + if (perspectiveSource.Id != perspectiveTarget.Id) + { + // If the names are the same, but the IDs are different, need to store the ID from the target in the source perspective so that when Create/Update subsequent tables (partitions, DSVs and special dimension properties), we know to substitute the Perspective ID + perspectiveSource.SubstituteId = perspectiveTarget.Id; + } + + DeletePerspective(perspectiveTarget.Id); + CreatePerspective(perspectiveSource); + } + } + + #endregion + + #region Roles + + /// + /// Delete role associated with the TabularModel object. + /// + /// The id of the role to be deleted. + public void DeleteRole(string id) + { + if (_amoDatabase.Roles.Contains(id)) + { + _amoDatabase.Roles.Remove(id); + } + + // Cube permissions + if (_amoDatabase.Cubes.Count > 0) + { + List cubePermissionIdsToDelete = new List(); + + foreach (CubePermission cubePermission in _amoDatabase.Cubes[0].CubePermissions) + { + if (cubePermission.RoleID == id) + { + cubePermissionIdsToDelete.Add(cubePermission.ID); + } + } + + foreach (string cubePermissionIdToDelete in cubePermissionIdsToDelete) + { + _amoDatabase.Cubes[0].CubePermissions.Remove(cubePermissionIdToDelete); + } + } + + // Dimension permissions + foreach (Dimension dim in _amoDatabase.Dimensions) + { + List dimPermissionIdsToDelete = new List(); + + foreach (DimensionPermission dimPermission in dim.DimensionPermissions) + { + if (dimPermission.RoleID == id) + { + dimPermissionIdsToDelete.Add(dimPermission.ID); + } + } + + foreach (string dimPermissionIdToDelete in dimPermissionIdsToDelete) + { + dim.DimensionPermissions.Remove(dimPermissionIdToDelete); + } + } + + // Database permissions + List dbPermissionIdsToDelete = new List(); + + foreach (DatabasePermission dbPermission in _amoDatabase.DatabasePermissions) + { + if (dbPermission.RoleID == id) + { + dbPermissionIdsToDelete.Add(dbPermission.ID); + } + } + + foreach (string dbPermissionIdToDelete in dbPermissionIdsToDelete) + { + _amoDatabase.DatabasePermissions.Remove(dbPermissionIdToDelete); + } + + // shell model + if (_roles.ContainsId(id)) + { + _roles.RemoveById(id); + } + } + + /// + /// Create role associated with the TabularModel object. + /// + /// Role object from the source tabular model to be created in the target. + public void CreateRole(Role roleSource) + { + Microsoft.AnalysisServices.Role amoRoleTarget = roleSource.AmoRole.Clone(); + + // Need to check if there is an existing role with same ID (some clever clogs might have renamed the object in source and kept same ID). If so, replace it with a new one and store it as substitute ID in source. + if (_amoDatabase.Roles.Contains(roleSource.Id)) + { + amoRoleTarget.ID = Convert.ToString(Guid.NewGuid()); + roleSource.SubstituteId = amoRoleTarget.ID; + } + + // Database permissions + foreach (DatabasePermission dbPermissionSource in roleSource.ParentTabularModel.AmoDatabase.DatabasePermissions) + { + if (dbPermissionSource.RoleID == roleSource.Id) + { + DatabasePermission dbPermissionTarget = dbPermissionSource.Clone(); + dbPermissionTarget.RoleID = amoRoleTarget.ID; + if (_amoDatabase.DatabasePermissions.Contains(dbPermissionTarget.ID)) + { + dbPermissionTarget.ID = Convert.ToString(Guid.NewGuid()); + } + if (_amoDatabase.DatabasePermissions.ContainsName(dbPermissionTarget.Name)) + { + if (_amoDatabase.DatabasePermissions.ContainsName(dbPermissionTarget.ID)) + { + dbPermissionTarget.Name = Convert.ToString(Guid.NewGuid()); + } + else + { + dbPermissionTarget.Name = dbPermissionTarget.ID; + } + } + _amoDatabase.DatabasePermissions.Add(dbPermissionTarget); + } + } + + // Dimension permissions + foreach (Dimension dimSource in roleSource.ParentTabularModel.AmoDatabase.Dimensions) + { + Dimension dimTarget = _amoDatabase.Dimensions.FindByName(dimSource.Name); + if (dimTarget != null) + { + foreach (DimensionPermission dimPermissionSource in dimSource.DimensionPermissions) + { + if (dimPermissionSource.RoleID == roleSource.Id) + { + DimensionPermission dimPermissionTarget = dimPermissionSource.Clone(); + dimPermissionTarget.RoleID = amoRoleTarget.ID; + if (dimSource.DimensionPermissions.Contains(dimPermissionTarget.ID)) + { + dimPermissionTarget.ID = Convert.ToString(Guid.NewGuid()); + } + if (!dimTarget.DimensionPermissions.ContainsName(dimPermissionTarget.Name)) + dimTarget.DimensionPermissions.Add(dimPermissionTarget); + } + } + } + } + + // Cube permissions + if (roleSource.ParentTabularModel.AmoDatabase.Cubes.Count > 0 && _amoDatabase.Cubes.Count > 0) + { + foreach (CubePermission cubePermissionSource in roleSource.ParentTabularModel.AmoDatabase.Cubes[0].CubePermissions) + { + if (cubePermissionSource.RoleID == roleSource.Id) + { + CubePermission cubePermissionTarget = cubePermissionSource.Clone(); + cubePermissionTarget.RoleID = amoRoleTarget.ID; + if (_amoDatabase.Cubes[0].CubePermissions.Contains(cubePermissionTarget.ID)) + { + cubePermissionTarget.ID = Convert.ToString(Guid.NewGuid()); + } + if (_amoDatabase.Cubes[0].CubePermissions.ContainsName(cubePermissionTarget.Name)) + { + if (_amoDatabase.Cubes[0].CubePermissions.ContainsName(cubePermissionTarget.ID)) + { + cubePermissionTarget.Name = Convert.ToString(Guid.NewGuid()); + } + else + { + cubePermissionTarget.Name = cubePermissionTarget.ID; + } + } + _amoDatabase.Cubes[0].CubePermissions.Add(cubePermissionTarget); + } + } + } + + _amoDatabase.Roles.Add(amoRoleTarget); + + // shell model + _roles.Add(new Role(this, amoRoleTarget)); + } + + /// + /// Update role associated with the TabularModel object. + /// + /// Role object from the source tabular model to be updated in the target. + /// Role object in the target tabular model to be updated. + public void UpdateRole(Role roleSource, Role roleTarget) + { + if (roleSource.Id != roleTarget.Id) + { + // If the names are the same, but the IDs are different, need to store the ID from the target in the source role so that when Create/Update subsequent tables (partitions, DSVs and special dimension properties), we know to substitute the Role ID + roleSource.SubstituteId = roleTarget.Id; + } + + DeleteRole(roleTarget.Id); + CreateRole(roleSource); + } + + #endregion + + /// + /// Delete calculation property associated with the TabularModel object. + /// + /// The calculation reference to be deleted. + public void DeleteCalculationProperty(string calculationReference) + { + if (_amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(calculationReference)) + { + _amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Remove(calculationReference); + } + } + + /// + /// Create calculation property associated with the TabularModel object. + /// + /// Calculation property object from the source tabular model to be created in the target. + /// Calculation reference from the source tabular model. + public void CreateCalculationProperty(CalculationProperty calculationPropertySource, string calculationReference) + { + if (_amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(calculationReference)) + { + _amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Remove(calculationReference); + } + + if (calculationPropertySource == null) + { + _amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Add(calculationReference); + } + else + { + _amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Add(calculationPropertySource.Clone()); + } + } + + /// + /// Populate MDX script in target tabular model based on Measures collection. + /// + public void PopulateMdxScript() + { + if (_amoDatabase.Cubes.Count > 0 && _amoDatabase.Cubes[0].MdxScripts[0].Commands.Count > 0) + { + if (_amoDatabase.CompatibilityLevel >= 1103) + { + // since sp1, each measure gets its own command, so need to clear out and recreate them + + // delete all commands except the first one + for (int i = _amoDatabase.Cubes[0].MdxScripts[0].Commands.Count - 1; i >= 0; i--) + { + //following is a clumsy check. We don't want to mess with the command containing "CREATE MEMBER CURRENTCUBE.Measures.[__No measures defined] AS 1;" + if (_amoDatabase.Cubes[0].MdxScripts[0].Commands[i].Text == "" || _amoDatabase.Cubes[0].MdxScripts[0].Commands[i].Text.Contains("-- PowerPivot measures command (do not modify manually) --")) + { + _amoDatabase.Cubes[0].MdxScripts[0].Commands.RemoveAt(i); + } + } + + foreach (Measure measure in this.Measures) + { + CreateMdxScriptCommand(measure); + } + foreach (Kpi kpi in _kpis) + { + CreateMdxScriptCommand(kpi); + } + } + #region pre SP1 + else if (_amoDatabase.Cubes[0].MdxScripts[0].Commands.Count > 1) + { + //pre sp1 + StringBuilder measuresCommand = new StringBuilder(); + + measuresCommand.AppendLine("----------------------------------------------------------"); + measuresCommand.AppendLine("-- PowerPivot measures command (do not modify manually) --"); + measuresCommand.AppendLine("----------------------------------------------------------"); + measuresCommand.AppendLine(""); + measuresCommand.AppendLine(""); + + foreach (Measure measure in this.Measures) + { + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", measure.TableName, measure.Name.Replace("]", "]]"), measure.Expression)); + } + + foreach (Kpi kpi in _kpis) + { + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", kpi.TableName, kpi.Name, kpi.Expression)); + measuresCommand.AppendLine(String.Format("CREATE MEMBER CURRENTCUBE.Measures.[{0}] AS '{1}', ASSOCIATED_MEASURE_GROUP = '{2}';", kpi.GoalMeasure.Name, kpi.GoalMeasure.Expression, kpi.GoalMeasure.TableName)); + measuresCommand.AppendLine(String.Format("CREATE MEMBER CURRENTCUBE.Measures.[{0}] AS '{1}', ASSOCIATED_MEASURE_GROUP = '{2}';", kpi.StatusMeasure.Name, kpi.StatusMeasure.Expression, kpi.StatusMeasure.TableName)); + measuresCommand.AppendLine(String.Format("CREATE MEMBER CURRENTCUBE.Measures.[{0}] AS '{1}', ASSOCIATED_MEASURE_GROUP = '{2}';", kpi.TrendMeasure.Name, kpi.TrendMeasure.Expression, kpi.TrendMeasure.TableName)); + //use MDX script method for KPIs, not object model (as does latest version of Tabular Editor): + measuresCommand.AppendLine(String.Format("CREATE KPI CURRENTCUBE.[{0}] AS Measures.[{0}], ASSOCIATED_MEASURE_GROUP = '{1}', GOAL = Measures.[{2}], STATUS = Measures.[{3}], TREND = Measures.[{4}], STATUS_GRAPHIC = '{5}', TREND_GRAPHIC = '{6}';", + kpi.Name, kpi.TableName, kpi.GoalMeasure.Name, kpi.StatusMeasure.Name, kpi.TrendMeasure.Name, kpi.StatusGraphic, kpi.TrendGraphic)); + + //It is possible (if there were existing, unchanged KPIs in target that use AMO, rather than script declaration) that this KPI is missing its calculation property + if (!_amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(kpi.KpiCalculationReference)) + { + CreateCalculationProperty(kpi.AmoKpiCalculationProperty, kpi.KpiCalculationReference); + } + } + + _amoDatabase.Cubes[0].MdxScripts[0].Commands[1].Text = measuresCommand.ToString(); + } + #endregion + + //Clear AMO KPIs collection (we are using MDX script declared KPIs, not AMO) just in case this cube happened to have them (don't want both versions). + _amoDatabase.Cubes[0].Kpis.Clear(); + } + } + + private void CreateMdxScriptCommand(Measure measure) + { + StringBuilder measuresCommand = new StringBuilder(); + + measuresCommand.AppendLine("----------------------------------------------------------"); + measuresCommand.AppendLine("-- PowerPivot measures command (do not modify manually) --"); + measuresCommand.AppendLine("----------------------------------------------------------"); + measuresCommand.AppendLine(""); + measuresCommand.AppendLine(""); + + if (measure is Kpi) + { + Kpi kpi = (Kpi)measure; + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", kpi.TableName, kpi.Name, kpi.Expression)); + + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", kpi.TableName, kpi.GoalMeasure.Name, kpi.GoalMeasure.Expression)); + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", kpi.TableName, kpi.StatusMeasure.Name, kpi.StatusMeasure.Expression)); + //measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", kpi.TableName, kpi.TrendMeasure.Name, kpi.TrendMeasure.Expression)); + + //use MDX script method for KPIs, not object model (as does the Tabular Editor since RC0): + measuresCommand.AppendLine(String.Format("CREATE KPI CURRENTCUBE.[{0}] AS Measures.[{0}], ASSOCIATED_MEASURE_GROUP = '{1}', GOAL = Measures.[{2}], STATUS = Measures.[{3}], STATUS_GRAPHIC = '{4}';", + kpi.Name, kpi.TableName, kpi.GoalMeasure.Name, kpi.StatusMeasure.Name, kpi.StatusGraphic)); + + //It is possible (if there were existing, unchanged KPIs in target that use AMO, rather than script declaration) that this KPI is missing its calculation property + if (!_amoDatabase.Cubes[0].MdxScripts[0].CalculationProperties.Contains(kpi.KpiCalculationReference)) + { + CreateCalculationProperty(kpi.AmoKpiCalculationProperty, kpi.KpiCalculationReference); + } + } + else + { + //regular measure + measuresCommand.AppendLine(String.Format("CREATE MEASURE '{0}'[{1}]={2}", measure.TableName, measure.Name, measure.Expression)); + } + + //add on sp1 annotations fluff + Microsoft.AnalysisServices.Command cmdToAdd = new Microsoft.AnalysisServices.Command(measuresCommand.ToString()); + cmdToAdd.Annotations.Add("FullName", measure.Name.Replace("]]", "]")); + cmdToAdd.Annotations.Add("Table", measure.TableName); + + _amoDatabase.Cubes[0].MdxScripts[0].Commands.Add(cmdToAdd); + } + + /// + /// Final checks and cleanup to ensure referential integrity between objects. + /// + public void FinalCleanup() + { + //check for database permissions to non-existing roles. Cannot do this when creating/updating dimensions because roles not yet created/deleted. + foreach (Dimension dimension in _amoDatabase.Dimensions) + { + List dimensionPermissionIdsToDelete = new List(); + + foreach (DimensionPermission dimensionPermission in dimension.DimensionPermissions) + { + if (!_amoDatabase.Roles.Contains(dimensionPermission.RoleID)) + { + dimensionPermissionIdsToDelete.Add(dimensionPermission.ID); + } + } + + foreach (string dimensionPermissionIdToDelete in dimensionPermissionIdsToDelete) + { + dimension.DimensionPermissions.Remove(dimensionPermissionIdToDelete); + } + } + + //check for redundant cube + if (_amoDatabase.Dimensions.Count == 0) + { + _amoDatabase.Cubes.Clear(); + _amoDatabase.DataSourceViews.Clear(); + } + } + + #endregion + + /// + /// Update target tabular model with changes resulting from the comparison. For database deployment, this will fire DeployDatabaseCallBack. + /// + /// Boolean indicating whether update was successful. + public bool Update() + { + if (_connectionInfo.UseProject) + { + UpdateProject(); + } + else //Database deployement + { + if (_comparisonInfo.PromptForDatabaseProcessing) + { + //Call back to show deployment form + DatabaseDeploymentEventArgs args = new DatabaseDeploymentEventArgs(); + _parentComparison.OnDatabaseDeployment(args); + return args.DeploymentSuccessful; + } + else + { + //Simple update target without setting passwords or processing + _amoDatabase.Update(UpdateOptions.ExpandFull); + } + } + + return true; + } + + private void UpdateProject() + { + _amoDatabase.Update(UpdateOptions.ExpandFull); + + if (_connectionInfo.Project != null) + { + EnvDTE._DTE dte = _connectionInfo.Project.DTE; + + //check out bim file if necessary + if (dte.SourceControl.IsItemUnderSCC(_connectionInfo.BimFileFullName) && !dte.SourceControl.IsItemCheckedOut(_connectionInfo.BimFileFullName)) + { + dte.SourceControl.CheckOutItem(_connectionInfo.BimFileFullName); + } + } + + //Script out db and write to project file + string xml = ScriptDatabase(toOverwriteProjectBimFile: true); + + //replace db name with "SemanticModel" + XmlDocument bimFileDoc = new XmlDocument(); + bimFileDoc.LoadXml(xml); + XmlNamespaceManager nsmgr = new XmlNamespaceManager(bimFileDoc.NameTable); + nsmgr.AddNamespace("myns1", "http://schemas.microsoft.com/analysisservices/2003/engine"); + XmlNode objectDefinitionDatabaseNameNode = bimFileDoc.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:Name", nsmgr); + objectDefinitionDatabaseNameNode.InnerText = "SemanticModel"; + + xml = WriteXmlFromDoc(bimFileDoc); + File.WriteAllText(_connectionInfo.BimFileFullName, xml); + } + + #region Database deployment and processing methods + + private const string _deployRowWorkItem = "Deploy metadata"; + private ProcessingTableCollection _tablesToProcess; + + /// + /// Perform database deployment and processing of required tables. + /// + /// Tables to process. + public void DatabaseDeployAndProcess(ProcessingTableCollection tablesToProcess) + { + try + { + _tablesToProcess = tablesToProcess; + + //Set passwords ready for processing + foreach (Microsoft.AnalysisServices.DataSource dataSource in _amoDatabase.DataSources) + { + if (dataSource.ImpersonationInfo != null && dataSource.ImpersonationInfo.ImpersonationMode == ImpersonationMode.ImpersonateAccount) + { + PasswordPromptEventArgs args = new PasswordPromptEventArgs(); + args.DataSourceName = dataSource.Name; + args.Username = dataSource.ImpersonationInfo.Account; + _parentComparison.OnPasswordPrompt(args); + if (args.UserCancelled) + { + // Show cancelled for all rows + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Deployment has been cancelled.", DeploymentStatus.Cancel)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Cancelled", DeploymentStatus.Cancel)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Cancel, null)); + return; + } + dataSource.ImpersonationInfo.Account = args.Username; + dataSource.ImpersonationInfo.Password = args.Password; + } + } + + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + try + { + _amoServer.RollbackTransaction(); + } + catch { } + _amoServer.BeginTransaction(); + } + + _amoDatabase.Update(UpdateOptions.ExpandFull); + + if (!_comparisonInfo.OptionsInfo.OptionTransaction) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Success. Metadata deployed.", DeploymentStatus.Success)); + } + + if (_tablesToProcess.Count > 0) + { + ProcessAsyncDelegate processAsyncCaller = new ProcessAsyncDelegate(Process); + processAsyncCaller.BeginInvoke(null, null); + } + else + { + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + _amoServer.CommitTransaction(); + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Success. Metadata deployed.", DeploymentStatus.Success)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Success, null)); + } + } + catch (Exception exc) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Error deploying metadata.", DeploymentStatus.Error)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Error", DeploymentStatus.Error)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + } + + private bool _stopProcessing; + string _sessionId; + internal delegate void ProcessAsyncDelegate(); + private void Process() + { + //string x13 = TraceColumn.ObjectName.ToString(); + //string x15 = TraceColumn.ObjectReference.ToString(); + //string x10 = TraceColumn.IntegerData.ToString(); + + try + { + _stopProcessing = false; + ProcessType processType = _comparisonInfo.OptionsInfo.OptionProcessingOption == ProcessingOption.Default ? ProcessType.ProcessDefault : ProcessType.ProcessFull; + + //Set up server trace to capture how many rows processed + _sessionId = _amoServer.SessionID; + Trace trace = _amoServer.Traces.Add(); + TraceEvent traceEvent = trace.Events.Add(TraceEventClass.ProgressReportCurrent); + traceEvent.Columns.Add(TraceColumn.ObjectID); + traceEvent.Columns.Add(TraceColumn.ObjectName); + traceEvent.Columns.Add(TraceColumn.ObjectReference); + traceEvent.Columns.Add(TraceColumn.IntegerData); + traceEvent.Columns.Add(TraceColumn.SessionID); + traceEvent.Columns.Add(TraceColumn.Spid); + trace.Update(); + trace.OnEvent += new TraceEventHandler(Trace_OnEvent); + trace.Start(); + + _amoServer.CaptureXml = true; + + if (_comparisonInfo.OptionsInfo.OptionAffectedTables) + { + foreach (ProcessingTable tableToProcess in _tablesToProcess) + { + Table table = this.Tables.FindByName(tableToProcess.Name); + if (table != null && table.AmoDimension.CanProcess(processType)) + { + table.AmoDimension.Process(processType); + } + } + } + else + { + _amoDatabase.Process(processType); + } + + _amoServer.CaptureXml = false; + + XmlaResultCollection results = _amoServer.ExecuteCaptureLog(true, true); + + try + { + trace.Stop(); + trace.Drop(); + } + catch { } + + string errorMessage = ""; + foreach (XmlaResult result in results) + { + foreach (XmlaMessage message in result.Messages) + { + if (message is XmlaError) + { + errorMessage += message.Description + System.Environment.NewLine; + } + } + } + + if (errorMessage != "") + { + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, errorMessage)); + } + else + { + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + if (_stopProcessing) + { + //already dealt with rolling back tran and error messages + return; + } + else + { + _amoServer.CommitTransaction(); + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Success. Metadata deployed.", DeploymentStatus.Success)); + } + } + + // Show row count for each table + foreach (ProcessingTable table in _tablesToProcess) + { + int rowCount = _connectionInfo.DirectQuery ? 0 : Core.Comparison.FindRowCount(_amoServer, table.Name, _amoDatabase.Name); + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Success. " + String.Format("{0:#,###0}", rowCount) + " rows transferred.", DeploymentStatus.Success)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Success, null)); + } + } + catch (Exception exc) + { + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + } + + /// + /// Stop processing if possible. + /// + public void StopProcessing() + { + _stopProcessing = true; + + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + try + { + _amoServer.RollbackTransaction(); + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, "Rolled back transaction.")); + } + catch (Exception exc) + { + if (exc is NullReferenceException || exc is InvalidOperationException) + { + return; + } + else + { + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + } + } + } + + private void Trace_OnEvent(object sender, TraceEventArgs e) + { + if (e.ObjectName != null && e.ObjectReference != null && e.SessionID == _sessionId) + { + XmlDocument document = new XmlDocument(); + document.LoadXml(e.ObjectReference); + + XmlNodeList partitionIdNodeList = document.GetElementsByTagName("PartitionID"); + XmlNodeList measureGroupIdNodeList = document.GetElementsByTagName("MeasureGroupID"); + + if (partitionIdNodeList != null && measureGroupIdNodeList != null) + { + if (_tablesToProcess.ContainsId(measureGroupIdNodeList[0].InnerText)) + { + ProcessingTable table = _tablesToProcess.FindById(measureGroupIdNodeList[0].InnerText); + + if (!table.ContainsPartition(partitionIdNodeList[0].InnerText)) + { + table.Partitions.Add(new PartitionRowCounter(partitionIdNodeList[0].InnerText)); + } + + PartitionRowCounter partition = table.FindPartition(partitionIdNodeList[0].InnerText); + partition.RowCount = e.IntegerData; + + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Retreived " + String.Format("{0:#,###0}", table.GetRowCount()) + " rows ...", DeploymentStatus.Deploying)); + } + } + + if (_stopProcessing && !_comparisonInfo.OptionsInfo.OptionTransaction) //transactions get cancelled in StopProcessing, not here + { + try + { + _amoServer.CancelCommand(_sessionId); + } + catch { } + } + + ////Doesn't work with Spid, so doing sessionid above + //int spid; + //if (_stopProcessing && int.TryParse(e.Spid, out spid)) + //{ + // try + // { + // //_amoServer.CancelCommand(e.Spid); + // string commandStatement = String.Format("{0}1", e.Spid); + // System.Diagnostics.Debug.WriteLine(commandStatement); + // _amoServer.Execute(commandStatement); + // //_connectionInfo.ExecuteXmlaCommand(_amoServer, commandStatement); + // } + // catch { } + //} + } + } + + private void ShowErrorsForAllRows() + { + // Show error for each item + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Error", DeploymentStatus.Error)); + } + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Error", DeploymentStatus.Error)); + } + } + + #endregion + + /// + /// Generate script containing full tabular model definition. + /// + /// Flag indicating whether the operation is to overwrite a project BIM file. If not, may need name substitution. + /// XMLA script of tabular model defintion. + public string ScriptDatabase(bool toOverwriteProjectBimFile = false) + { + string xml = WriteXmlFromDatabase(); + + if (_connectionInfo.UseProject) + { + //replace db/cube name/id with name of deploymnet db from the project file + XmlDocument xmlaScriptDoc = new XmlDocument(); + xmlaScriptDoc.LoadXml(xml); + XmlNamespaceManager nsmgr2 = new XmlNamespaceManager(xmlaScriptDoc.NameTable); + nsmgr2.AddNamespace("myns1", "http://schemas.microsoft.com/analysisservices/2003/engine"); + XmlNode objectDefinitionDatabaseIdNode = xmlaScriptDoc.SelectSingleNode("//myns1:Object/myns1:DatabaseID", nsmgr2); + XmlNode objectDefinitionDatabaseId2Node = xmlaScriptDoc.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:ID", nsmgr2); + XmlNode objectDefinitionDatabaseNameNode = xmlaScriptDoc.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:Name", nsmgr2); + XmlNode objectDefinitionCubeIdNode = xmlaScriptDoc.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:Cubes/myns1:Cube/myns1:ID", nsmgr2); + XmlNode objectDefinitionCubeNameNode = xmlaScriptDoc.SelectSingleNode("//myns1:ObjectDefinition/myns1:Database/myns1:Cubes/myns1:Cube/myns1:Name", nsmgr2); + + if (toOverwriteProjectBimFile) + { + objectDefinitionDatabaseIdNode.InnerText = "SemanticModel"; + objectDefinitionDatabaseId2Node.InnerText = "SemanticModel"; + objectDefinitionDatabaseNameNode.InnerText = "SemanticModel"; + if (objectDefinitionCubeIdNode != null) objectDefinitionCubeIdNode.InnerText = "Model"; + if (objectDefinitionCubeNameNode != null) objectDefinitionCubeNameNode.InnerText = "Model"; + } + else + { + if (!String.IsNullOrEmpty(_connectionInfo.DeploymentServerDatabase)) + { + objectDefinitionDatabaseIdNode.InnerText = _connectionInfo.DeploymentServerDatabase; + objectDefinitionDatabaseId2Node.InnerText = _connectionInfo.DeploymentServerDatabase; + objectDefinitionDatabaseNameNode.InnerText = _connectionInfo.DeploymentServerDatabase; + } + if (!String.IsNullOrEmpty(_connectionInfo.DeploymentServerCubeName)) + { + if (objectDefinitionCubeIdNode != null) objectDefinitionCubeIdNode.InnerText = _connectionInfo.DeploymentServerCubeName; + if (objectDefinitionCubeNameNode != null) objectDefinitionCubeNameNode.InnerText = _connectionInfo.DeploymentServerCubeName; + } + } + xml = WriteXmlFromDoc(xmlaScriptDoc); + } + + return xml; + } + + private XmlWriterSettings _settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true, + //IndentChars = " ", + NewLineChars = "\r\n", + NewLineHandling = NewLineHandling.Replace + }; + + private string WriteXmlFromDoc(XmlDocument bimFileDoc) + { + string xml; + StringBuilder builder = new StringBuilder(); + using (XmlWriter writer = XmlWriter.Create(builder, _settings)) + { + bimFileDoc.Save(writer); + } + xml = builder.ToString(); + return xml; + } + + private string WriteXmlFromDatabase() + { + string xml; + StringBuilder builder = new StringBuilder(); + using (XmlWriter writer = XmlWriter.Create(builder, _settings)) + { + Scripter.WriteAlter(writer, _amoDatabase, true, true); + } + xml = builder.ToString(); + return xml; + } + + public override string ToString() => this.GetType().FullName; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_amoServer != null) + { + _amoServer.Dispose(); + } + } + + _disposed = true; + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/OptionsInfo.cs b/BismNormalizer/BismNormalizer/TabularCompare/OptionsInfo.cs new file mode 100644 index 0000000..8cd80dc --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/OptionsInfo.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Information about options for a comparison. This is serialized/deserialized to/from the BSMN file. + /// + public class OptionsInfo + { + private bool _optionPerspectives; + private bool _optionMergePerspectives; + private bool _optionCultures; + private bool _optionMergeCultures; + private bool _optionRoles; + private bool _optionActions; + private bool _optionPartitions; + private bool _optionRetainPartitions; + private bool _optionMeasureDependencies; + private ProcessingOption _optionProcessingOption; + private bool _optionTransaction; + private bool _optionAffectedTables; + + /// + /// Initializes a new instance of the OptionsInfo class. + /// + public OptionsInfo() + { + _optionPerspectives = Settings.Default.OptionPerspectives; + _optionMergePerspectives = Settings.Default.OptionMergePerspectives; + _optionCultures = Settings.Default.OptionCultures; + _optionMergeCultures = Settings.Default.OptionMergeCultures; + _optionRoles = Settings.Default.OptionRoles; + _optionActions = Settings.Default.OptionActions; + _optionPartitions = Settings.Default.OptionPartitions; + _optionRetainPartitions = Settings.Default.OptionRetainPartitions; + _optionMeasureDependencies = Settings.Default.OptionMeasureDependencies; + _optionProcessingOption = (ProcessingOption)Enum.Parse(typeof(ProcessingOption), Settings.Default.OptionProcessingOption); + _optionTransaction = Settings.Default.OptionTransaction; + } + + /// + /// A Boolean specifying whether to include perspectives in the comparison. + /// + public bool OptionPerspectives + { + get { return _optionPerspectives; } + set { _optionPerspectives = value; } + } + + /// + /// A Boolean specifying whether to merge perspective selections when updating, rather than replace. + /// + public bool OptionMergePerspectives + { + get { return _optionMergePerspectives; } + set { _optionMergePerspectives = value; } + } + + /// + /// A Boolean specifying whether to include cultures in the comparison. This is ignored by tabular models with multidimensional metadata as they don't support cultures. + /// + public bool OptionCultures + { + get { return _optionCultures; } + set { _optionCultures = value; } + } + + /// + /// A Boolean specifying whether to merge culture translations when updating, rather than replace. + /// + public bool OptionMergeCultures + { + get { return _optionMergeCultures; } + set { _optionMergeCultures = value; } + } + + /// + /// A Boolean specifying whether to include roles in the comparison. + /// + public bool OptionRoles + { + get { return _optionRoles; } + set { _optionRoles = value; } + } + + /// + /// A Boolean specifying whether to include actions in the comparison. + /// + [XmlIgnore()] + //[Obsolete("This property is obsolete. Left over from BISM Normalizer 2, which supported BIDS Helper actions.")] + public bool OptionActions + { + get { return _optionActions; } + set { _optionActions = value; } + } + + /// + /// A Boolean specifying whether to consider partitions when comparing tables. + /// + public bool OptionPartitions + { + get { return _optionPartitions; } + set { _optionPartitions = value; } + } + + /// + /// A Boolean specifying whether to retain partitions for table updates. + /// + public bool OptionRetainPartitions + { + get { return _optionRetainPartitions; } + set { _optionRetainPartitions = value; } + } + + /// + /// A Boolean specifying whether to display warnings for missing measure dependencies. + /// + public bool OptionMeasureDependencies + { + get { return _optionMeasureDependencies; } + set { _optionMeasureDependencies = value; } + } + + /// + /// Processing option for database deployment. + /// + public ProcessingOption OptionProcessingOption + { + get { return _optionProcessingOption; } + set { _optionProcessingOption = value; } + } + + /// + /// A Boolean specifying whether to deploy within a transaction. + /// + [XmlIgnore()] + //[Obsolete("This property is obsolete. Do not use. Left over from a previous version that supported transactions.")] + public bool OptionTransaction + { + get { return _optionTransaction; } + set { _optionTransaction = value; } + } + + /// + /// A Boolean specifying whether to process only the tables affected by the comparison. + /// + public bool OptionAffectedTables + { + get { return _optionAffectedTables; } + set { _optionAffectedTables = value; } + } + + /// + /// Save options as default selections for new comparisons that are not deserialized from BSMN file. + /// + public void Save() + { + Settings.Default.OptionPerspectives = _optionPerspectives; + Settings.Default.OptionMergePerspectives = _optionMergePerspectives; + Settings.Default.OptionCultures = _optionCultures; + Settings.Default.OptionMergeCultures = _optionMergeCultures; + Settings.Default.OptionRoles = _optionRoles; + Settings.Default.OptionActions = _optionActions; + Settings.Default.OptionPartitions = _optionPartitions; + Settings.Default.OptionRetainPartitions = _optionRetainPartitions; + Settings.Default.OptionMeasureDependencies = _optionMeasureDependencies; + Settings.Default.OptionProcessingOption = _optionProcessingOption.ToString(); + Settings.Default.OptionTransaction = _optionTransaction; + Settings.Default.OptionAffectedTables = _optionAffectedTables; + + Settings.Default.Save(); + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/PartitionRowCounter.cs b/BismNormalizer/BismNormalizer/TabularCompare/PartitionRowCounter.cs new file mode 100644 index 0000000..c9cf2e1 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/PartitionRowCounter.cs @@ -0,0 +1,36 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Counter for number of rows processed for a partition. + /// + public class PartitionRowCounter + { + private string _id; + private long _rowCount; + + /// + /// Id of the partition being processed. + /// + public string Id => _id; + + /// + /// Count of rows counted for the partition. + /// + public long RowCount + { + get { return _rowCount; } + set { _rowCount = value; } + } + + /// + /// Initializes a new instance of the PartitionRowCounter class using an id. + /// + public PartitionRowCounter(string id) + { + _id = id; + _rowCount = 0; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/PasswordPromptEventArgs.cs b/BismNormalizer/BismNormalizer/TabularCompare/PasswordPromptEventArgs.cs new file mode 100644 index 0000000..913b281 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/PasswordPromptEventArgs.cs @@ -0,0 +1,40 @@ +using System; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Initializes data for the Comparison.PasswordPrompt event. + /// + public class PasswordPromptEventArgs : EventArgs + { + /// + /// Gets or sets the authentication kind. + /// + public string AuthenticationKind { get; set; } + + /// + /// Gets or sets the name of the data source. + /// + public string DataSourceName { get; set; } + + /// + /// Gets or sets the username. + /// + public string Username { get; set; } + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } + + /// + /// Gets or sets the privacy level. + /// + public string PrivacyLevel { get; set; } + + /// + /// Gets or sets a value indicating if the user cancelled the deployment. + /// + public bool UserCancelled { get; set; } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTable.cs b/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTable.cs new file mode 100644 index 0000000..0dafd34 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTable.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Keeps track of tables for processing, and dynamically increments the rows processed in each partition. + /// + public class ProcessingTable : IComparable + { + private string _name; + private string _id; + private List _partitions = new List(); + + /// + /// Name of the table being processed. + /// + public string Name => _name; + + /// + /// Id of the table being processed. Can be different for tabular models with multidimensional metadata. + /// + public string Id => _id; + + /// + /// Collection of PartitionRowCounter objects. + /// + public List Partitions => _partitions; + + /// + /// Initializes a new instance of the PartitionRowCounter class using a name and id. + /// + public ProcessingTable(string name, string id) + { + _name = name; + _id = id; + } + + /// + /// Get the total row count for all partitions associated with the table. + /// + /// Total row count. + public long GetRowCount() + { + long rowCount = 0; + foreach (PartitionRowCounter partition in _partitions) + { + rowCount += partition.RowCount; + } + return rowCount; + } + + /// + /// Find partition based on its Id. + /// + /// + /// PartitionRowCounter object. Null if not found. + public PartitionRowCounter FindPartition(string id) + { + foreach (PartitionRowCounter partition in _partitions) + { + if (partition.Id == id) + { + return partition; + } + } + return null; + } + + /// + /// Verifies if table contains a partition with the specified Id. + /// + /// + /// A Boolean specifying whether the partition is contained by the table. + public bool ContainsPartition(string id) + { + foreach (PartitionRowCounter partition in _partitions) + { + if (partition.Id == id) + { + return true; + } + } + return false; + } + + public int CompareTo(ProcessingTable other) => string.Compare(this.Name, other.Name); + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTableCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTableCollection.cs new file mode 100644 index 0000000..55c27f3 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/ProcessingTableCollection.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Represents a collection of ProcessingTable objects. + /// + public class ProcessingTableCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// ProcessingTable object if found. Null if not found. + public ProcessingTable FindByName(string name) + { + foreach (ProcessingTable table in this) + { + if (table.Name == name) + { + return table; + } + } + return null; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// ProcessingTable object if found. Null if not found. + public ProcessingTable FindById(string id) + { + foreach (ProcessingTable table in this) + { + if (table.Id == id) + { + return table; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if an object of that name is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (ProcessingTable table in this) + { + if (table.Name == name) + { + return true; + } + } + return false; + } + + /// + /// A Boolean specifying whether the collection contains object by Id. + /// + /// + /// True if an object of that name is found, or False if it's not found. + public bool ContainsId(string id) + { + foreach (ProcessingTable table in this) + { + if (table.Id == id) + { + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/SkipSelection.cs b/BismNormalizer/BismNormalizer/TabularCompare/SkipSelection.cs new file mode 100644 index 0000000..897af81 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/SkipSelection.cs @@ -0,0 +1,69 @@ +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Represents a skipped ComparisonObject. + /// + public class SkipSelection + { + private ComparisonObjectType _comparisonObjectType; + private ComparisonObjectStatus _comparisonObjectStatus; + private string _sourceObjectInternalName; + private string _targetObjectInternalName; + + /// + /// Initializes a new instance of the SkipSelection class. + /// + public SkipSelection() { } + + /// + /// Initializes a new instance of the SkipSelection class using a ComparisonInfo object. + /// + /// ComparisonInfo object typically deserialized from a BSMN file. + public SkipSelection(Core.ComparisonObject comparisonObject) + { + _comparisonObjectType = comparisonObject.ComparisonObjectType; + _comparisonObjectStatus = comparisonObject.Status; + _sourceObjectInternalName = comparisonObject.SourceObjectInternalName; + _targetObjectInternalName = comparisonObject.TargetObjectInternalName; + } + + /// + /// The comparison object type such as Table, Measure, Relationship, etc. + /// + public ComparisonObjectType ComparisonObjectType + { + get { return _comparisonObjectType; } + set { _comparisonObjectType = value; } + } + + /// + /// The comparison object status such as Same Definition, Different Definitions, Missing in Target and Missing in Source. + /// + public ComparisonObjectStatus Status + { + get { return _comparisonObjectStatus; } + set { _comparisonObjectStatus = value; } + } + + /// + /// The source object internal name. + /// + public string SourceObjectInternalName + { + get { return _sourceObjectInternalName; } + set { _sourceObjectInternalName = value; } + } + + /// + /// The target object internal name. + /// + public string TargetObjectInternalName + { + get { return _targetObjectInternalName; } + set { _targetObjectInternalName = value; } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/SkipSelectionCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/SkipSelectionCollection.cs new file mode 100644 index 0000000..f18a50b --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/SkipSelectionCollection.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare +{ + /// + /// Represents a collection of SkipSelection objects. This is serialized/deserialized to/from the BSMN file. + /// + public class SkipSelectionCollection : List + { + public SkipSelectionCollection() { } + + /// + /// A Boolean specifying whether the collection contains an Core.ComparisonObject . + /// + /// + /// True if an object of that name is found, or False if it's not found. + public bool Contains(Core.ComparisonObject comparisonObj) + { + foreach (SkipSelection skipSelection in this) + { + if (skipSelection.ComparisonObjectType == comparisonObj.ComparisonObjectType && skipSelection.Status == comparisonObj.Status && (skipSelection.Status == ComparisonObjectStatus.MissingInSource || skipSelection.SourceObjectInternalName == comparisonObj.SourceObjectInternalName) && (skipSelection.Status == ComparisonObjectStatus.MissingInTarget || skipSelection.TargetObjectInternalName == comparisonObj.TargetObjectInternalName)) + { + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularCompare.dgml b/BismNormalizer/BismNormalizer/TabularCompare/TabularCompare.dgml new file mode 100644 index 0000000..d67ebb9 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularCompare.dgml @@ -0,0 +1,922 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependency.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependency.cs new file mode 100644 index 0000000..2e52682 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependency.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Dependency between partitions, M expressions and data sources + /// + public class CalcDependency + { + private TabularModel _parentTabularModel; + private CalcDependencyObjectType _objectType; + private string _tableName; + private string _objectName; + private string _expression; + private CalcDependencyObjectType _referencedObjectType; + private string _referencedTableName; + private string _referencedObjectName; + private string _referencedExpression; + + /// + /// Initializes a new instance of an CalcDependency class using multiple parameters . + /// + /// TabularModel object that the CalcDependency object belongs to. + public CalcDependency(TabularModel parentTabularModel, string objectType, string tableName, string objectName, string expression, string referencedObjectType, string referencedTableName, string referencedObjectName, string referencedExpression) + { + _parentTabularModel = parentTabularModel; + switch (objectType) + { + case "PARTITION": + _objectType = CalcDependencyObjectType.Partition; + break; + case "M_EXPRESSION": + _objectType = CalcDependencyObjectType.Expression; + break; + default: + break; + } + _tableName = tableName; + _objectName = objectName; + _expression = expression; + switch (referencedObjectType) + { + case "PARTITION": + _referencedObjectType = CalcDependencyObjectType.Partition; + break; + case "M_EXPRESSION": + _referencedObjectType = CalcDependencyObjectType.Expression; + break; + case "DATA_SOURCE": + _referencedObjectType = CalcDependencyObjectType.DataSource; + break; + default: + break; + } + _referencedTableName = referencedTableName; + _referencedObjectName = referencedObjectName; + _referencedExpression = referencedExpression; + } + + /// + /// The object type of the dependency. + /// + public CalcDependencyObjectType ObjectType => _objectType; + + /// + /// The table name of the dependency. + /// + public string TableName => _tableName; + + /// + /// The object name of the dependency. + /// + public string ObjectName => _objectName; + + /// + /// The expression of the dependency. + /// + public string Expression => _expression; + + /// + /// The referenced object type of the dependency. + /// + public CalcDependencyObjectType ReferencedObjectType => _referencedObjectType; + + /// + /// The referenced object name of the dependency. + /// + public string ReferencedTableName => _referencedTableName; + + /// + /// The referenced object name of the dependency. + /// + public string ReferencedObjectName => _referencedObjectName; + + /// + /// The referenced expression of the dependency. + /// + public string ReferencedExpression => _referencedExpression; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependencyCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependencyCollection.cs new file mode 100644 index 0000000..bc8e12d --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CalcDependencyCollection.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of CalcDependency objects. + /// + public class CalcDependencyCollection : List + { + /// + /// Returns collection of calc dependencies that the object identified by the params references (directly or indirectly). + /// + /// Type of the object to look up dependencies. + /// Name of the object to look up dependencies. + /// + public CalcDependencyCollection DependenciesReferenceFrom(CalcDependencyObjectType objectType, string tableName, string objectName) + { + CalcDependencyCollection returnVal = new CalcDependencyCollection(); + LookUpDependenciesReferenceFrom(objectType, tableName, objectName, returnVal); + return returnVal; + } + + private void LookUpDependenciesReferenceFrom(CalcDependencyObjectType objectType, string tableName, string objectName, CalcDependencyCollection returnVal) + { + foreach (CalcDependency calcDependency in this) + { + if (calcDependency.ObjectType == objectType && calcDependency.TableName == tableName && calcDependency.ObjectName == objectName) + { + LookUpDependenciesReferenceFrom(calcDependency.ReferencedObjectType, calcDependency.ReferencedTableName, calcDependency.ReferencedObjectName, returnVal); + returnVal.Add(calcDependency); + } + } + } + + /// + /// Returns collection of M dependency objects that hold references to the object identified by the param values (directly or chained). + /// + /// Type of the object to look up dependencies. + /// Name of the object to look up dependencies. + /// + public CalcDependencyCollection DependenciesReferenceTo(CalcDependencyObjectType referencedObjectType, string referencedObjectName) + { + CalcDependencyCollection returnVal = new CalcDependencyCollection(); + LookUpDependenciesReferenceTo(referencedObjectType, referencedObjectName, returnVal); + return returnVal; + } + + private void LookUpDependenciesReferenceTo(CalcDependencyObjectType referencedObjectType, string referencedObjectName, CalcDependencyCollection returnVal) + { + foreach (CalcDependency calcDependency in this) + { + if (calcDependency.ReferencedObjectType == referencedObjectType && calcDependency.ReferencedObjectName == referencedObjectName) + { + LookUpDependenciesReferenceTo(calcDependency.ObjectType, calcDependency.ObjectName, returnVal); + returnVal.Add(calcDependency); + } + } + } + + /// + /// Find an object in the collection by name. + /// + /// + /// CalcDependency object if found. Null if not found. + public CalcDependency FindByName(string name) + { + foreach (CalcDependency calcDependency in this) + { + if (calcDependency.ObjectName == name) + { + return calcDependency; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (CalcDependency calcDependency in this) + { + if (calcDependency.ObjectName == name) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool Remove(string name) + { + foreach (CalcDependency calcDependency in this) + { + if (calcDependency.ObjectName == name) + { + this.Remove(calcDependency); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Comparison.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Comparison.cs new file mode 100644 index 0000000..9ef27fe --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Comparison.cs @@ -0,0 +1,1540 @@ +using System; +using System.Collections.Generic; +using BismNormalizer.TabularCompare.Core; +using Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a source vs. target comparison of an SSAS tabular model. This class is for tabular models that use tabular metadata with SSAS compatibility level 1200 or above. + /// + public class Comparison : Core.Comparison + { + #region Private Variables + + private TabularModel _sourceTabularModel; + private TabularModel _targetTabularModel; + private bool _uncommitedChanges = false; + private DateTime _lastSourceSchemaUpdate = DateTime.MinValue; + private DateTime _lastTargetSchemaUpdate = DateTime.MinValue; + private bool _disposed = false; + + #endregion + + #region Properties + + /// + /// TabularModel object being used as the source for comparison. + /// + public TabularModel SourceTabularModel + { + get { return _sourceTabularModel; } + set { _sourceTabularModel = value; } + } + + /// + /// TabularModel object being used as the target for comparison. + /// + public TabularModel TargetTabularModel + { + get { return _targetTabularModel; } + set { _targetTabularModel = value; } + } + + #endregion + + public Comparison(ComparisonInfo comparisonInfo) + : base(comparisonInfo) + { + _sourceTabularModel = new TabularModel(this, comparisonInfo.ConnectionInfoSource, comparisonInfo); + _targetTabularModel = new TabularModel(this, comparisonInfo.ConnectionInfoTarget, comparisonInfo); + } + + /// + /// Connect to source and target tabular models, and instantiate their properties. + /// + public override void Connect() + { + _sourceTabularModel.Connect(); + _targetTabularModel.Connect(); + } + + /// + /// Disconnect from source and target tabular models. + /// + public override void Disconnect() + { + _sourceTabularModel.Disconnect(); + _targetTabularModel.Disconnect(); + } + + public override void CompareTabularModels() + { + _comparisonObjectCount = 0; + + #region DataSources + + foreach (DataSource dataSourceSource in _sourceTabularModel.DataSources) + { + // check if source is not in target + if (!_targetTabularModel.DataSources.ContainsName(dataSourceSource.Name)) + { + ComparisonObject comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.MissingInTarget, dataSourceSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + else + { + // there is a DataSource in the target with the same name at least + DataSource dataSourceTarget = _targetTabularModel.DataSources.FindByName(dataSourceSource.Name); + ComparisonObject comparisonObjectDataSource; + + // check if DataSource object definition is different + if (dataSourceSource.ObjectDefinition != dataSourceTarget.ObjectDefinition) + { + comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.DifferentDefinitions, dataSourceSource, dataSourceTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.SameDefinition, dataSourceSource, dataSourceTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + } + } + + foreach (DataSource dataSourceTarget in _targetTabularModel.DataSources) + { + // if target DataSource is Missing in Source, offer deletion + if (!_sourceTabularModel.DataSources.ContainsName(dataSourceTarget.Name)) + { + ComparisonObject comparisonObjectDataSource = new ComparisonObject(ComparisonObjectType.DataSource, ComparisonObjectStatus.MissingInSource, null, dataSourceTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectDataSource); + _comparisonObjectCount += 1; + } + } + + #endregion + + #region Tables + + foreach (Table tblSource in _sourceTabularModel.Tables) + { + // check if source is not in target + TableCollection targetTablesForComparison = _targetTabularModel.Tables; + + if (!targetTablesForComparison.ContainsName(tblSource.Name)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInTarget, tblSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for table Missing in Target + + // all relationships in source are not in target (the target table doesn't even exist) + foreach (Relationship relSource in tblSource.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInTarget, relSource, null, MergeAction.Create); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures / KPIs for Table that is Missing in Target + + foreach (Measure measureSource in tblSource.Measures.FilterByTableName(tblSource.Name)) + { + ComparisonObjectType comparisonObjectType = measureSource.IsKpi ? ComparisonObjectType.Kpi : ComparisonObjectType.Measure; + ComparisonObject comparisonObjectMeasure = new ComparisonObject(comparisonObjectType, ComparisonObjectStatus.MissingInTarget, measureSource, null, MergeAction.Create); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + } + else + { + //table name is in source and target + + Table tblTarget = _targetTabularModel.Tables.FindByName(tblSource.Name); + ComparisonObject comparisonObjectTable; + + if (tblSource.ObjectDefinition == tblTarget.ObjectDefinition) + { + comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.SameDefinition, tblSource, tblTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.DifferentDefinitions, tblSource, tblTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + } + + #region Relationships source/target tables exist + + foreach (Relationship relSource in tblSource.Relationships) + { + // check if source is not in target + if (!tblTarget.Relationships.ContainsName(relSource.Name)) //Using Name, not InternalName in case internal name is different + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInTarget, relSource, null, MergeAction.Create); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + else + { + //relationship is in source and target + + Relationship relTarget = tblTarget.Relationships.FindByName(relSource.Name); + ComparisonObject comparisonObjectRelationship; + + if (relSource.ObjectDefinition == relTarget.ObjectDefinition) + { + comparisonObjectRelationship = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.SameDefinition, relSource, relTarget, MergeAction.Skip); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelationship); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectRelationship = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.DifferentDefinitions, relSource, relTarget, MergeAction.Update); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelationship); + _comparisonObjectCount += 1; + } + } + } + + // see if relationships in target table that don't exist in source table + foreach (Relationship relTarget in tblTarget.Relationships) + { + // check if source is not in target + if (!tblSource.Relationships.ContainsName(relTarget.Name)) //Using Name, not InternalName in case internal name is different + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInSource, null, relTarget, MergeAction.Delete); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + } + + #endregion + + #region Measures / KPIs (table in source and target) + + // see if matching measure in source and target + foreach (Measure measureSource in tblSource.Measures.FilterByTableName(tblSource.Name)) + { + ComparisonObjectType comparisonObjectType = measureSource.IsKpi ? ComparisonObjectType.Kpi : ComparisonObjectType.Measure; + + if (tblTarget.Measures.FilterByTableName(tblTarget.Name).ContainsName(measureSource.Name)) + { + //Measure in source and target, so check definition + Measure measureTarget = tblTarget.Measures.FilterByTableName(tblTarget.Name).FindByName(measureSource.Name); + if (measureSource.ObjectDefinition == measureTarget.ObjectDefinition) + { + //Measure has same definition + ComparisonObject comparisonObjectMeasure = new ComparisonObject(comparisonObjectType, ComparisonObjectStatus.SameDefinition, measureSource, measureTarget, MergeAction.Skip); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + else + { + //Measure has different definition + ComparisonObject comparisonObjectMeasure = new ComparisonObject(comparisonObjectType, ComparisonObjectStatus.DifferentDefinitions, measureSource, measureTarget, MergeAction.Update); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + else + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(comparisonObjectType, ComparisonObjectStatus.MissingInTarget, measureSource, null, MergeAction.Create); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + //now check if target contains measures Missing in Source + foreach (Measure measureTarget in tblTarget.Measures.FilterByTableName(tblTarget.Name)) + { + ComparisonObjectType comparisonObjectType = measureTarget.IsKpi ? ComparisonObjectType.Kpi : ComparisonObjectType.Measure; + if (!tblSource.Measures.FilterByTableName(tblSource.Name).ContainsName(measureTarget.Name)) + { + ComparisonObject comparisonObjectMeasure = new ComparisonObject(comparisonObjectType, ComparisonObjectStatus.MissingInSource, null, measureTarget, MergeAction.Delete); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + } + + #endregion + } + } + + foreach (Table tblTarget in _targetTabularModel.Tables) + { + // check if target is not in source + if (!_sourceTabularModel.Tables.ContainsName(tblTarget.Name)) + { + ComparisonObject comparisonObjectTable = new ComparisonObject(ComparisonObjectType.Table, ComparisonObjectStatus.MissingInSource, null, tblTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectTable); + _comparisonObjectCount += 1; + + #region Relationships for table Missing in Source + + // all relationships in target are not in source (the source table doesn't even exist) + foreach (Relationship relTarget in tblTarget.Relationships) + { + ComparisonObject comparisonObjectRelation = new ComparisonObject(ComparisonObjectType.Relationship, ComparisonObjectStatus.MissingInSource, null, relTarget, MergeAction.Delete); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectRelation); + _comparisonObjectCount += 1; + } + + #endregion + + #region Measures for Table that is Missing in Source + + foreach (Measure measureTarget in tblTarget.Measures.FilterByTableName(tblTarget.Name)) + { + ComparisonObjectType comparisonObjectType = measureTarget.IsKpi ? ComparisonObjectType.Kpi : ComparisonObjectType.Measure; + ComparisonObject comparisonObjectMeasure = new ComparisonObject(ComparisonObjectType.Measure, ComparisonObjectStatus.MissingInSource, null, measureTarget, MergeAction.Delete); + comparisonObjectTable.ChildComparisonObjects.Add(comparisonObjectMeasure); + _comparisonObjectCount += 1; + } + + #endregion + } + } + + #endregion + + #region Expressions + + foreach (Expression expressionSource in _sourceTabularModel.Expressions) + { + // check if source is not in target + if (!_targetTabularModel.Expressions.ContainsName(expressionSource.Name)) + { + ComparisonObject comparisonObjectExpression = new ComparisonObject(ComparisonObjectType.Expression, ComparisonObjectStatus.MissingInTarget, expressionSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectExpression); + _comparisonObjectCount += 1; + } + else + { + // there is a expression in the target with the same name at least + Expression expressionTarget = _targetTabularModel.Expressions.FindByName(expressionSource.Name); + ComparisonObject comparisonObjectExpression; + + // check if expression object definition is different + if (expressionSource.ObjectDefinition != expressionTarget.ObjectDefinition) + { + comparisonObjectExpression = new ComparisonObject(ComparisonObjectType.Expression, ComparisonObjectStatus.DifferentDefinitions, expressionSource, expressionTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectExpression); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectExpression = new ComparisonObject(ComparisonObjectType.Expression, ComparisonObjectStatus.SameDefinition, expressionSource, expressionTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectExpression); + _comparisonObjectCount += 1; + } + } + } + + foreach (Expression expressionTarget in _targetTabularModel.Expressions) + { + // if target expression is Missing in Source, offer deletion + if (!_sourceTabularModel.Expressions.ContainsName(expressionTarget.Name)) + { + ComparisonObject comparisonObjectExpression = new ComparisonObject(ComparisonObjectType.Expression, ComparisonObjectStatus.MissingInSource, null, expressionTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectExpression); + _comparisonObjectCount += 1; + } + } + + #endregion + + #region Perspectives + + if (_comparisonInfo.OptionsInfo.OptionPerspectives) + { + foreach (Perspective perspectiveSource in _sourceTabularModel.Perspectives) + { + // check if source is not in target + if (!_targetTabularModel.Perspectives.ContainsName(perspectiveSource.Name)) + { + ComparisonObject comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.MissingInTarget, perspectiveSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + else + { + // there is a perspective in the target with the same name at least + Perspective perspectiveTarget = _targetTabularModel.Perspectives.FindByName(perspectiveSource.Name); + ComparisonObject comparisonObjectPerspective; + + // check if perspective object definition is different + //if (perspectiveSource.ObjectDefinition != perspectiveTarget.ObjectDefinition) + if ( (_comparisonInfo.OptionsInfo.OptionMergePerspectives && perspectiveTarget.ContainsOtherPerspectiveSelections(perspectiveSource)) || + (!_comparisonInfo.OptionsInfo.OptionMergePerspectives && perspectiveTarget.ContainsOtherPerspectiveSelections(perspectiveSource) && perspectiveSource.ContainsOtherPerspectiveSelections(perspectiveTarget)) ) + { + // they are equal, ... + comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.SameDefinition, perspectiveSource, perspectiveTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.DifferentDefinitions, perspectiveSource, perspectiveTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + } + } + + foreach (Perspective perspectiveTarget in _targetTabularModel.Perspectives) + { + // if target perspective is Missing in Source, offer deletion + if (!_sourceTabularModel.Perspectives.ContainsName(perspectiveTarget.Name)) + { + ComparisonObject comparisonObjectPerspective = new ComparisonObject(ComparisonObjectType.Perspective, ComparisonObjectStatus.MissingInSource, null, perspectiveTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectPerspective); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Cultures + + if (_comparisonInfo.OptionsInfo.OptionCultures) + { + foreach (Culture cultureSource in _sourceTabularModel.Cultures) + { + // check if source is not in target + if (!_targetTabularModel.Cultures.ContainsName(cultureSource.Name)) + { + ComparisonObject comparisonObjectCulture = new ComparisonObject(ComparisonObjectType.Culture, ComparisonObjectStatus.MissingInTarget, cultureSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectCulture); + _comparisonObjectCount += 1; + } + else + { + // there is a culture in the target with the same name at least + Culture cultureTarget = _targetTabularModel.Cultures.FindByName(cultureSource.Name); + ComparisonObject comparisonObjectCulture; + + // check if culture object definition is different + //if (cultureSource.ObjectDefinition != cultureTarget.ObjectDefinition) + if ((_comparisonInfo.OptionsInfo.OptionMergeCultures && cultureTarget.ContainsOtherCultureTranslations(cultureSource)) || + (!_comparisonInfo.OptionsInfo.OptionMergeCultures && cultureTarget.ContainsOtherCultureTranslations(cultureSource) && cultureSource.ContainsOtherCultureTranslations(cultureTarget))) + { + // they are equal, ... + comparisonObjectCulture = new ComparisonObject(ComparisonObjectType.Culture, ComparisonObjectStatus.SameDefinition, cultureSource, cultureTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectCulture); + _comparisonObjectCount += 1; + } + else + { + comparisonObjectCulture = new ComparisonObject(ComparisonObjectType.Culture, ComparisonObjectStatus.DifferentDefinitions, cultureSource, cultureTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectCulture); + _comparisonObjectCount += 1; + } + } + } + + foreach (Culture cultureTarget in _targetTabularModel.Cultures) + { + // if target culture is Missing in Source, offer deletion + if (!_sourceTabularModel.Cultures.ContainsName(cultureTarget.Name)) + { + ComparisonObject comparisonObjectCulture = new ComparisonObject(ComparisonObjectType.Culture, ComparisonObjectStatus.MissingInSource, null, cultureTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectCulture); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Roles + + if (_comparisonInfo.OptionsInfo.OptionRoles) + { + foreach (Role roleSource in _sourceTabularModel.Roles) + { + // check if source is not in target + if (!_targetTabularModel.Roles.ContainsName(roleSource.Name)) + { + ComparisonObject comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.MissingInTarget, roleSource, null, MergeAction.Create); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + else + { + // there is a role in the target with the same name at least + Role roleTarget = _targetTabularModel.Roles.FindByName(roleSource.Name); + ComparisonObject comparisonObjectRole; + + // check if role object definition is different + if (roleSource.ObjectDefinition != roleTarget.ObjectDefinition) + { + comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.DifferentDefinitions, roleSource, roleTarget, MergeAction.Update); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + else + { + // they are equal, ... + comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.SameDefinition, roleSource, roleTarget, MergeAction.Skip); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + } + } + + foreach (Role roleTarget in _targetTabularModel.Roles) + { + // if target role is Missing in Source, offer deletion + if (!_sourceTabularModel.Roles.ContainsName(roleTarget.Name)) + { + ComparisonObject comparisonObjectRole = new ComparisonObject(ComparisonObjectType.Role, ComparisonObjectStatus.MissingInSource, null, roleTarget, MergeAction.Delete); + _comparisonObjects.Add(comparisonObjectRole); + _comparisonObjectCount += 1; + } + } + } + + #endregion + + #region Sorting + + _comparisonObjects.Sort(); + foreach (ComparisonObject childComparisonObject in _comparisonObjects) + { + childComparisonObject.ChildComparisonObjects.Sort(); + foreach (ComparisonObject grandChildComparisonObject in childComparisonObject.ChildComparisonObjects) + { + grandChildComparisonObject.ChildComparisonObjects.Sort(); + } + } + + #endregion + + this.RefreshComparisonObjectsFromSkipSelections(); + + _uncommitedChanges = false; + _lastSourceSchemaUpdate = _sourceTabularModel.TomDatabase.LastSchemaUpdate; + _lastTargetSchemaUpdate = _targetTabularModel.TomDatabase.LastSchemaUpdate; + } + + /// + /// Validate selection of actions to perform on target tabular model. Warnings and informational messages are provided by invoking ShowStatusMessageCallBack. + /// + public override void ValidateSelection() + { + #region Refresh/reconnect source and target dbs to check if server definition has changed + + if (_uncommitedChanges) + { + // Reconnect to re-initialize + _sourceTabularModel = new TabularModel(this, _comparisonInfo.ConnectionInfoSource, _comparisonInfo); + _sourceTabularModel.Connect(); + _targetTabularModel = new TabularModel(this, _comparisonInfo.ConnectionInfoTarget, _comparisonInfo); + _targetTabularModel.Connect(); + } + else + { + _sourceTabularModel.TomDatabase.Refresh(); + _targetTabularModel.TomDatabase.Refresh(); + } + + if (!_sourceTabularModel.ConnectionInfo.UseProject && _sourceTabularModel.TomDatabase.LastSchemaUpdate > _lastSourceSchemaUpdate) + { + throw new Exception("The definition of the source database has changed since the comparison was run. Please re-run the comparison."); + } + if (!_targetTabularModel.ConnectionInfo.UseProject && _targetTabularModel.TomDatabase.LastSchemaUpdate > _lastTargetSchemaUpdate) + { + throw new Exception("The definition of the target database has changed since the comparison was run. Please re-run the comparison."); + } + + _uncommitedChanges = true; + + #endregion + + #region Iterate of objects for delete/create/updates + + #region Backup perspectives, cultures and roles + + /*It's easier to take a backup of perspectives, cultures and roles now and add back after table changes, rather than every + time update a table, take a temp backup to add back columns/measures. Also would need to remove deleted tables/meausures, ... + Gets pretty hairy. + */ + + _targetTabularModel.BackupAffectedObjects(); + + #endregion + + #region DataSources + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + DeleteDataSource(comparisonObject); + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + CreateDataSource(comparisonObject); + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + UpdateDataSource(comparisonObject); + } + + #endregion + + #region Expressions + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + DeleteExpression(comparisonObject); + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + CreateExpression(comparisonObject); + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + UpdateExpression(comparisonObject); + } + + #endregion + + #region Tables + + // do deletions first to minimize chance of conflict + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + DeleteTable(comparisonObject); + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + CreateTable(comparisonObject); + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + UpdateTable(comparisonObject); + } + + #endregion + + #region Relationships + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + DeleteRelationship(childComparisonObject); //Relationship + } + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + CreateRelationship(childComparisonObject, comparisonObject.SourceObjectName); //Relationship, Table + } + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + UpdateRelationship(childComparisonObject, comparisonObject.SourceObjectName); //Relationship, Table + } + } + + _targetTabularModel.ValidateRelationships(); + + #endregion + + _targetTabularModel.CleanUpVariations(); + + #region Measures / KPIs + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + DeleteMeasure(childComparisonObject); //Measure + } + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + CreateMeasure(childComparisonObject, comparisonObject.SourceObjectName); //Measure, Table + } + } + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + foreach (ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + UpdateMeasure(childComparisonObject, comparisonObject.SourceObjectName); //Measure, Table + } + } + + #endregion + + #region Perspectives + + //Restore perspectives that were backed up earlier. Having done this there won't be any dependency issues, so can start comparison changes. + _targetTabularModel.RestorePerspectives(); + + // Do separate loops of _comparisonObjectects for Delete, Create, Update to ensure informational logging order is consistent with other object types. This also ensures deletions are done first to minimize chance of conflict. + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeletePerspective(comparisonObject.TargetObjectInternalName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete perspective [{comparisonObject.TargetObjectName}].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Create) + { + _targetTabularModel.CreatePerspective(_sourceTabularModel.Perspectives.FindById(comparisonObject.SourceObjectInternalName).TomPerspective); + OnValidationMessage(new ValidationMessageEventArgs($"Create perspective [{comparisonObject.SourceObjectName}].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Perspective && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdatePerspective(_sourceTabularModel.Perspectives.FindById(comparisonObject.SourceObjectInternalName).TomPerspective, _targetTabularModel.Perspectives.FindById(comparisonObject.TargetObjectInternalName).TomPerspective); + OnValidationMessage(new ValidationMessageEventArgs($"Update perspective [{comparisonObject.TargetObjectName}].", ValidationMessageType.Perspective, ValidationMessageStatus.Informational)); + } + } + + #endregion + + #region Roles + + //Restore roles that were backed up earlier. Having done this there won't be any dependency issues, so can start comparison changes. + _targetTabularModel.RestoreRoles(); + + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteRole(comparisonObject.TargetObjectInternalName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete role [{comparisonObject.TargetObjectName}].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Create) + { + _targetTabularModel.CreateRole(_sourceTabularModel.Roles.FindById(comparisonObject.SourceObjectInternalName).TomRole); + OnValidationMessage(new ValidationMessageEventArgs($"Create role [{comparisonObject.SourceObjectName}].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Role && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateRole(_sourceTabularModel.Roles.FindById(comparisonObject.SourceObjectInternalName), _targetTabularModel.Roles.FindById(comparisonObject.TargetObjectInternalName)); + OnValidationMessage(new ValidationMessageEventArgs($"Update role [{comparisonObject.TargetObjectName}].", ValidationMessageType.Role, ValidationMessageStatus.Informational)); + } + } + + _targetTabularModel.RolesCleanup(); + + #endregion + + #region Cultures + + //Restore cultures that were backed up earlier. Having done this there won't be any dependency issues, so can start comparison changes. + //Note that cannot restore cultures before finished perspective comparison changes above, because cultures can have dependencies on perspectives. + _targetTabularModel.RestoreCultues(); + + // Do separate loops of _comparisonObjectects for Delete, Create, Update to ensure informational logging order is consistent with other object types. This also ensures deletions are done first to minimize chance of conflict. + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Culture && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteCulture(comparisonObject.TargetObjectInternalName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete culture [{comparisonObject.TargetObjectName}].", ValidationMessageType.Culture, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Culture && comparisonObject.MergeAction == MergeAction.Create) + { + _targetTabularModel.CreateCulture(_sourceTabularModel.Cultures.FindById(comparisonObject.SourceObjectInternalName).TomCulture); + OnValidationMessage(new ValidationMessageEventArgs($"Create culture [{comparisonObject.SourceObjectName}].", ValidationMessageType.Culture, ValidationMessageStatus.Informational)); + } + } + foreach (ComparisonObject comparisonObject in _comparisonObjects) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Culture && comparisonObject.MergeAction == MergeAction.Update) + { + _targetTabularModel.UpdateCulture(_sourceTabularModel.Cultures.FindById(comparisonObject.SourceObjectInternalName).TomCulture, _targetTabularModel.Cultures.FindById(comparisonObject.TargetObjectInternalName).TomCulture); + OnValidationMessage(new ValidationMessageEventArgs($"Update culture [{comparisonObject.TargetObjectName}].", ValidationMessageType.Culture, ValidationMessageStatus.Informational)); + } + } + + #endregion + + #endregion + + #region Missing measure dependencies + + if (_comparisonInfo.OptionsInfo.OptionMeasureDependencies) + { + foreach (Table table in _targetTabularModel.Tables) + { + foreach (Measure measure in table.Measures) + { + foreach (string missingDependency in measure.FindMissingMeasureDependencies()) + { + OnValidationMessage(new ValidationMessageEventArgs($"Measure [{measure.InternalName}] in table '{table.Name}' contains dependency on measure/column [{missingDependency}], which (considering changes to target) cannot be found in target model.", ValidationMessageType.MeasureCalculationDependency, ValidationMessageStatus.Warning)); + } + } + } + } + + #endregion + + OnResizeValidationHeaders(new EventArgs()); + } + + #region Private methods for validation + + #region Calc dependencies validation + + private bool HasBlockingToDependenciesInTarget(string targetObjectName, CalcDependencyObjectType targetObjectType, ref List warningObjectList) + { + //For deletion. + //Check any objects in target that depend on this object are also going to be deleted or updated. + + bool returnVal = false; + CalcDependencyCollection targetToDepdendencies = _targetTabularModel.MDependencies.DependenciesReferenceTo(targetObjectType, targetObjectName); + foreach (CalcDependency targetToDependency in targetToDepdendencies) + { + foreach (ComparisonObject comparisonObjectToCheck in _comparisonObjects) + { + switch (targetToDependency.ObjectType) + { + case CalcDependencyObjectType.Expression: + //Does this expression (comparisonObjectToCheck) have a dependency on the object about to be deleted (targetObjectName)? + + if (comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.Expression && + comparisonObjectToCheck.TargetObjectName == targetToDependency.ObjectName && + ( + comparisonObjectToCheck.MergeAction == MergeAction.Skip || //Skip covers if this expression is for deletion and being skipped, or if same defintion and not being touched in target (in either case, dependency will remain). + ( + comparisonObjectToCheck.MergeAction == MergeAction.Update && //Updates (if successful) are fine because covered by source dependency checking. So need to check if the update will be unsuccessful (and therefore dependency will remain). + HasBlockingFromDependenciesInSource( + "", //Expressions don't have table value + comparisonObjectToCheck.TargetObjectName, + CalcDependencyObjectType.Expression) + ) //Create expression is not possible to have a dependency on this object about to be deleted. Delete expression is fine. + ) + ) + { + string warningObject = $"Expression {comparisonObjectToCheck.TargetObjectName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + } + break; + case CalcDependencyObjectType.Partition: + //Does this table (comparisonObjectToCheck) have a dependency on the object about to be deleted (targetObjectName)? + + if (comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.Table && + comparisonObjectToCheck.TargetObjectName == targetToDependency.TableName && + ( + comparisonObjectToCheck.MergeAction == MergeAction.Skip || //Skip covers if this table is for deletion and being skipped, or if same defintion and not being touched in target (in either case, dependency will remain). + ( + comparisonObjectToCheck.MergeAction == MergeAction.Update && //Updates (if successful) are fine because covered by source dependency checking. So need to check if the update will be unsuccessful (and therefore dependency will remain). + ( + HasBlockingFromDependenciesInSourceForTable(_sourceTabularModel.Tables.FindByName(comparisonObjectToCheck.TargetObjectName)) || + _targetTabularModel.CanRetainPartitions( //But also check if doing retain partitions on this table (if so, dependency will remain). + _sourceTabularModel.Tables.FindByName(comparisonObjectToCheck.TargetObjectName), + _targetTabularModel.Tables.FindByName(comparisonObjectToCheck.TargetObjectName), + out string retainPartitionsMessage) + ) + ) //Create table is not possible to have a dependency on this object about to be deleted. Delete table is fine. + ) + ) + { + string warningObject = $"Table {comparisonObjectToCheck.TargetObjectName}/Partition {targetToDependency.ObjectName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + } + break; + default: + break; + } + } + } + return returnVal; + } + + private bool HasBlockingFromDependenciesInSource(string sourceTableName, string sourceObjectName, CalcDependencyObjectType sourceObjectType, ref List warningObjectList, out bool nonStructuredDataSource) + { + //For creation and updates. + //Check any objects in source that this object depends on are also going to be created OR updated (if not already in target). + + bool returnVal = false; + nonStructuredDataSource = false; + + CalcDependencyCollection sourceFromDepdendencies = _sourceTabularModel.MDependencies.DependenciesReferenceFrom(sourceObjectType, sourceTableName, sourceObjectName); + foreach (CalcDependency sourceFromDependency in sourceFromDepdendencies) + { + foreach (ComparisonObject comparisonObjectToCheck in _comparisonObjects) + { + switch (sourceFromDependency.ReferencedObjectType) + { + case CalcDependencyObjectType.Expression: + //Does the object about to be created/updated (sourceObjectName) have a source dependency on this expression (comparisonObjectToCheck)? + + if (!_targetTabularModel.Expressions.ContainsName(sourceFromDependency.ReferencedObjectName) && + comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.Expression && + comparisonObjectToCheck.SourceObjectName == sourceFromDependency.ReferencedObjectName && + comparisonObjectToCheck.Status == ComparisonObjectStatus.MissingInTarget && //Creates being skipped (dependency will be missing). + comparisonObjectToCheck.MergeAction == MergeAction.Skip) + //Deletes are impossible for this object to depend on, so don't need to detect. Other Skips can assume are fine, so don't need to detect. + { + string warningObject = $"Expression {comparisonObjectToCheck.SourceObjectName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + } + + break; + case CalcDependencyObjectType.DataSource: + //Does the object about to be created/updated (sourceObjectName) have a source dependency on this data source (comparisonObjectToCheck)? + + if (!_targetTabularModel.DataSources.ContainsName(sourceFromDependency.ReferencedObjectName) && + comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.DataSource && + comparisonObjectToCheck.SourceObjectName == sourceFromDependency.ReferencedObjectName && + comparisonObjectToCheck.Status == ComparisonObjectStatus.MissingInTarget && //Creates being skipped (dependency will be missing). + comparisonObjectToCheck.MergeAction == MergeAction.Skip) + //Deletes are impossible for this object to depend on, so don't need to detect. Other Skips can assume are fine, so don't need to detect. + { + string warningObject = $"Data Source {comparisonObjectToCheck.SourceObjectName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + } + + //Check if target data source type is provider and source is structured. Won't be updated. + if (comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.DataSource && + comparisonObjectToCheck.Status == ComparisonObjectStatus.DifferentDefinitions && + comparisonObjectToCheck.SourceObjectName == sourceFromDependency.ReferencedObjectName && + _targetTabularModel.DataSources.ContainsName(sourceFromDependency.ReferencedObjectName) && + _sourceTabularModel.DataSources.ContainsName(sourceFromDependency.ReferencedObjectName) && + _targetTabularModel.DataSources.FindByName(sourceFromDependency.ReferencedObjectName).TomDataSource.Type == DataSourceType.Provider && + _sourceTabularModel.DataSources.FindByName(sourceFromDependency.ReferencedObjectName).TomDataSource.Type == DataSourceType.Structured) //Don't need to check if Skip or not because can't update if different data source types anyway + { + string warningObject = $"Data Source {comparisonObjectToCheck.SourceObjectName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + nonStructuredDataSource = true; + } + + break; + default: + break; + } + } + } + return returnVal; + } + + private bool HasBlockingFromDependenciesInSource(string sourceTableName, string sourceObjectName, CalcDependencyObjectType sourceObjectType) + { + List warningObjectList = new List(); + return HasBlockingFromDependenciesInSource(sourceTableName, sourceObjectName, sourceObjectType, ref warningObjectList, out bool nonStructuredDataSource); + } + + private bool HasBlockingFromDependenciesInSourceForTable(Table sourceTable) + { + bool returnVal = false; + foreach (Partition partition in sourceTable.TomTable.Partitions) + { + if (HasBlockingFromDependenciesInSource(sourceTable.Name, partition.Name, CalcDependencyObjectType.Partition)) + { + returnVal = true; + break; + } + } + return returnVal; + } + + private bool HasBlockingOldPartitionDependency(Partition partition, ref List warningObjectList) + { + //Only for old partition types + + bool returnVal = false; + + if (partition.SourceType == PartitionSourceType.Query && + !_targetTabularModel.DataSources.ContainsName(((QueryPartitionSource)partition.Source).DataSource.Name)) + { + string dataSourceName = ((QueryPartitionSource)partition.Source).DataSource.Name; + + //For old non-M partitions, check if data source references exist + foreach (ComparisonObject comparisonObjectToCheck in _comparisonObjects) + { + if (comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.DataSource && + comparisonObjectToCheck.SourceObjectName == dataSourceName && + comparisonObjectToCheck.Status == ComparisonObjectStatus.MissingInTarget && + comparisonObjectToCheck.MergeAction == MergeAction.Skip) + { + string warningObject = $"Data Source {dataSourceName}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + returnVal = true; + } + } + } + return returnVal; + } + + #endregion + + //DataSources + + private void DeleteDataSource(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Delete) + { + //Check any objects in target that depend on the DataSource are also going to be deleted + List warningObjectList = new List(); + bool toDependencies = HasBlockingToDependenciesInTarget(comparisonObject.TargetObjectName, CalcDependencyObjectType.DataSource, ref warningObjectList); + + //For old non-M partitions, check if any such tables have reference to this DataSource, and will not be deleted + foreach (Table table in _targetTabularModel.Tables) + { + foreach (Partition partition in table.TomTable.Partitions) + { + if (partition.SourceType == PartitionSourceType.Query && + table.DataSourceName == comparisonObject.TargetObjectName) + { + foreach (ComparisonObject comparisonObjectToCheck in _comparisonObjects) + { + if ( + ( + comparisonObjectToCheck.ComparisonObjectType == ComparisonObjectType.Table && + comparisonObjectToCheck.TargetObjectName == table.Name + ) && + ( + ( //Skipped deletes, dependency will remain + comparisonObjectToCheck.Status == ComparisonObjectStatus.MissingInSource && + comparisonObjectToCheck.MergeAction == MergeAction.Skip + ) || + ( //Same definition, dependency will remain + comparisonObjectToCheck.Status == ComparisonObjectStatus.SameDefinition + ) || + ( //Different definition, and skip update (source already dependencies covered), dependency will remain + comparisonObjectToCheck.Status == ComparisonObjectStatus.DifferentDefinitions && + comparisonObjectToCheck.MergeAction == MergeAction.Skip + ) + ) + ) + { + string warningObject = $"Table {table.Name}/Partition {partition.Name}"; + if (!warningObjectList.Contains(warningObject)) + { + warningObjectList.Add(warningObject); + } + toDependencies = true; + } + } + } + } + } + + if (!toDependencies) + { + _targetTabularModel.DeleteDataSource(comparisonObject.TargetObjectName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete data source [{comparisonObject.TargetObjectName}].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to delete data source {comparisonObject.TargetObjectName} because the following object(s) depend on it: {String.Join(", ", warningObjectList)}.", ValidationMessageType.DataSource, ValidationMessageStatus.Warning)); + } + } + } + + private void CreateDataSource(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Create) + { + _targetTabularModel.CreateDataSource(_sourceTabularModel.DataSources.FindByName(comparisonObject.SourceObjectName)); + OnValidationMessage(new ValidationMessageEventArgs($"Create data source [{comparisonObject.SourceObjectName}].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + } + + private void UpdateDataSource(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.DataSource && comparisonObject.MergeAction == MergeAction.Update) + { + DataSource sourceDataSource = _sourceTabularModel.DataSources.FindByName(comparisonObject.SourceObjectName); + DataSource targetDataSource = _targetTabularModel.DataSources.FindByName(comparisonObject.TargetObjectName); + + if (sourceDataSource.TomDataSource.Type != targetDataSource.TomDataSource.Type) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to update data source {comparisonObject.TargetObjectName} because the source/target types (provider/structured) don't match, which is not supported.", ValidationMessageType.DataSource, ValidationMessageStatus.Warning)); + } + else + { + _targetTabularModel.UpdateDataSource(sourceDataSource, targetDataSource); + OnValidationMessage(new ValidationMessageEventArgs($"Update data source [{comparisonObject.TargetObjectName}].", ValidationMessageType.DataSource, ValidationMessageStatus.Informational)); + } + } + } + + //Expressions + + private void DeleteExpression(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Expression && comparisonObject.MergeAction == MergeAction.Delete) + { + //Check any objects in target that depend on the expression are also going to be deleted + List warningObjectList = new List(); + if (!HasBlockingToDependenciesInTarget(comparisonObject.TargetObjectName, CalcDependencyObjectType.Expression, ref warningObjectList)) + { + _targetTabularModel.DeleteExpression(comparisonObject.TargetObjectName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete expression [{comparisonObject.TargetObjectName}].", ValidationMessageType.Expression, ValidationMessageStatus.Informational)); + } + else + { + string message = $"Unable to delete expression {comparisonObject.TargetObjectName} because the following objects depend on it: {String.Join(", ", warningObjectList)}."; + if (_comparisonInfo.OptionsInfo.OptionRetainPartitions) + { + message += " Note: the option to retain partitions is on, which may be affecting this."; + } + OnValidationMessage(new ValidationMessageEventArgs(message, ValidationMessageType.Expression, ValidationMessageStatus.Warning)); + } + } + } + + private void CreateExpression(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Expression && comparisonObject.MergeAction == MergeAction.Create) + { + //Check any objects in source that this expression depends on are also going to be created if not already in target + List warningObjectList = new List(); + if (!HasBlockingFromDependenciesInSource( + "", //can assume blank table for an expression + comparisonObject.SourceObjectName, + CalcDependencyObjectType.Expression, + ref warningObjectList, + out bool nonStructuredDataSource)) + { + _targetTabularModel.CreateExpression(_sourceTabularModel.Expressions.FindByName(comparisonObject.SourceObjectName).TomExpression); + OnValidationMessage(new ValidationMessageEventArgs($"Create expression [{comparisonObject.SourceObjectName}].", ValidationMessageType.Expression, ValidationMessageStatus.Informational)); + } + else + { + if (!nonStructuredDataSource) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create expression {comparisonObject.SourceObjectName} because it depends on the following objects, which (considering changes) are missing from target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Expression, ValidationMessageStatus.Warning)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create expression {comparisonObject.SourceObjectName} because it depends on the following objects, which (considering changes) are missing from target and/or depend on a structured data source that is provider in the target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Expression, ValidationMessageStatus.Warning)); + } + } + } + } + + private void UpdateExpression(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Expression && comparisonObject.MergeAction == MergeAction.Update) + { + //Check any objects in source that this expression depends on are also going to be created if not already in target + List warningObjectList = new List(); + if (!HasBlockingFromDependenciesInSource( + "", //Can assume blank table for expression + comparisonObject.SourceObjectName, + CalcDependencyObjectType.Expression, + ref warningObjectList, + out bool nonStructuredDataSource)) + { + _targetTabularModel.UpdateExpression(_sourceTabularModel.Expressions.FindByName(comparisonObject.SourceObjectName), _targetTabularModel.Expressions.FindByName(comparisonObject.TargetObjectName)); + OnValidationMessage(new ValidationMessageEventArgs($"Update expression [{comparisonObject.TargetObjectName}].", ValidationMessageType.Expression, ValidationMessageStatus.Informational)); + } + else + { + if (!nonStructuredDataSource) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to update expression {comparisonObject.TargetObjectName} because version from the source depends on the following objects, which (considering changes) are missing from target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Expression, ValidationMessageStatus.Warning)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to update expression {comparisonObject.TargetObjectName} because version from the source depends on the following objects, which (considering changes) are missing from target and/or depend on a structured data source that is provider in the target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Expression, ValidationMessageStatus.Warning)); + } + } + } + } + + //Tables + + private void DeleteTable(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Table && comparisonObject.MergeAction == MergeAction.Delete) + { + _targetTabularModel.DeleteTable(comparisonObject.TargetObjectName); + OnValidationMessage(new ValidationMessageEventArgs($"Delete table '{comparisonObject.TargetObjectName}'.", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + } + + private void CreateTable(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Table && comparisonObject.MergeAction == MergeAction.Create) + { + Table sourceTable = _sourceTabularModel.Tables.FindByName(comparisonObject.SourceObjectName); + List warningObjectList = new List(); + bool fromDependencies = false; + bool nonStructuredDataSourceLocal = false; + + foreach (Partition partition in sourceTable.TomTable.Partitions) + { + //Check any objects in source that this partition depends on are also going to be created if not already in target + if (HasBlockingFromDependenciesInSource(sourceTable.Name, partition.Name, CalcDependencyObjectType.Partition, ref warningObjectList, out bool nonStructuredDataSource)) + { + fromDependencies = true; + if (nonStructuredDataSource) + nonStructuredDataSourceLocal = true; + } + + //For old non-M partitions, check if data source references exist + if (HasBlockingOldPartitionDependency(partition, ref warningObjectList)) + fromDependencies = true; //Need if clause in case last of n partitions has no dependencies and sets back to true + } + + if (!fromDependencies) + { + _targetTabularModel.CreateTable(sourceTable); + OnValidationMessage(new ValidationMessageEventArgs($"Create table '{comparisonObject.SourceObjectName}'.", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + else + { + if (!nonStructuredDataSourceLocal) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create table {comparisonObject.SourceObjectName} because it depends on the following objects, which (considering changes) are missing from target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create table {comparisonObject.SourceObjectName} because it depends on the following objects, which (considering changes) are missing from target and/or depend on a structured data source that is provider in the target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + } + } + } + + private void UpdateTable(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Table && comparisonObject.MergeAction == MergeAction.Update) + { + Table tableSource = _sourceTabularModel.Tables.FindByName(comparisonObject.SourceObjectName); + Table tableTarget = _targetTabularModel.Tables.FindByName(comparisonObject.TargetObjectName); + List warningObjectList = new List(); + bool fromDependencies = false; + bool nonStructuredDataSourceLocal = false; + + //Will this table retain partitions? If yes, don't need to bother with source dependency (target dependency checking will cover for deletes). + if (!_targetTabularModel.CanRetainPartitions(tableSource, tableTarget, out string retainPartitionsMessageTemp)) + { + //Check any objects in source that this table depends on are also going to be created/updated if not already in target + foreach (Partition partition in tableSource.TomTable.Partitions) + { + if (HasBlockingFromDependenciesInSource(tableSource.Name, partition.Name, CalcDependencyObjectType.Partition, ref warningObjectList, out bool nonStructuredDataSource)) + { + fromDependencies = true; + if (nonStructuredDataSource) + nonStructuredDataSourceLocal = true; + } + + //For old non-M partitions, check if data source references exist + if (HasBlockingOldPartitionDependency(partition, ref warningObjectList)) + fromDependencies = true; //Need if clause in case last of n partitions has no dependencies and sets back to true + } + } + + if (!fromDependencies) + { + _targetTabularModel.UpdateTable(tableSource, tableTarget, out string retainPartitionsMessage); + OnValidationMessage(new ValidationMessageEventArgs($"Update table '{comparisonObject.TargetObjectName}'. {retainPartitionsMessage}", ValidationMessageType.Table, ValidationMessageStatus.Informational)); + } + else + { + if (!nonStructuredDataSourceLocal) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to update table {comparisonObject.TargetObjectName} because version from the source depends on the following objects, which (considering changes) are missing from target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to update table {comparisonObject.TargetObjectName} because version from the source depends on the following objects, which (considering changes) are missing from target and/or depend on a structured data source that is provider in the target: {String.Join(", ", warningObjectList)}.", ValidationMessageType.Table, ValidationMessageStatus.Warning)); + } + } + } + } + + //Relationships + + private void DeleteRelationship(ComparisonObject comparisonObject) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Relationship && comparisonObject.MergeAction == MergeAction.Delete) + { + foreach (Table tableTarget in _targetTabularModel.Tables) + { + Relationship relationshipTarget = tableTarget.Relationships.FindByName(comparisonObject.TargetObjectName.Trim()); + + if (relationshipTarget != null) + { + // Relationship may have already been deleted if parent table was deleted + tableTarget.DeleteRelationship(comparisonObject.TargetObjectInternalName); + break; + } + } + + OnValidationMessage(new ValidationMessageEventArgs($"Delete relationship {comparisonObject.TargetObjectName.Trim()}.", ValidationMessageType.Relationship, ValidationMessageStatus.Informational)); + } + } + + private void CreateRelationship(ComparisonObject comparisonObject, string tableName) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Relationship && comparisonObject.MergeAction == MergeAction.Create) + { + Table tableSource = _sourceTabularModel.Tables.FindByName(tableName); + Table tableTarget = _targetTabularModel.Tables.FindByName(tableName); + Relationship relationshipSource = tableSource.Relationships.FindByInternalName(comparisonObject.SourceObjectInternalName); + Table parentTableSource = _sourceTabularModel.Tables.FindByName(relationshipSource.ToTableName); + + string warningMessage = $"Unable to create relationship {comparisonObject.SourceObjectName.Trim()} because (considering changes) necessary table/column(s) not found in target model."; + if (tableTarget != null && tableTarget.CreateRelationshipWithValidation(relationshipSource, parentTableSource.TomTable, comparisonObject.SourceObjectName.Trim(), ref warningMessage)) + { + OnValidationMessage(new ValidationMessageEventArgs($"Create relationship {comparisonObject.SourceObjectName.Trim()}.", ValidationMessageType.Relationship, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs(warningMessage, ValidationMessageType.Relationship, ValidationMessageStatus.Warning)); + } + } + } + + private void UpdateRelationship(ComparisonObject comparisonObject, string tableName) + { + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Relationship && comparisonObject.MergeAction == MergeAction.Update) + { + Table tableSource = _sourceTabularModel.Tables.FindByName(tableName); + Table tableTarget = _targetTabularModel.Tables.FindByName(tableName); + Relationship relationshipSource = tableSource.Relationships.FindByInternalName(comparisonObject.SourceObjectInternalName); + Table parentTableSource = _sourceTabularModel.Tables.FindByName(relationshipSource.ToTableName); + + string warningMessage = ""; + if (tableTarget.UpdateRelationship(relationshipSource, parentTableSource.TomTable, comparisonObject.SourceObjectName.Trim(), ref warningMessage)) + { + OnValidationMessage(new ValidationMessageEventArgs($"Update relationship {comparisonObject.SourceObjectName.Trim()}.", ValidationMessageType.Relationship, ValidationMessageStatus.Informational)); + } + else + { + OnValidationMessage(new ValidationMessageEventArgs(warningMessage, ValidationMessageType.Relationship, ValidationMessageStatus.Warning)); + } + } + } + + //Measures / KPIs + + private void DeleteMeasure(ComparisonObject comparisonObject) + { + if ((comparisonObject.ComparisonObjectType == ComparisonObjectType.Measure || comparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi) && + comparisonObject.MergeAction == MergeAction.Delete) + { + foreach (Table tableTarget in _targetTabularModel.Tables) + { + Measure measureTarget = tableTarget.Measures.FindByName(comparisonObject.TargetObjectInternalName); + + if (measureTarget != null) + { + // Measure may have already been deleted if parent table was deleted + tableTarget.DeleteMeasure(comparisonObject.TargetObjectInternalName); + break; + } + } + + OnValidationMessage(new ValidationMessageEventArgs($"Delete measure / KPI {comparisonObject.TargetObjectInternalName}.", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + + private void CreateMeasure(ComparisonObject comparisonObject, string tableName) + { + if ((comparisonObject.ComparisonObjectType == ComparisonObjectType.Measure || comparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi) && + comparisonObject.MergeAction == MergeAction.Create) + { + foreach (Table tableInTarget in _targetTabularModel.Tables) + { + Measure measureInTarget = tableInTarget.Measures.FindByName(comparisonObject.SourceObjectInternalName); + + if (measureInTarget != null) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create measure / KPI {comparisonObject.SourceObjectInternalName} because name already exists in target model.", ValidationMessageType.Measure, ValidationMessageStatus.Warning)); + return; + } + } + + Table tableSource = _sourceTabularModel.Tables.FindByName(tableName); + Table tableTarget = _targetTabularModel.Tables.FindByName(tableName); + + if (tableTarget == null) + { + OnValidationMessage(new ValidationMessageEventArgs($"Unable to create measure / KPI {comparisonObject.SourceObjectInternalName} because (considering changes) target table does not exist.", ValidationMessageType.Measure, ValidationMessageStatus.Warning)); + return; + } + + //If we get here, can create measure/kpi + Measure measureSource = tableSource.Measures.FindByName(comparisonObject.SourceObjectInternalName); + tableTarget.CreateMeasure(measureSource.TomMeasure); + OnValidationMessage(new ValidationMessageEventArgs($"Create measure / KPI {comparisonObject.SourceObjectInternalName}.", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + + private void UpdateMeasure(ComparisonObject comparisonObject, string tableName) + { + if ((comparisonObject.ComparisonObjectType == ComparisonObjectType.Measure || comparisonObject.ComparisonObjectType == ComparisonObjectType.Kpi) && + comparisonObject.MergeAction == MergeAction.Update) + { + Table tableSource = _sourceTabularModel.Tables.FindByName(tableName); + Table tableTarget = _targetTabularModel.Tables.FindByName(tableName); + Measure measureSource = tableSource.Measures.FindByName(comparisonObject.SourceObjectInternalName); + + tableTarget.UpdateMeasure(measureSource.TomMeasure); + OnValidationMessage(new ValidationMessageEventArgs($"Update measure / KPI {comparisonObject.SourceObjectInternalName}.", ValidationMessageType.Measure, ValidationMessageStatus.Informational)); + } + } + + #endregion + + /// + /// Update target tabular model with changes defined by actions in ComparisonObject instances. + /// + /// Flag to indicate whether update was successful. + public override bool Update() => _targetTabularModel.Update(); + + /// + /// Gets a collection of ProcessingTable objects depending on Process Affected Tables option. + /// + /// Collection of ProcessingTable objects. + public override ProcessingTableCollection GetTablesToProcess() + { + ProcessingTableCollection tablesToProcess = new ProcessingTableCollection(); + + if (_comparisonInfo.OptionsInfo.OptionProcessingOption != ProcessingOption.DoNotProcess) + { + if (_comparisonInfo.OptionsInfo.OptionAffectedTables) + { + foreach (Core.ComparisonObject comparisonObject in _comparisonObjects) + { + ProcessAffectedTables(comparisonObject, tablesToProcess); + } + } + else + { + foreach (Table table in _targetTabularModel.Tables) + { + tablesToProcess.Add(new ProcessingTable(table.Name, table.InternalName)); + } + } + } + + tablesToProcess.Sort(); + return tablesToProcess; + } + + private void ProcessAffectedTables(Core.ComparisonObject comparisonObject, ProcessingTableCollection tablesToProcess) + { + //Recursively call for multiple levels to ensure catch calculated tables or those child of DataSource + + if (comparisonObject.ComparisonObjectType == ComparisonObjectType.Table && + (comparisonObject.MergeAction == MergeAction.Create || comparisonObject.MergeAction == MergeAction.Update) + ) + { + tablesToProcess.Add(new ProcessingTable(comparisonObject.SourceObjectName, comparisonObject.SourceObjectInternalName)); + } + + foreach (Core.ComparisonObject childComparisonObject in comparisonObject.ChildComparisonObjects) + { + ProcessAffectedTables(childComparisonObject, tablesToProcess); + } + } + + /// + /// Deploy database to target server and perform processing if required. + /// + /// + public override void DatabaseDeployAndProcess(ProcessingTableCollection tablesToProcess) + { + _targetTabularModel.DatabaseDeployAndProcess(tablesToProcess); + } + + /// + /// Stop processing of deployed database. + /// + public override void StopProcessing() + { + _targetTabularModel.StopProcessing(); + } + + /// + /// Generate script of target database including changes. + /// + /// Script. + public override string ScriptDatabase() => _targetTabularModel.ScriptDatabase(); + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_sourceTabularModel != null) + { + _sourceTabularModel.Dispose(); + } + if (_targetTabularModel != null) + { + _targetTabularModel.Dispose(); + } + } + + _disposed = true; + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ComparisonObject.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ComparisonObject.cs new file mode 100644 index 0000000..0fa8e96 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ComparisonObject.cs @@ -0,0 +1,216 @@ +using System; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents source and target objects for comparison, their type and status. This class is for tabular models that use tabular metadata with SSAS compatibility level 1200 or above. + /// + public class ComparisonObject : Core.ComparisonObject + { + private TabularObject _sourceObject; + private TabularObject _targetObject; + + public ComparisonObject( + ComparisonObjectType comparisonObjectType, + ComparisonObjectStatus status, + TabularObject sourceObject, + TabularObject targetObject, + MergeAction mergeAction) : base(comparisonObjectType, status, mergeAction) + { + _sourceObject = sourceObject; + _targetObject = targetObject; + } + + /// + /// Source TabularObject instance for comparison. + /// + public TabularObject SourceObject + { + get { return _sourceObject; } + set { _sourceObject = value; } + } + + /// + /// Name of source TabularObject instance. + /// + public override string SourceObjectName + { + get + { + if (_sourceObject == null) + { + return ""; + } + else + { + if (_comparisonObjectType == ComparisonObjectType.Relationship || _comparisonObjectType == ComparisonObjectType.Measure || _comparisonObjectType == ComparisonObjectType.Kpi) + { + return " " + _sourceObject.Name; + } + else + { + return _sourceObject.Name; + } + } + } + } + + /// + /// Internal name of source TabularObject instance. This can be different than SourceObjectName for Relationship objects where the internal name is the SSDT assigned GUID. + /// + public override string SourceObjectInternalName + { + get + { + if (_sourceObject == null) + { + return ""; + } + else + { + return _sourceObject.InternalName; + } + } + } + + /// + /// Definition of source TabularObject instance. + /// + public override string SourceObjectDefinition + { + get + { + if (_sourceObject == null) + { + return ""; + } + else + { + return _sourceObject.ObjectDefinition; + } + } + } + + /// + /// Target TabularObject instance for comparison. + /// + public TabularObject TargetObject + { + get { return _targetObject; } + set { _targetObject = value; } + } + + /// + /// Name of target TabularObject instance. + /// + public override string TargetObjectName + { + get + { + if (_targetObject == null) + { + return ""; + } + else + { + if (_comparisonObjectType == ComparisonObjectType.Relationship || _comparisonObjectType == ComparisonObjectType.Measure || _comparisonObjectType == ComparisonObjectType.Kpi) + { + return " " + _targetObject.Name; + } + else + { + return _targetObject.Name; + } + } + } + } + + /// + /// Internal name of target TabularObject instance. This can be different than SourceObjectName for Relationship objects where the internal name is the SSDT assigned GUID. + /// + public override string TargetObjectInternalName + { + get + { + if (_targetObject == null) + { + return ""; + } + else + { + return _targetObject.InternalName; + } + } + } + + /// + /// Definition of target TabularObject instance. + /// + public override string TargetObjectDefinition + { + get + { + if (_targetObject == null) + { + return ""; + } + else + { + return _targetObject.ObjectDefinition; + } + } + } + + /// + /// Provides key for CompareTo method. + /// + public override string SortKey() + { + string sortKey = ""; + + switch (this.ComparisonObjectType) + { + //tabular objects + case ComparisonObjectType.DataSource: + sortKey = "A"; + break; + case ComparisonObjectType.Expression: + sortKey = "B"; + break; + case ComparisonObjectType.Table: + sortKey = "C"; + break; + case ComparisonObjectType.Relationship: + sortKey = "D"; + break; + case ComparisonObjectType.Measure: + sortKey = "E"; + break; + case ComparisonObjectType.Kpi: + sortKey = "F"; + break; + case ComparisonObjectType.Action: + sortKey = "G"; + break; + case ComparisonObjectType.Perspective: + sortKey = "H"; + break; + case ComparisonObjectType.Culture: + sortKey = "I"; + break; + case ComparisonObjectType.Role: + sortKey = "J"; + break; + + default: + sortKey = "Z"; + break; + } + sortKey += this.SourceObjectName != "" ? this.SourceObjectName : this.TargetObjectName; + return sortKey; + } + + public override int CompareTo(Core.ComparisonObject other) => string.Compare(this.SortKey(), other.SortKey()); + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Culture.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Culture.cs new file mode 100644 index 0000000..619adda --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Culture.cs @@ -0,0 +1,87 @@ +using Microsoft.AnalysisServices.Tabular; +using Tom=Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model culture with properties and methods for comparison purposes. + /// + public class Culture : TabularObject + { + #region Private Members + + private TabularModel _parentTabularModel; + private Tom.Culture _tomCulture; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the Culture class using multiple parameters. + /// + /// TabularModel object that the Culture object belongs to. + /// Tabular Object Model Culture object abtstracted by the Culture class. + public Culture(TabularModel parentTabularModel, Tom.Culture tomCulture) : base(tomCulture) + { + _parentTabularModel = parentTabularModel; + _tomCulture = tomCulture; + } + + #endregion + + #region Properties + + /// + /// TabularModel object that the Culture object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Tabular Object Model Culture object abtstracted by the Culture class. + /// + public Tom.Culture TomCulture => _tomCulture; + + public override string ToString() => this.GetType().FullName; + + #endregion + + #region Public Methods + + /// + /// Verifies whether another Culture object's translations are included in this one. + /// + /// The other culture for comparison. + /// Returns true if all translations contained. + public bool ContainsOtherCultureTranslations(Culture otherCulture) + { + foreach (ObjectTranslation otherTranslation in otherCulture.TomCulture.ObjectTranslations) + { + bool foundMatch = false; + foreach (ObjectTranslation translation in _tomCulture.ObjectTranslations) + { + if (translation.Object is NamedMetadataObject && + otherTranslation.Object is NamedMetadataObject && + ( + ((NamedMetadataObject)translation.Object).Name == ((NamedMetadataObject)otherTranslation.Object).Name //Name of the object matches + || (translation.Object.ObjectType == ObjectType.Model && otherTranslation.Object.ObjectType == ObjectType.Model) //Model name can legitimately have different names - and there can only be 1 model, so we are OK. + ) && + translation.Object.ObjectType == otherTranslation.Object.ObjectType && //ObjectType like Measure, Table, ... + translation.Property == otherTranslation.Property && //Property like Caption, DisplayFolder, Description + translation.Value == otherTranslation.Value) //Value is the translated value + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + return false; + } + } + return true; + } + + #endregion + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CultureCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CultureCollection.cs new file mode 100644 index 0000000..ec3af97 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/CultureCollection.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Culture objects. + /// + public class CultureCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Culture object if found. Null if not found. + public Culture FindByName(string name) + { + foreach (Culture culture in this) + { + if (culture.Name == name) + { + return culture; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Culture culture in this) + { + if (culture.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Culture object if found. Null if not found. + public Culture FindById(string id) + { + foreach (Culture culture in this) + { + if (culture.InternalName == id) + { + return culture; + } + } + return null; + } + + /// + /// Removes an object from the collection by its internal name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool Remove(string internalName) + { + foreach (Culture culture in this) + { + if (culture.InternalName == internalName) + { + this.Remove(culture); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSource.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSource.cs new file mode 100644 index 0000000..07d4da2 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSource.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Microsoft.AnalysisServices.Tabular; +using Tom=Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model DataSource with properties and methods for comparison purposes. + /// + public class DataSource : TabularObject + { + private TabularModel _parentTabularModel; + private Microsoft.AnalysisServices.Tabular.DataSource _tomDataSource; + + /// + /// Initializes a new instance of the DataSource class using multiple parameters. + /// + /// TabularModel object that the DataSource object belongs to. + /// Tabular Object Model ProviderDataSource object abtstracted by the DataSource class. + public DataSource(TabularModel parentTabularModel, Microsoft.AnalysisServices.Tabular.DataSource dataSource) : base(dataSource) + { + _parentTabularModel = parentTabularModel; + _tomDataSource = dataSource; + } + + /// + /// TabularModel object that the DataSource object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Tabular Object Model ProviderDataSource object abtstracted by the DataSource class. + /// + public Tom.DataSource TomDataSource => _tomDataSource; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSourceCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSourceCollection.cs new file mode 100644 index 0000000..e03df75 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/DataSourceCollection.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of DataSource objects. + /// + public class DataSourceCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// DataSource object if found. Null if not found. + public DataSource FindByName(string name) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Name == name) + { + return dataSource; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveByName(string name) + { + foreach (DataSource dataSource in this) + { + if (dataSource.Name == name) + { + this.Remove(dataSource); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Expression.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Expression.cs new file mode 100644 index 0000000..b25c1ec --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Expression.cs @@ -0,0 +1,36 @@ +using Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model expression with properties and methods for comparison purposes. + /// + public class Expression : TabularObject + { + private TabularModel _parentTabularModel; + private NamedExpression _tomExpression; + + /// + /// Initializes a new instance of the Expression class using multiple parameters. + /// + /// TabularModel object that the Expression object belongs to. + /// Tabular Object Model Expression object abtstracted by the Expression class. + public Expression(TabularModel parentTabularModel, NamedExpression expression) : base(expression) + { + _parentTabularModel = parentTabularModel; + _tomExpression = expression; + } + + /// + /// TabularModel object that the Expression object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Tabular Object Model NamedExpression object abtstracted by the Expression class. + /// + public NamedExpression TomExpression => _tomExpression; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ExpressionCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ExpressionCollection.cs new file mode 100644 index 0000000..d2f56ea --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/ExpressionCollection.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Expression objects. + /// + public class ExpressionCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Expression object if found. Null if not found. + public Expression FindByName(string name) + { + foreach (Expression expression in this) + { + if (expression.Name == name) + { + return expression; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Expression expression in this) + { + if (expression.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Expression object if found. Null if not found. + public Expression FindById(string id) + { + foreach (Expression expression in this) + { + if (expression.InternalName == id) + { + return expression; + } + } + return null; + } + + /// + /// Removes an object from the collection by its internal name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool Remove(string internalName) + { + foreach (Expression expression in this) + { + if (expression.InternalName == internalName) + { + this.Remove(expression); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Measure.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Measure.cs new file mode 100644 index 0000000..b05f977 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Measure.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; +using Microsoft.AnalysisServices.Tabular; +using Tom=Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model measure with properties and methods for comparison purposes. + /// + public class Measure : TabularObject + { + private Table _parentTable; + private Tom.Measure _tomMeasure; + private bool _IsKpi; + + /// + /// Initializes a new instance of the Measure class using multiple parameters. + /// + /// Table object that the measure belongs to. + /// Tabular Object Model Measure object abtstracted by the Measure class. + /// Indicates whether the measure is a KPI. + public Measure(Table parentTable, Tom.Measure tomMeasure, bool isKpi) : base(tomMeasure) + { + _parentTable = parentTable; + _tomMeasure = tomMeasure; + _IsKpi = isKpi; + } + + /// + /// Table object that the Relationship oject belongs to. + /// + public Table ParentTable => _parentTable; + + /// + /// Tabular Object Model Measure object abtstracted by the Measure class. + /// + public Tom.Measure TomMeasure => _tomMeasure; + + /// + /// Name of the table that the Measure oject belongs to. + /// + public string TableName => _tomMeasure.Table.Name; + + /// + /// Boolean indicating if the Measure object is a KPI. + /// + public bool IsKpi + { + get { return _IsKpi; } + set { _IsKpi = value; } + } + + public override string ToString() => this.GetType().FullName; + + /// + /// Find missing calculation dependencies by inspecting the DAX expression for the measure and iterating columns and other measures in the tabular model for validity of the expression. + /// + /// List of missing dependencies to be displayed or logged as warnings. + public List FindMissingMeasureDependencies() + { + List dependencies = new List(); + + using (StringReader lines = new StringReader(_tomMeasure.Expression)) + { + string line = string.Empty; + while ((line = lines.ReadLine()) != null) + { + if (line.TrimStart().Length > 1 && line.TrimStart().Substring(0, 2) != "--") //Ignore comments + { + //Todo2: still need to parse for /* blah */ type comments. Currently can show missing dependency that doesn't apply if within a comment + + string whatsRemainingOfLine = line; + + while (whatsRemainingOfLine.Contains('[') && whatsRemainingOfLine.Contains(']')) + { + int openSquareBracketPosition = whatsRemainingOfLine.IndexOf('[', 0); + //brilliant person at microsoft has ]] instead of ] + int closeSquareBracketPosition = whatsRemainingOfLine.Replace("]]", " ").IndexOf(']', openSquareBracketPosition + 1); + + if (openSquareBracketPosition < closeSquareBracketPosition - 1) + { + string potentialDependency = whatsRemainingOfLine.Substring(openSquareBracketPosition + 1, closeSquareBracketPosition - openSquareBracketPosition - 1); + if (!potentialDependency.Contains('"') && !dependencies.Contains(potentialDependency)) + { + //unbelievable: some genius at m$ did a replace on ] with ]] + dependencies.Add(potentialDependency); + } + } + + whatsRemainingOfLine = whatsRemainingOfLine.Substring(closeSquareBracketPosition + 1); + } + } + } + } + + List missingDependencies = new List(); + foreach (string dependency in dependencies) + { + bool foundDependency = false; + + foreach (Table table in _parentTable.ParentTabularModel.Tables) + { + //Check if another measure or column has same name + if (table.Measures.ContainsName(dependency) || table.TomTable.Columns.ContainsName(dependency)) + { + foundDependency = true; + break; + } + } + + if (!foundDependency) + { + missingDependencies.Add(dependency); + } + } + + return missingDependencies; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/MeasureCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/MeasureCollection.cs new file mode 100644 index 0000000..e51c5bb --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/MeasureCollection.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Measure objects. + /// + public class MeasureCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Measure object if found. Null if not found. + public Measure FindByName(string name) + { + foreach (Measure measure in this) + { + if (measure.Name == name) + { + return measure; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Measure measure in this) + { + if (measure.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Measure objects filtered by the parent table's name. + /// + /// + /// MeasureCollection + public MeasureCollection FilterByTableName(string tableName) + { + MeasureCollection returnMeasures = new MeasureCollection(); + foreach (Measure measure in this) + { + if (measure.TableName == tableName) + { + returnMeasures.Add(measure); + } + } + return returnMeasures; + } + + /// + /// Removes an object from the collection by its name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveByName(string name) + { + foreach (Measure measure in this) + { + if (measure.Name == name) + { + this.Remove(measure); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Perspective.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Perspective.cs new file mode 100644 index 0000000..560a5cb --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Perspective.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.AnalysisServices.Tabular; +using Tom=Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model perspective with properties and methods for comparison purposes. + /// + public class Perspective : TabularObject + { + private TabularModel _parentTabularModel; + private Tom.Perspective _tomPerspective; + + /// + /// Initializes a new instance of the Perspective class using multiple parameters. + /// + /// TabularModel object that the perspective belongs to. + /// Tabular Object Model Perspective object abtstracted by the Perspective class. + public Perspective(TabularModel parentTabularModel, Tom.Perspective tomPerspective): base(tomPerspective) + { + _parentTabularModel = parentTabularModel; + _tomPerspective = tomPerspective; + } + + /// + /// TabularModel object that the Perspective object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Tabular Object Model Perspective object abtstracted by the Perspective class. + /// + public Tom.Perspective TomPerspective => _tomPerspective; + + public override string ToString() => this.GetType().FullName; + + /// + /// Verifies whether another Perspective object's selections are included in this Perspective object. + /// + /// The other Perspective object to be verified. + /// True if otherPerspective matches. False if does not match. + public bool ContainsOtherPerspectiveSelections(Perspective otherPerspective) + { + bool everythingMatches = true; + + //Tables + foreach (PerspectiveTable otherTable in otherPerspective.TomPerspective.PerspectiveTables) + { + bool foundTableMatch = false; + foreach (PerspectiveTable perspectiveTable in _tomPerspective.PerspectiveTables) + { + if (perspectiveTable.Name == otherTable.Name) + { + foundTableMatch = true; + + #region Columns + foreach (PerspectiveColumn otherColumn in otherTable.PerspectiveColumns) + { + bool foundColumnMatch = false; + foreach (PerspectiveColumn perspectiveColumn in perspectiveTable.PerspectiveColumns) + { + if (perspectiveColumn.Name == otherColumn.Name) + { + foundColumnMatch = true; + break; + } + } + if (!foundColumnMatch) + { + everythingMatches = false; + break; + } + } + #endregion + + #region Hierarchies + foreach (PerspectiveHierarchy otherHierarchy in otherTable.PerspectiveHierarchies) + { + bool foundHierarchyMatch = false; + foreach (PerspectiveHierarchy perspectiveHierarchy in perspectiveTable.PerspectiveHierarchies) + { + if (perspectiveHierarchy.Hierarchy.Name == otherHierarchy.Hierarchy.Name) + { + foundHierarchyMatch = true; + break; + } + } + if (!foundHierarchyMatch) + { + everythingMatches = false; + break; + } + } + #endregion + + #region Measures + foreach (PerspectiveMeasure otherMeasure in otherTable.PerspectiveMeasures) + { + bool foundMeasureMatch = false; + foreach (PerspectiveMeasure perspectiveMeasure in perspectiveTable.PerspectiveMeasures) + { + if (perspectiveMeasure.Name == otherMeasure.Name) + { + foundMeasureMatch = true; + break; + } + } + if (!foundMeasureMatch) + { + everythingMatches = false; + break; + } + } + #endregion + + } + if (!everythingMatches) break; + } + + if (!foundTableMatch) + { + everythingMatches = false; + break; + } + } + + return everythingMatches; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/PerspectiveCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/PerspectiveCollection.cs new file mode 100644 index 0000000..4b3dbfb --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/PerspectiveCollection.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Perspective objects. + /// + public class PerspectiveCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Perspective object if found. Null if not found. + public Perspective FindByName(string name) + { + foreach (Perspective perspective in this) + { + if (perspective.Name == name) + { + return perspective; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Perspective perspective in this) + { + if (perspective.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Culture object if found. Null if not found. + public Perspective FindById(string id) + { + foreach (Perspective perspective in this) + { + if (perspective.InternalName == id) + { + return perspective; + } + } + return null; + } + + /// + /// Removes an object from the collection by its internal name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool Remove(string internalName) + { + foreach (Perspective perspective in this) + { + if (perspective.InternalName == internalName) + { + this.Remove(perspective); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Relationship.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Relationship.cs new file mode 100644 index 0000000..054bb94 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Relationship.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model relationship with properties and methods for comparison purposes. + /// + public class Relationship : TabularObject + { + private Table _table; + private SingleColumnRelationship _tomRelationship; + private string _relationshipName; + private bool _copiedFromSource; + private bool _modifiedInternalName; + private string _oldInternalName; + + /// + /// Initializes a new instance of the Relationship class using multiple parameters. + /// + /// Table object that the Relationship belongs to. + /// Tabular Object Model SingleColumnRelationship object abtstracted by the Relationship class. + /// Boolean indicating whether the relationship was copied from the source TabularModel object. + /// Boolean indicating whether the TOM Relationship object Name property was changed to avoid name conflict. + /// If the TOM Relationship object Name property was changed, this parameter shows the old value. + public Relationship(Table table, SingleColumnRelationship tomRelationship, bool copiedFromSource = false, bool modifiedInternalName = false, string oldInternalName = "") + : base(tomRelationship) + { + _table = table; + _tomRelationship = tomRelationship; + + //_relationshipName = $"'{_relationship.FromTable.Name}'->'{_relationship.ToTable.Name}'"; + //_relationshipName = $"[{_relationship.FromColumn.Name}]->'{_relationship.ToTable.Name}'[{_relationship.ToColumn.Name}]"; + _relationshipName = $"'{_tomRelationship.FromTable.Name}'[{_tomRelationship.FromColumn.Name}]->'{_tomRelationship.ToTable.Name}'[{_tomRelationship.ToColumn.Name}]"; + + _copiedFromSource = copiedFromSource; + _modifiedInternalName = modifiedInternalName; + _oldInternalName = oldInternalName; + } + + /// + /// Table object that the Relationship oject belongs to. + /// + public Table Table => _table; + + /// + /// Tabular Object Model SingleColumnRelationship object abtstracted by the Relationship class. + /// + public SingleColumnRelationship TomRelationship + { + get { return _tomRelationship; } + set { _tomRelationship = value; } + } + + /// + /// Name of the Relationship object. Uses a friendly format. + /// + public override string Name => _relationshipName; + + /// + /// The TOM Relationship object Name property, which is not displayed to users as its of GUID format. + /// + public override string InternalName => _tomRelationship.Name; + + /// + /// Name of the from table for the Relationship object. + /// + public string FromTableName => _tomRelationship.FromTable.Name; + + /// + /// Name of the to table for the Relationship object. + /// + public string ToTableName => _tomRelationship.ToTable.Name; + + /// + /// Boolean indicating whether the relationship was copied from the source TabularModel object. + /// + public bool CopiedFromSource => _copiedFromSource; + + /// + /// Boolean indicating whether the TOM Relationship object Name property was changed to avoid name conflict. + /// + public bool ModifiedInternalName => _modifiedInternalName; + + /// + /// If the TOM Relationship object Name property was changed, this parameter shows the old value. + /// + public string OldInternalName => _oldInternalName; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipChain.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipChain.cs new file mode 100644 index 0000000..90a23be --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipChain.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a chain of RelationshipLink objects, used to detect ambiguity in relationship paths. + /// + public class RelationshipChain : List + { + /// + /// Find end table by name. + /// + /// Name of the end table. + /// RelationshipLink object if found; null if not found. + public RelationshipLink FindByEndTableName(string endTableName) + { + foreach (RelationshipLink ReferencedTable in this) + { + if (ReferencedTable.EndTable.Name == endTableName) + { + return ReferencedTable; + } + } + return null; + } + + /// + /// Find the root RelationshipLink in the chain. + /// + /// RelationshipLink object if found; null if not found. + public RelationshipLink FindRoot() + { + foreach (RelationshipLink ReferencedTable in this) + { + if (ReferencedTable.Root) + { + return ReferencedTable; + } + } + return null; + } + + /// + /// Check if chain of RelationshipLink objects contains an end table with specified name. + /// + /// Name of the end table. + /// Boolean indicating if the end table was found. + public bool ContainsEndTableName(string endTableName) + { + foreach (RelationshipLink ReferencedTable in this) + { + if (ReferencedTable.EndTable.Name == endTableName) + { + return true; + } + } + return false; + } + + /// + /// Remove end table from chain of RelationshipLink objects. + /// + /// Name of end table to remvoe. + /// Boolean indicating if the end table was successfully removed. + public bool RemoveByEndTableName(string endTableName) + { + foreach (RelationshipLink ReferencedTable in this) + { + if (ReferencedTable.EndTable.Name == endTableName) + { + this.Remove(ReferencedTable); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipCollection.cs new file mode 100644 index 0000000..53edb33 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipCollection.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Relationship objects. + /// + public class RelationshipCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Relationship object if found. Null if not found. + public Relationship FindByName(string name) + { + foreach (Relationship relationship in this) + { + if (relationship.Name == name) + { + return relationship; + } + } + return null; + } + + /// + /// Find an object in the collection by internal name. + /// + /// + /// Relationship object if found. Null if not found. + public Relationship FindByInternalName(string internalName) + { + foreach (Relationship relationship in this) + { + if (relationship.InternalName == internalName) + { + return relationship; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Relationship relationship in this) + { + if (relationship.Name == name.Trim()) + { + return true; + } + } + return false; + } + + /// + /// A Boolean specifying whether the collection contains object by internal name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsInternalName(string internalName) + { + foreach (Relationship relationship in this) + { + if (relationship.InternalName == internalName) + { + return true; + } + } + return false; + } + + /// + /// Removes an object from the collection by its internal name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveByInternalName(string internalName) + { + foreach (Relationship relationship in this) + { + if (relationship.InternalName == internalName) + { + this.Remove(relationship); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipLink.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipLink.cs new file mode 100644 index 0000000..8f8c21e --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RelationshipLink.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a link in a relationship chain, used to detect ambiguity in relationship paths. + /// + public class RelationshipLink + { + private Table _beginTable; + private Table _endTable; + private bool _root; + private string _tablePath; + private Relationship _filteringRelationship; + + /// + /// Initializes a new instance of the RelationshipLink class using multiple parameters. + /// + /// Table object representing begin table. + /// Table object representing end table. + /// Boolean indicating if the root link in a the relationship chain. + /// Recursive path to the preceding table in the relationship chain. + /// Relationship object for the relationship link. + public RelationshipLink(Table beginTable, Table endTable, bool root, string precedingTablePath, Relationship filteringRelationship) + { + _beginTable = beginTable; + _endTable = endTable; + _root = root; + if (root) + { + _tablePath = $"'{beginTable.Name}'->'{endTable.Name}'"; + } + else + { + _tablePath = $"{precedingTablePath}->'{endTable.Name}'"; + } + _filteringRelationship = filteringRelationship; + } + + /// + /// BeginTable might be the From or the To table in TOM Relationship object; it depends on CrossFilterDirection and specific relationship direction. + /// + public Table BeginTable => _beginTable; + + /// + /// EndTable might be the From or the To table in TOM Relationship object; it depends on CrossFilterDirection and specific relationship direction. + /// + public Table EndTable => _endTable; + + /// + /// Boolean indicating if the root link in a the relationship chain. + /// + public bool Root => _root; + + /// + /// Recursive path to the preceding table in the relationship chain. + /// + public string TablePath => _tablePath; + + /// + /// Relationship object for the relationship link. + /// + public Relationship FilteringRelationship => _filteringRelationship; + + public override string ToString() => _tablePath; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Role.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Role.cs new file mode 100644 index 0000000..ba38498 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Role.cs @@ -0,0 +1,36 @@ +using Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model role with properties and methods for comparison purposes. + /// + public class Role : TabularObject + { + private TabularModel _parentTabularModel; + private ModelRole _tomRole; + + /// + /// Initializes a new instance of the Role class using multiple parameters. + /// + /// TabularModel object that the Role object belongs to. + /// Tabular Object Model Role object abtstracted by the Role class. + public Role(TabularModel parentTabularModel, ModelRole role) : base(role) + { + _parentTabularModel = parentTabularModel; + _tomRole = role; + } + + /// + /// TabularModel object that the Role object belongs to. + /// + public TabularModel ParentTabularModel => _parentTabularModel; + + /// + /// Tabular Object Model ModelRole object abtstracted by the Role class. + /// + public ModelRole TomRole => _tomRole; + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RoleCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RoleCollection.cs new file mode 100644 index 0000000..de32b2e --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/RoleCollection.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Role objects. + /// + public class RoleCollection : List + { + /// + /// Find an object in the collection by name. + /// + /// + /// Role object if found. Null if not found. + public Role FindByName(string name) + { + foreach (Role role in this) + { + if (role.Name == name) + { + return role; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Role role in this) + { + if (role.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Find an object in the collection by Id. + /// + /// + /// Role object if found. Null if not found. + public Role FindById(string id) + { + foreach (Role role in this) + { + if (role.InternalName == id) + { + return role; + } + } + return null; + } + + /// + /// Removes an object from the collection by its internal name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool Remove(string internalName) + { + foreach (Role role in this) + { + if (role.InternalName == internalName) + { + this.Remove(role); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Table.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Table.cs new file mode 100644 index 0000000..26cac2d --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/Table.cs @@ -0,0 +1,410 @@ +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(); + + /// + /// 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; + _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; + + /// + /// Tabular Object Model Table object abtstracted by the Table class. + /// + public Tom.Table TomTable => _tomTable; + + private void PopulateProperties() + { + base.RemovePropertyFromObjectDefinition("measures"); + + _partitionsDefinition = ""; + _dataSourceName = ""; + bool hasMOrQueryPartition = 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) + { + if (partition.SourceType == PartitionSourceType.M) + { + hasMOrQueryPartition = 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) + { + hasMOrQueryPartition = true; + _dataSourceName = ((QueryPartitionSource)partition.Source).DataSource.Name; + break; + } + } + + if (hasMOrQueryPartition) + { + _partitionsDefinition = base.RetrievePropertyFromObjectDefinition("partitions"); + + //Option to hide partitions only applies to M and query 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)); + } + } + + #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 FindFilteringRelationships() + { + //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 || + (relationship.ToTableName == this.Name && relationship.TomRelationship.CrossFilteringBehavior == CrossFilteringBehavior.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; + } + + // 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); + } + + #endregion + + public override string ToString() => this.GetType().FullName; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TableCollection.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TableCollection.cs new file mode 100644 index 0000000..a848c31 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TableCollection.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.AnalysisServices.Tabular; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a collection of Table objects. + /// + public class TableCollection : List
+ { + /// + /// Find an object in the collection by name. + /// + /// + /// Table object if found. Null if not found. + public Table FindByName(string name) + { + foreach (Table table in this) + { + if (table.Name == name) + { + return table; + } + } + return null; + } + + /// + /// A Boolean specifying whether the collection contains object by name. + /// + /// + /// True if the object is found, or False if it's not found. + public bool ContainsName(string name) + { + foreach (Table table in this) + { + if (table.Name == name) + { + return true; + } + } + return false; + } + + /// + /// Returns a collection of Table objects filtered by the parent DataSource's name. + /// + /// + /// TableCollection + public TableCollection FilterByDataSource(Microsoft.AnalysisServices.Tabular.DataSource dataSource) + { + TableCollection returnTables = new TableCollection(); + foreach (Table table in this) + { + if (table.DataSourceName == dataSource.Name) + { + returnTables.Add(table); + } + } + return returnTables; + } + + /// + /// Returns a collection of Table objects that do not have a DataSource associated with them. These can be calculated tables or tables with M partitions that do not refer to a DataSource. + /// + /// + public TableCollection WithoutDataSource(Model model) + { + TableCollection tablesWithDataSource = new TableCollection(); + foreach (Microsoft.AnalysisServices.Tabular.DataSource dataSource in model.DataSources) + { + tablesWithDataSource.AddRange(this.FilterByDataSource(dataSource)); + } + + TableCollection tablesWithoutDataSource = new TableCollection(); + foreach (Table table in this) + { + if (!tablesWithDataSource.ContainsName(table.Name)) + { + tablesWithoutDataSource.Add(table); + } + } + + return tablesWithoutDataSource; + } + + /// + /// Removes an object from the collection by its name. + /// + /// + /// True if the object was removed, or False if was not found. + public bool RemoveByName(string name) + { + foreach (Table table in this) + { + if (table.Name == name) + { + this.Remove(table); + return true; + } + } + return false; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularMetadataClassDiagram.cd b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularMetadataClassDiagram.cd new file mode 100644 index 0000000..e52b875 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularMetadataClassDiagram.cd @@ -0,0 +1,192 @@ + + + + + + + + + + + + ACAAAAAAAAAAAAAEACAAAAJAAAAAAAAAAAAAAAAAAAA= + TabularMetadata\Connection.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAACAAA= + TabularMetadata\ConnectionCollection.cs + + + + + + + + + + + + Tom\Database.cs + + + + + QAUQFQEAEGQMAEMGViBAVZUQBgCHEIwCZAILACBRBAA= + TabularMetadata\TabularModel.cs + + + + + + + + + + IASEAIAAECAEAADDBAAYBoQCAgAAGAAAUJAAAAAAAAA= + TabularMetadata\Comparison.cs + + + + + + + + + + + AAAAAABEAAAAACQQAAAAAEAIAAAACAQAAAAAgAEAABA= + TabularMetadata\ComparisonObject.cs + + + + + + + AAAAAAAAEAAAAAAEAAQAIAAAAAAAAIAAAEAADQAAAAA= + TabularMetadata\Measure.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAIAAAABgAAAAAAAAAAACAAA= + TabularMetadata\MeasureCollection.cs + + + + + + AAAAAAAACAAAAAAEAAAAAAJAEAAAAAgAAAAAAAAAAAA= + TabularMetadata\Perspective.cs + + + + + + AAAAAAAAAAAAAAAAAAAABAAAAABBAAAAAAAAAAACAAA= + TabularMetadata\PerspectiveCollection.cs + + + + + + AABAAAAECAAAggAEAgEAQgUAAAAAIAgAAAACAAAAAAA= + TabularMetadata\Relationship.cs + + + + + + AAAAIAAAAAAABAAAAEAAAAAAAABAAAAAAAAAAAACAAA= + TabularMetadata\RelationshipCollection.cs + + + + + + AAAAAAAAAAAAAAAEggAAAAJAAAAAAAAAAAAAAAAAAAA= + TabularMetadata\Role.cs + + + + + + AAAAAAAAAAAAAAAAAAAABAAAAABBAAAAAAAAAAACAAA= + TabularMetadata\RoleCollection.cs + + + + + + AIAAAACIAAEAAAAEAAAAgCZAADACAAAAADAAAQIAABQ= + TabularMetadata\Table.cs + + + + + + AAABABAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAACAAA= + TabularMetadata\TableCollection.cs + + + + + + AAAAAAAAAAAAAAAEAAEAAAJAAAAACAAAAAAAAAAAAAE= + TabularMetadata\Culture.cs + + + + + + AAAAAAAAAAAAAAAAAAAABAAAAABBAAAAAAAAAAACAAA= + TabularMetadata\CultureCollection.cs + + + + + + AAAAIAAAAAAAAAAAkgAAAAQEAAAgAAAAAAAAAAAAAAA= + TabularMetadata\TabularObject.cs + + + + + + AAQAAAAAECAEQBBCAAIABIAAAAAAAAAAQAAAAAIGAIg= + Core\Comparison.cs + + + + + + AAAABAAEAAAAACAAAEAAAAAIAAAACAQAQAAAgAEAABA= + Core\ComparisonObject.cs + + + + + + AAAAAAAAAAAAAAAAAAABABAAAAAAAAAAAAAAAAAAAAA= + Core\Comparison.cs + + + + + + AAQAAAAAAAAAAAAAAAAAAAAAAAEAAEAAAAAAAAAAEAA= + Core\Comparison.cs + + + + + + AAAAAAAAgIAAAAAAAAAAAAgAAAAAQAAAAAAAAAgAAAA= + Core\Comparison.cs + + + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularModel.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularModel.cs new file mode 100644 index 0000000..762208e --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularModel.cs @@ -0,0 +1,1869 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.AnalysisServices.Tabular; +using Tom = Microsoft.AnalysisServices.Tabular; +using Amo = Microsoft.AnalysisServices; +using Newtonsoft.Json.Linq; +using BismNormalizer.TabularCompare.Core; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Abstraction of a tabular model table with properties and methods for comparison purposes. This class can represent a database on a server, or a project in Visual Studio. + /// + public class TabularModel : IDisposable + { + #region Private Members + + private Comparison _parentComparison; + private ConnectionInfo _connectionInfo; + private ComparisonInfo _comparisonInfo; + private Server _server; + private Database _database; + private DataSourceCollection _dataSources = new DataSourceCollection(); + private TableCollection _tables = new TableCollection(); + private ExpressionCollection _expressions = new ExpressionCollection(); + private PerspectiveCollection _perspectives = new PerspectiveCollection(); + private CultureCollection _cultures = new CultureCollection(); + private RoleCollection _roles = new RoleCollection(); + private CalcDependencyCollection _calcDependencies = new CalcDependencyCollection(); + private List _tomPerspectivesBackup; + private List _tomCulturesBackup; + private List _tomRolesBackup; + private bool _disposed = false; + + #endregion + + /// + /// Initializes a new instance of the TabularModel class using multiple parameters. + /// + /// Comparison object that the tabular model belongs to. + /// ConnectionInfo object for the tabular model. + /// ComparisonInfo object for the tabular model. + public TabularModel(Comparison parentComparison, ConnectionInfo connectionInfo, ComparisonInfo comparisonInfo) + { + _parentComparison = parentComparison; + _connectionInfo = connectionInfo; + _comparisonInfo = comparisonInfo; + } + + /// + /// Connect to SSAS server and instantiate properties of the TabularModel object. + /// + public void Connect() + { + this.Disconnect(); + + _server = new Server(); + _server.Connect($"Provider=MSOLAP;Data Source={_connectionInfo.ServerName}"); + + _database = _server.Databases.FindByName(_connectionInfo.DatabaseName); + if (_database == null) + { + //Don't need try to load from project here as will already be done before instantiated Comparison + throw new Amo.ConnectionException($"Could not connect to database {_connectionInfo.DatabaseName}"); + } + InitializeCalcDependencies(); + + //Shell model + foreach (Tom.DataSource dataSource in _database.Model.DataSources) + { + if (dataSource.Type == DataSourceType.Provider || dataSource.Type == DataSourceType.Structured) + { + _dataSources.Add(new DataSource(this, dataSource)); + } + } + foreach (Tom.Table table in _database.Model.Tables) + { + _tables.Add(new Table(this, table)); + } + foreach (Tom.NamedExpression expression in _database.Model.Expressions) + { + _expressions.Add(new Expression(this, expression)); + } + foreach (ModelRole role in _database.Model.Roles) + { + //Workaround for AAD role members - todo delete if changed in Azure AS + if (_parentComparison?.TargetTabularModel?.ConnectionInfo?.ServerName.Substring(0, 7) == "asazure") + { + List membersToAdd = new List(); + foreach (ModelRoleMember member in role.Members) + { + if (member is ExternalModelRoleMember && ((ExternalModelRoleMember)member).IdentityProvider == "AzureAD" && member.MemberID != null) + { + //AAD member from SSAS to Azure AS + ExternalModelRoleMember externalMemberOld = (ExternalModelRoleMember)member; + ExternalModelRoleMember externalMemberToAdd = new ExternalModelRoleMember(); + externalMemberOld.CopyTo(externalMemberToAdd); + externalMemberToAdd.MemberID = null; + membersToAdd.Add(externalMemberToAdd); + } + } + foreach (ExternalModelRoleMember memberToAdd in membersToAdd) + { + role.Members.Remove(memberToAdd.Name); + role.Members.Add(memberToAdd); + } + } + else + { + List membersToAdd = new List(); + foreach (ModelRoleMember member in role.Members) + { + if (member is ExternalModelRoleMember && ((ExternalModelRoleMember)member).IdentityProvider == "AzureAD" && String.IsNullOrEmpty(member.MemberID)) + { + //AAD member from Azure AS to SSAS + ExternalModelRoleMember externalMemberOld = (ExternalModelRoleMember)member; + ExternalModelRoleMember externalMemberToAdd = new ExternalModelRoleMember(); + externalMemberOld.CopyTo(externalMemberToAdd); + externalMemberToAdd.MemberID = member.MemberName; //*** + membersToAdd.Add(externalMemberToAdd); + } + } + foreach (ExternalModelRoleMember memberToAdd in membersToAdd) + { + role.Members.Remove(memberToAdd.Name); + role.Members.Add(memberToAdd); + } + } + + _roles.Add(new Role(this, role)); + } + foreach (Tom.Perspective perspective in _database.Model.Perspectives) + { + _perspectives.Add(new Perspective(this, perspective)); + } + foreach (Tom.Culture culture in _database.Model.Cultures) + { + _cultures.Add(new Culture(this, culture)); + } + } + + private void InitializeCalcDependencies() + { + _calcDependencies.Clear(); + string command = "SELECT * FROM $System.DISCOVER_CALC_DEPENDENCY WHERE OBJECT_TYPE = 'PARTITION' OR OBJECT_TYPE = 'M_EXPRESSION';"; + XmlNodeList rows = Core.Comparison.ExecuteXmlaCommand(_server, _connectionInfo.DatabaseName, command); + + foreach (XmlNode row in rows) + { + string objectType = ""; + string tableName = ""; + string objectName = ""; + string expression = ""; + string referencedObjectType = ""; + string referencedTableName = ""; + string referencedObjectName = ""; + string referencedExpression = ""; + + foreach (XmlNode col in row.ChildNodes) + { + if (col.Name == "OBJECT_TYPE") objectType = col.InnerText; + if (col.Name == "TABLE") tableName = col.InnerText; + if (col.Name == "OBJECT") objectName = col.InnerText; + if (col.Name == "EXPRESSION") expression = col.InnerText; + if (col.Name == "REFERENCED_OBJECT_TYPE") referencedObjectType = col.InnerText; + if (col.Name == "REFERENCED_TABLE") referencedTableName = col.InnerText; + if (col.Name == "REFERENCED_OBJECT") referencedObjectName = col.InnerText; + if (col.Name == "REFERENCED_EXPRESSION") referencedExpression = col.InnerText; + } + + _calcDependencies.Add(new CalcDependency(this, + objectType, + tableName, + objectName, + expression, + referencedObjectType, + referencedTableName, + referencedObjectName, + referencedExpression + ) + ); + } + } + + /// + /// Disconnect from the SSAS server. + /// + public void Disconnect() + { + try + { + if (_server != null) _server.Disconnect(); + } + catch { } + } + + #region Properties + + /// + /// Tabular Object Model Database object abtstracted by the TabularModel class. + /// + public Database TomDatabase + { + get { return _database; } + set { _database = value; } + } + + /// + /// Collection of DataSources for the TabularModel object. + /// + public DataSourceCollection DataSources => _dataSources; + + /// + /// Collection of tables for the TabularModel object. + /// + public TableCollection Tables => _tables; + + /// + /// Collection of expressions for the TabularModel object. + /// + public ExpressionCollection Expressions => _expressions; + + /// + /// Collection of perspectives for the TabularModel object. + /// + public PerspectiveCollection Perspectives => _perspectives; + + /// + /// Collection of cultures for the TabularModel object. + /// + public CultureCollection Cultures => _cultures; + + /// + /// Collection of roles for the TabularModel object. + /// + public RoleCollection Roles => _roles; + + /// + /// Collection of M dependencies for the TabularModel object. + /// + public CalcDependencyCollection MDependencies => _calcDependencies; + + /// + /// ConnectionInfo object for the tabular model. + /// + public ConnectionInfo ConnectionInfo => _connectionInfo; + + /// + /// ComparisonInfo object for the tabular model. + /// + public ComparisonInfo ComparisonInfo => _comparisonInfo; + + #endregion + + #region Relationships + + /// + /// Check whether the TabularModel object contains a relationship. + /// + /// The internal name of the relationship. + /// True if found; false if not. + public bool ContainsRelationshipByInternalName(string relationshipInternalName) + { + bool foundRelationship = false; + + foreach (Table table in _tables) + { + foreach (Relationship relationship in table.Relationships) + { + if (relationship.InternalName == relationshipInternalName) + { + foundRelationship = true; + break; + } + } + if (foundRelationship) + { + break; + } + } + + return foundRelationship; + } + + #endregion + + #region DataSources + + /// + /// Delete DataSource associated with the TabularModel object. + /// + /// The name of the DataSource to be deleted. + public void DeleteDataSource(string name) + { + if (_database.Model.DataSources.ContainsName(name)) + { + _database.Model.DataSources.Remove(name); + } + + // shell model + if (_dataSources.ContainsName(name)) + { + _dataSources.RemoveByName(name); + } + } + + /// + /// Create DataSource associated with the TabularModel object. + /// + /// DataSource object from the source tabular model to be created in the target. + public void CreateDataSource(DataSource dataSourceSource) + { + if (dataSourceSource.TomDataSource is ProviderDataSource) + { + ProviderDataSource providerTarget = new ProviderDataSource(); + dataSourceSource.TomDataSource.CopyTo(providerTarget); + + _database.Model.DataSources.Add(providerTarget); + + // shell model + _dataSources.Add(new DataSource(this, providerTarget)); + } + else + { + StructuredDataSource structuredTarget = new StructuredDataSource(); + dataSourceSource.TomDataSource.CopyTo(structuredTarget); + + _database.Model.DataSources.Add(structuredTarget); + + // shell model + _dataSources.Add(new DataSource(this, structuredTarget)); + } + } + + /// + /// Update DataSource associated with the TabularModel object. + /// + /// DataSource object from the source tabular model to be updated in the target. + /// DataSource object in the target tabular model to be updated. + public void UpdateDataSource(DataSource dataSourceSource, DataSource dataSourceTarget) + { + if (dataSourceSource.TomDataSource is ProviderDataSource && dataSourceTarget.TomDataSource is ProviderDataSource) + { + ProviderDataSource providerSource = (ProviderDataSource)dataSourceSource.TomDataSource; + ProviderDataSource providerTarget = (ProviderDataSource)dataSourceTarget.TomDataSource; + + providerTarget.Description = providerSource.Description; + providerTarget.ConnectionString = providerSource.ConnectionString; + providerTarget.ImpersonationMode = providerSource.ImpersonationMode; + providerTarget.Account = providerSource.Account; + } + else if (dataSourceSource.TomDataSource is StructuredDataSource) //(can replace a provider with a structured, but not vice versa) && dataSourceTarget.TomDataSource is StructuredDataSource) + { + DeleteDataSource(dataSourceTarget.Name); + CreateDataSource(dataSourceSource); + } + else + { + throw new NotImplementedException(); + } + } + + #endregion + + #region Tables + + /// + /// Delete Table object, it's table in the underlying model, and all associated relationships. + /// + /// Name of the table to be deleted. + /// Collection of all associated relationships that had to be deleted. Useful if updating tables as then need to add back. + public List DeleteTable(string name) + { + List deletedRelationships = new List(); + + // shell model + if (_tables.ContainsName(name)) + { + deletedRelationships = _tables.FindByName(name).DeleteAllAssociatedRelationships(); + _tables.RemoveByName(name); + } + + if (_database.Model.Tables.Contains(name)) + { + _database.Model.Tables.Remove(name); + } + + return deletedRelationships; + } + + /// + /// Create table associated with the TabularModel object. + /// + /// Table object from the source tabular model to be created in the target. + public void CreateTable(Table tableSource) + { + Tom.Table tomTableTarget = new Tom.Table(); + tableSource.TomTable.CopyTo(tomTableTarget); + + //decouple from original model to the current one + foreach (Partition partition in tomTableTarget.Partitions) + { + if (partition.SourceType == PartitionSourceType.Query) + { + QueryPartitionSource queryPartition = ((QueryPartitionSource)partition.Source); + string dataSourceName = queryPartition.DataSource.Name; + queryPartition.DataSource = _dataSources.FindByName(dataSourceName).TomDataSource; + } + } + + tomTableTarget.Measures.Clear(); //Measures will be added separately later + + _database.Model.Tables.Add(tomTableTarget); + _tables.Add(new Table(this, tomTableTarget)); + } + + /// + /// Update table associated with the TabularModel object. + /// + /// Table object from the source tabular model to be updated in the target. + /// Table object in the target tabular model to be updated. + public void UpdateTable(Table tableSource, Table tableTarget, out string retainPartitionsMessage) + { + bool canRetainPartitions = CanRetainPartitions(tableSource, tableTarget, out retainPartitionsMessage); + Tom.Table tomTableTargetOrig = tableTarget.TomTable.Clone(); + List tomRelationshipsToAddBack = DeleteTable(tableTarget.Name); + CreateTable(tableSource); + + //get back the newly created table + tableTarget = _tables.FindByName(tableSource.Name); + + //retain partitions if possible + if (canRetainPartitions) + RetainPartitions(tableTarget, tomTableTargetOrig, out retainPartitionsMessage); + + //add back deleted relationships where possible + foreach (SingleColumnRelationship tomRelationshipToAddBack in tomRelationshipsToAddBack) + { + Table fromTable = _tables.FindByName(tomRelationshipToAddBack.FromTable.Name); + Table toTable = _tables.FindByName(tomRelationshipToAddBack.ToTable.Name); + + if (fromTable != null && fromTable.TomTable.Columns.ContainsName(tomRelationshipToAddBack.FromColumn.Name) && + toTable != null & toTable.TomTable.Columns.ContainsName(tomRelationshipToAddBack.ToColumn.Name)) + { + //decouple from original table to the current one + tomRelationshipToAddBack.FromColumn = fromTable.TomTable.Columns.Find(tomRelationshipToAddBack.FromColumn.Name); + tomRelationshipToAddBack.ToColumn = toTable.TomTable.Columns.Find(tomRelationshipToAddBack.ToColumn.Name); + + fromTable.CreateRelationship(tomRelationshipToAddBack); + } + } + + //add back measures + foreach (Tom.Measure tomMeasureToAddBack in tomTableTargetOrig.Measures) + { + tableTarget.CreateMeasure(tomMeasureToAddBack); + } + } + + public bool CanRetainPartitions(Table tableSource, Table tableTarget, out string retainPartitionsMessage) + { + retainPartitionsMessage = ""; + + //only applies to db deployment, and need option checked + if (!_comparisonInfo.OptionsInfo.OptionRetainPartitions) + return false; + + //both tables need to have M or query partitions. Also type needs to match (won't copy query partition to M table). If a table has no partitions, do nothing. + PartitionSourceType sourceTypeTarget = PartitionSourceType.None; + foreach (Partition partition in tableSource.TomTable.Partitions) + { + sourceTypeTarget = partition.SourceType; + break; + } + if (!(sourceTypeTarget == PartitionSourceType.M || sourceTypeTarget == PartitionSourceType.Query)) + { + retainPartitionsMessage = $"Retain partitions not applicable to partition types."; + return false; + } + + PartitionSourceType sourceTypeOrig = PartitionSourceType.None; + foreach (Partition partitionOrig in tableTarget.TomTable.Partitions) + { + sourceTypeOrig = partitionOrig.SourceType; + break; + } + if (!(sourceTypeOrig == PartitionSourceType.M || sourceTypeOrig == PartitionSourceType.Query)) + { + retainPartitionsMessage = $"Retain partitions not applicable to partition types."; + return false; + } + + if (sourceTypeOrig != sourceTypeTarget) + { + retainPartitionsMessage = $"Retain partitions not applied because source partition type is {sourceTypeTarget.ToString()} and target partition type is {sourceTypeOrig.ToString()}."; + return false; + } + + if (tableSource.PartitionsDefinition == tableTarget.PartitionsDefinition) + { + retainPartitionsMessage = "Source & target partition definitions match, so retain partitions not necessary."; + return false; + } + + return true; + } + + private void RetainPartitions(Table modifiedTableTarget, Tom.Table tomTableTargetOrig, out string retainPartitionsMessage) + { + //Actually do the retain partitions + retainPartitionsMessage = "Retain target partitions applied: "; + modifiedTableTarget.TomTable.Partitions.Clear(); + foreach (Partition partitionOrig in tomTableTargetOrig.Partitions) + { + Partition partitionTarget = partitionOrig.Clone(); + if (partitionTarget.SourceType == PartitionSourceType.Query) + { + QueryPartitionSource querySource = (QueryPartitionSource)partitionTarget.Source; + querySource.DataSource = _dataSources.FindByName(querySource.DataSource?.Name).TomDataSource; + } + modifiedTableTarget.TomTable.Partitions.Add(partitionTarget); + retainPartitionsMessage += $"{partitionTarget.Name}, "; + } + retainPartitionsMessage = retainPartitionsMessage.Substring(0, retainPartitionsMessage.Length - 2) + "."; + } + + #endregion + + #region Expressions + + /// + /// Delete expression associated with the TabularModel object. + /// + /// Name of the expression to be deleted. + public void DeleteExpression(string name) + { + if (_database.Model.Expressions.Contains(name)) + { + _database.Model.Expressions.Remove(name); + } + + // shell model + if (_expressions.ContainsName(name)) + { + _expressions.Remove(name); + } + } + + /// + /// Create expression associated with the TabularModel object. + /// + /// Tabular Object Model NamedExpression object from the source tabular model to be abstracted in the target. + public void CreateExpression(NamedExpression tomExpressionSource) + { + NamedExpression tomExpressionTarget = new NamedExpression(); + tomExpressionSource.CopyTo(tomExpressionTarget); + _database.Model.Expressions.Add(tomExpressionTarget); + + // shell model + _expressions.Add(new Expression(this, tomExpressionTarget)); + } + + /// + /// Update expression associated with the TabularModel object. + /// + /// Expression object from the source tabular model to be updated in the target. + /// Expression object in the target tabular model to be updated. + public void UpdateExpression(Expression expressionSource, Expression expressionTarget) + { + DeleteExpression(expressionTarget.Name); + CreateExpression(expressionSource.TomExpression); + } + + #endregion + + #region Validation of Relationships + + /// + /// Set relationships copied from source to inactive if cause ambigious paths. + /// + public void ValidateRelationships() + { + foreach (Table beginTable in _tables) + { + //beginTable might be the From or the To table in TOM Relationship object; it depends on CrossFilterDirection. + + RelationshipChain referencedTableCollection = new RelationshipChain(); + foreach (Relationship filteringRelationship in beginTable.FindFilteringRelationships()) + { + // EndTable can be either the From or the To table of a Relationship object depending on CrossFilteringBehavior + string endTableName = filteringRelationship.TomRelationship.FromTable.Name == beginTable.Name ? filteringRelationship.TomRelationship.ToTable.Name : filteringRelationship.TomRelationship.FromTable.Name; + + RelationshipLink rootLink = new RelationshipLink(beginTable, _tables.FindByName(endTableName), true, "", filteringRelationship); + ValidateLink(rootLink, referencedTableCollection); + } + } + + //also issue a warning for any table that had it's guid name modified + foreach (Table table in _tables) + { + foreach (Relationship relationship in table.Relationships) + { + if (relationship.ModifiedInternalName) + { + _parentComparison.OnValidationMessage(new ValidationMessageEventArgs( + $"Relationship {relationship.Name.Trim()} has been created/updated, but its Name property was changed from \"{relationship.OldInternalName}\" to \"{relationship.InternalName}\" to avoid conflict with an existing relationship.", + ValidationMessageType.Relationship, + ValidationMessageStatus.Warning)); + } + } + } + } + + private void ValidateLink(RelationshipLink link, RelationshipChain chain) + { + if (chain.ContainsEndTableName(link.EndTable.Name)) + { + // If we are here, we have identified 2 active paths to get to the same table. + // So, the one that was already there in the target should win. + + RelationshipLink otherLink = chain.FindByEndTableName(link.EndTable.Name); + string rootTableName = chain.FindRoot().BeginTable.Name; + + if (link.FilteringRelationship.CopiedFromSource) + { + link.FilteringRelationship.TomRelationship.IsActive = false; + + _parentComparison.OnValidationMessage(new ValidationMessageEventArgs( + $"Relationship {link.FilteringRelationship.Name.Trim()} (which is active in the source) has been created/updated, but is set to inactive in the target because it introduces ambiguous paths between '{rootTableName}' and '{link.EndTable.Name}': '{link.TablePath} and {otherLink.TablePath}'.", + ValidationMessageType.Relationship, + ValidationMessageStatus.Warning)); + } + else + { + // link.FilteringRelationship is the one that was already in the target. So need to remove the other one that leads to the same table (which must have been copied from source) + otherLink.FilteringRelationship.TomRelationship.IsActive = false; + + _parentComparison.OnValidationMessage(new ValidationMessageEventArgs( + $"Relationship {otherLink.FilteringRelationship.Name.Trim()} (which is active in the source) has been created/updated, but is set to inactive in the target because it introduces ambiguous paths between '{rootTableName}' and '{otherLink.EndTable.Name}': '{otherLink.TablePath} and {link.TablePath}'.", + ValidationMessageType.Relationship, + ValidationMessageStatus.Warning)); + + //remove OTHER ONE from collection as no longer in filtering chain + chain.RemoveByEndTableName(otherLink.EndTable.Name); + } + } + + if (link.FilteringRelationship.TomRelationship.IsActive) //If not, we must have just set it to false above + { + //Add the link to the chain and re-iterate ... + chain.Add(link); + + Table beginTable = link.EndTable; //EndTable is now the begin table as iterating next level ... + foreach (Relationship filteringRelationship in beginTable.FindFilteringRelationships()) + { + // EndTable can be either the From or the To table of a Relationship object depending on direction of CrossFilteringBehavior + string endTableName = filteringRelationship.TomRelationship.FromTable.Name == beginTable.Name ? filteringRelationship.TomRelationship.ToTable.Name : filteringRelationship.TomRelationship.FromTable.Name; + + //Need to check if endTableName has already been covered by TablePath to avoid CrossFilteringBehavior leading both ways and never ending loop + if (!link.TablePath.Contains("'" + endTableName + "'")) + { + RelationshipLink newLink = new RelationshipLink(beginTable, _tables.FindByName(endTableName), false, link.TablePath, filteringRelationship); + ValidateLink(newLink, chain); + } + } + } + } + + #endregion + + #region Variation Cleanup + + /// + /// Remove variations referring to objects that don't exist. + /// + public void CleanUpVariations() + { + List targetVariationTablesRemaining = new List(); + + foreach (Table table in _tables) + { + foreach (Column column in table.TomTable.Columns) + { + List variationsToRemove = new List(); + + foreach (Variation variation in column.Variations) + { + if (!_database.Model.Relationships.ContainsName(variation.Relationship.Name)) + { + variationsToRemove.Add(variation.Name); + break; + } + + if (variation.DefaultColumn != null) + { + if (_database.Model.Tables.ContainsName(variation.DefaultColumn.Table?.Name)) + { + //the referenced table is there, how about the referenced column? + if (!_database.Model.Tables.Find(variation.DefaultColumn.Table.Name).Columns.ContainsName(variation.DefaultColumn.Name)) + { + variationsToRemove.Add(variation.Name); + break; + } + } + else + { + variationsToRemove.Add(variation.Name); + break; + } + + //If we get here, the variation is valid + targetVariationTablesRemaining.Add(variation.DefaultColumn.Table?.Name); + break; + } + + if (variation.DefaultHierarchy != null) + { + if (_database.Model.Tables.ContainsName(variation.DefaultHierarchy.Table?.Name)) + { + //the referenced table is there, how about the referenced hierarchy? + if (!_database.Model.Tables.Find(variation.DefaultHierarchy.Table.Name).Hierarchies.ContainsName(variation.DefaultHierarchy.Name)) + { + variationsToRemove.Add(variation.Name); + break; + } + } + else + { + variationsToRemove.Add(variation.Name); + break; + } + + //If we get here, the variation is valid + targetVariationTablesRemaining.Add(variation.DefaultHierarchy.Table?.Name); + break; + } + } + + foreach (string variationToRemove in variationsToRemove) + { + column.Variations.Remove(variationToRemove); + } + } + } + + //Check if any tables that have ShowAsVariationsOnly = true really have variatinos pointing at them + foreach (Table table in _tables) + { + if (table.TomTable.ShowAsVariationsOnly == true && !targetVariationTablesRemaining.Contains(table.Name)) + { + table.TomTable.ShowAsVariationsOnly = false; + } + } + } + + #endregion + + #region Backup / Restore + + /// + /// Perspectives, cultures and roles will be affected by changes to tables, measures, etc. and can end up invalid. To avoid this, take a backup. + /// + public void BackupAffectedObjects() + { + _tomPerspectivesBackup = new List(); + foreach (Tom.Perspective perspective in _database.Model.Perspectives) + { + Tom.Perspective perspectiveToBackup = new Tom.Perspective(); + perspective.CopyTo(perspectiveToBackup); + _tomPerspectivesBackup.Add(perspectiveToBackup); + } + + _tomRolesBackup = new List(); + foreach (ModelRole role in _database.Model.Roles) + { + ModelRole roleToBackup = new ModelRole(); + role.CopyTo(roleToBackup); + _tomRolesBackup.Add(roleToBackup); + } + + _tomCulturesBackup = new List(); + foreach (Tom.Culture culture in _database.Model.Cultures) + { + Tom.Culture cultureToBackup = new Tom.Culture(); + culture.CopyTo(cultureToBackup); + _tomCulturesBackup.Add(cultureToBackup); + } + } + + /// + /// Restore perspectives after changes to tables, measures, etc. + /// + public void RestorePerspectives() + { + if (_tomPerspectivesBackup != null) + { + _database.Model.Perspectives.Clear(); + _perspectives.Clear(); + + foreach (Tom.Perspective tomPerspective in _tomPerspectivesBackup) + { + CreatePerspective(tomPerspective); + } + } + } + + /// + /// Restore roles after changes to tables, measures, etc. + /// + public void RestoreRoles() + { + if (_tomRolesBackup != null) + { + _database.Model.Roles.Clear(); + _roles.Clear(); + + foreach (ModelRole tomRole in _tomRolesBackup) + { + CreateRole(tomRole); + } + } + } + + /// + /// Restore perspectives after changes to tables, measures, etc. Note that perspectives need to be done and dusted before restore cultures or dependency error. + /// + public void RestoreCultues() + { + if (_tomCulturesBackup != null) + { + _database.Model.Cultures.Clear(); + _cultures.Clear(); + + foreach (Tom.Culture tomCulture in _tomCulturesBackup) + { + CreateCulture(tomCulture); + } + } + } + + #endregion + + #region Perspectives + + /// + /// Delete perspective associated with the TabularModel object. + /// + /// Name of the perspective to be deleted. + public void DeletePerspective(string name) + { + if (_database.Model.Perspectives.ContainsName(name)) + { + _database.Model.Perspectives.Remove(name); + } + + // shell model + if (_perspectives.ContainsName(name)) + { + _perspectives.Remove(name); + } + } + + /// + /// Create perspective associated with the TabularModel object. + /// + /// Tabular Object Model Perspective object from the source tabular model to be abstracted in the target. + /// Newly abstracted Tabular Object Model Perspective object. + public Tom.Perspective CreatePerspective(Tom.Perspective tomPerspective) + { + Tom.Perspective tomPerspectiveTarget = new Tom.Perspective(); + tomPerspectiveTarget.Name = tomPerspective.Name; + _database.Model.Perspectives.Add(tomPerspectiveTarget); + _perspectives.Add(new Perspective(this, tomPerspectiveTarget)); + + SyncPerspectives(tomPerspective, tomPerspectiveTarget); + return tomPerspectiveTarget; + } + + /// + /// Update perspective associated with the TabularModel object. + /// + /// Tabular Object Model Perspective object from the source tabular model to be abstracted in the target. + /// Tabular Object Model Perspective object in the target tabular model to be abstracted. + public void UpdatePerspective(Tom.Perspective tomPerspectiveSource, Tom.Perspective tomPerspectiveTarget) + { + if (_comparisonInfo.OptionsInfo.OptionMergePerspectives) + { + Tom.Perspective perspectiveBackup = null; + foreach (Tom.Perspective perspective in _tomPerspectivesBackup) + { + if (perspective.Name == tomPerspectiveTarget.Name) + { + perspectiveBackup = perspective; + break; + } + } + + if (perspectiveBackup != null) + { + DeletePerspective(tomPerspectiveTarget.Name); + Tom.Perspective perspectiveTargetNew = CreatePerspective(perspectiveBackup); + + SyncPerspectives(tomPerspectiveSource, perspectiveTargetNew); + } + } + else + { + DeletePerspective(tomPerspectiveTarget.Name); + CreatePerspective(tomPerspectiveSource); + } + } + + private void SyncPerspectives(Tom.Perspective perspectiveSource, Tom.Perspective perspectiveTarget) + { + //Tables + foreach (PerspectiveTable perspectiveTableSource in perspectiveSource.PerspectiveTables) + { + Table tableTarget = _tables.FindByName(perspectiveTableSource.Name); + if (tableTarget != null) + { + //Following line is returning null in CTP3.3, when it shouldn't, so having to iterate to find + //PerspectiveTable perspectiveTableTarget = perspectiveTarget.PerspectiveTables.Find(perspectiveTableSource.Name); //When merging perspectives, might already be there + PerspectiveTable perspectiveTableTarget = null; + foreach (PerspectiveTable table in perspectiveTarget.PerspectiveTables) + { + if (table.Name == perspectiveTableSource.Name) + { + perspectiveTableTarget = table; + break; + } + } + + if (perspectiveTableTarget == null) + { + perspectiveTableTarget = new PerspectiveTable(); + perspectiveTarget.PerspectiveTables.Add(perspectiveTableTarget); + perspectiveTableTarget.Name = perspectiveTableSource.Name; + perspectiveTableTarget.Table = tableTarget.TomTable; + } + + //Columns + foreach (PerspectiveColumn perspectiveColumnSource in perspectiveTableSource.PerspectiveColumns) + { + Column column = tableTarget.TomTable.Columns.Find(perspectiveColumnSource.Name); + if (column != null) + { + //Following line is returning null in CTP3.3, when it shouldn't, so having to iterate to find + //PerspectiveColumn perspectiveColumnTarget = perspectiveTarget.PerspectiveColumns.Find(perspectiveColumnSource.Name); //When merging perspectives, might already be there + PerspectiveColumn perspectiveColumnTarget = null; + foreach (PerspectiveColumn perspectiveColumn in perspectiveTableTarget.PerspectiveColumns) + { + if (perspectiveColumn.Name == perspectiveColumnSource.Name) + { + perspectiveColumnTarget = perspectiveColumn; + break; + } + } + + if (perspectiveColumnTarget == null) + { + perspectiveColumnTarget = new PerspectiveColumn(); + perspectiveTableTarget.PerspectiveColumns.Add(perspectiveColumnTarget); + perspectiveColumnTarget.Name = perspectiveColumnSource.Name; + perspectiveColumnTarget.Column = column; + } + } + } + + //Hierarchies + foreach (PerspectiveHierarchy perspectiveHierarchySource in perspectiveTableSource.PerspectiveHierarchies) + { + Hierarchy hierarchy = tableTarget.TomTable.Hierarchies.Find(perspectiveHierarchySource.Name); + if (hierarchy != null) + { + //Following line is returning null in CTP3.3, when it shouldn't, so having to iterate to find + //PerspectiveHierarchy perspectiveHierarchyTarget = perspectiveTarget.PerspectiveHierarchies.Find(perspectiveHierarchySource.Name); //When merging perspectives, might already be there + PerspectiveHierarchy perspectiveHierarchyTarget = null; + foreach (PerspectiveHierarchy perspectiveHierarchy in perspectiveTableTarget.PerspectiveHierarchies) + { + if (perspectiveHierarchy.Name == perspectiveHierarchySource.Name) + { + perspectiveHierarchyTarget = perspectiveHierarchy; + break; + } + } + + if (perspectiveHierarchyTarget == null) + { + perspectiveHierarchyTarget = new PerspectiveHierarchy(); + perspectiveTableTarget.PerspectiveHierarchies.Add(perspectiveHierarchyTarget); + perspectiveHierarchyTarget.Name = perspectiveHierarchySource.Name; + perspectiveHierarchyTarget.Hierarchy = hierarchy; + } + } + } + + //Measures + foreach (PerspectiveMeasure perspectiveMeasureSource in perspectiveTableSource.PerspectiveMeasures) + { + Tom.Measure measure = tableTarget.TomTable.Measures.Find(perspectiveMeasureSource.Name); + if (measure != null) + { + //Following line is returning null in CTP3.3, when it shouldn't, so having to iterate to find + //PerspectiveMeasure perspectiveMeasureTarget = perspectiveTarget.PerspectiveMeasures.Find(perspectiveMeasureSource.Name); //When merging perspectives, might already be there + PerspectiveMeasure perspectiveMeasureTarget = null; + foreach (PerspectiveMeasure perspectiveMeasure in perspectiveTableTarget.PerspectiveMeasures) + { + if (perspectiveMeasure.Name == perspectiveMeasureSource.Name) + { + perspectiveMeasureTarget = perspectiveMeasure; + break; + } + } + + if (perspectiveMeasureTarget == null) + { + perspectiveMeasureTarget = new PerspectiveMeasure(); + perspectiveTableTarget.PerspectiveMeasures.Add(perspectiveMeasureTarget); + perspectiveMeasureTarget.Name = perspectiveMeasureSource.Name; + perspectiveMeasureTarget.Measure = measure; + } + } + } + } + } + } + + #endregion + + #region Cultures + + /// + /// Delete culture associated with the TabularModel object. + /// + /// Name of the culture to be deleted. + public void DeleteCulture(string name) + { + if (_database.Model.Cultures.ContainsName(name)) + { + _database.Model.Cultures.Remove(name); + } + + // shell model + if (_cultures.ContainsName(name)) + { + _cultures.Remove(name); + } + } + + /// + /// Create culture associated with the TabularModel object. + /// + /// Tabular Object Model Culture object from the source tabular model to be abstracted in the target. + /// Newly abstracted Tabular Object Model Culture object. + public Tom.Culture CreateCulture(Tom.Culture tomCulture) + { + Tom.Culture tomCultureTarget = new Tom.Culture(); + tomCultureTarget.Name = tomCulture.Name; + _database.Model.Cultures.Add(tomCultureTarget); + _cultures.Add(new Culture(this, tomCultureTarget)); + + SyncCultures(tomCulture, tomCultureTarget); + return tomCultureTarget; + } + + /// + /// Update culture associated with the TabularModel object. + /// + /// Tabular Object Model Culture object from the source tabular model to be abstracted in the target. + /// Tabular Object Model Culture object in the target tabular model to be abstracted. + public void UpdateCulture(Tom.Culture tomCultureSource, Tom.Culture tomCultureTarget) + { + if (_comparisonInfo.OptionsInfo.OptionMergeCultures) + { + Tom.Culture tomCultureBackup = null; + foreach (Tom.Culture tomCulture in _tomCulturesBackup) + { + if (tomCulture.Name == tomCultureTarget.Name) + { + tomCultureBackup = tomCulture; + break; + } + } + + if (tomCultureBackup != null) + { + DeleteCulture(tomCultureTarget.Name); + Tom.Culture tomCultureTargetNew = CreateCulture(tomCultureBackup); + + SyncCultures(tomCultureSource, tomCultureTargetNew); + } + } + else + { + DeleteCulture(tomCultureTarget.Name); + CreateCulture(tomCultureSource); + } + } + + private void SyncCultures(Tom.Culture tomCultureSource, Tom.Culture tomCultureTarget) + { + foreach (ObjectTranslation translationSource in tomCultureSource.ObjectTranslations) + { + if (translationSource.Object is NamedMetadataObject) + { + NamedMetadataObject namedObjectSource = (NamedMetadataObject)translationSource.Object; + NamedMetadataObject namedObjectTarget = null; + + //Find the object in the target model that this translation applies to + switch (namedObjectSource.ObjectType) + { + case ObjectType.Model: + //if (namedObjectSource.Name == tomCultureTarget.Model.Name) + //{ + //Model name can legitimately have different names - and there can only be 1 model, so we are OK not doing check in if clause above. + namedObjectTarget = tomCultureTarget.Model; + //} + break; + case ObjectType.Table: + foreach (Tom.Table tomTableTarget in tomCultureTarget.Model.Tables) + { + if (namedObjectSource.Name == tomTableTarget.Name) + { + namedObjectTarget = tomTableTarget; + break; + } + } + break; + case ObjectType.Column: + Column columnSource = (Column)namedObjectSource; + foreach (Tom.Table tableTarget in tomCultureTarget.Model.Tables) + { + bool foundColumn = false; + if (columnSource.Table?.Name == tableTarget.Name) + { + foreach (Column columnTarget in tableTarget.Columns) + { + if (columnSource.Name == columnTarget.Name) + { + namedObjectTarget = columnTarget; + foundColumn = true; + break; + } + } + } + if (foundColumn) break; + } + break; + case ObjectType.Measure: + Tom.Measure tomMeasureSource = (Tom.Measure)namedObjectSource; + foreach (Tom.Table tomTableTarget in tomCultureTarget.Model.Tables) + { + bool foundMeasure = false; + if (tomMeasureSource.Table?.Name == tomTableTarget.Name) + { + foreach (Tom.Measure measureTarget in tomTableTarget.Measures) + { + if (tomMeasureSource.Name == measureTarget.Name) + { + namedObjectTarget = measureTarget; + foundMeasure = true; + break; + } + } + } + if (foundMeasure) break; + } + break; + case ObjectType.Hierarchy: + Hierarchy hierarchySource = (Hierarchy)namedObjectSource; + foreach (Tom.Table tomTableTarget in tomCultureTarget.Model.Tables) + { + bool foundHierarchy = false; + if (hierarchySource.Table?.Name == tomTableTarget.Name) + { + foreach (Hierarchy hierarchyTarget in tomTableTarget.Hierarchies) + { + if (hierarchySource.Name == hierarchyTarget.Name) + { + namedObjectTarget = hierarchyTarget; + foundHierarchy = true; + break; + } + } + } + if (foundHierarchy) break; + } + break; + case ObjectType.Level: + Level levelSource = (Level)namedObjectSource; + foreach (Tom.Table tomTableTarget in tomCultureTarget.Model.Tables) + { + bool foundLevel = false; + if (levelSource.Hierarchy?.Table?.Name == tomTableTarget.Name) + { + foreach (Hierarchy hierarchyTarget in tomTableTarget.Hierarchies) + { + if (levelSource.Hierarchy?.Name == hierarchyTarget.Name) + { + foreach (Level levelTarget in hierarchyTarget.Levels) + { + if (levelSource.Name == levelTarget.Name) + { + namedObjectTarget = levelTarget; + foundLevel = true; + break; + } + } + } + if (foundLevel) break; + } + if (foundLevel) break; + } + } + break; + case ObjectType.Perspective: + foreach (Tom.Perspective tomPerspectiveTarget in tomCultureTarget.Model.Perspectives) + { + if (namedObjectSource.Name == tomPerspectiveTarget.Name) + { + namedObjectTarget = tomPerspectiveTarget; + break; + } + } + break; + case ObjectType.Role: + foreach (ModelRole tomRoleTarget in tomCultureTarget.Model.Roles) + { + if (namedObjectSource.Name == tomRoleTarget.Name) + { + namedObjectTarget = tomRoleTarget; + break; + } + } + break; + case ObjectType.Expression: + foreach (NamedExpression tomExpressionTarget in tomCultureTarget.Model.Expressions) + { + if (namedObjectSource.Name == tomExpressionTarget.Name) + { + namedObjectTarget = tomExpressionTarget; + break; + } + } + break; + //case ObjectType.KPI: //KPIs dealt with by measures above + // break; + default: + break; + } + + //todo delete + ////Not sure why switch statement not stopping on "case ObjectType.Perspective:" above. Fudge below. Todo2: test with later build. + //if (namedObjectSource.ObjectType.ToString() == "Perspective") + //{ + // foreach (Tom.Perspective tomPerspectiveTarget in tomCultureTarget.Model.Perspectives) + // { + // if (namedObjectSource.Name == tomPerspectiveTarget.Name) + // { + // namedObjectTarget = tomPerspectiveTarget; + // break; + // } + // } + //} + + //If namedObjectTarget is null, the model object doesn't exist in target, so can ignore + if (namedObjectTarget != null) + { + //Does the translation already exist in cultureTarget? + ObjectTranslation translationTarget = null; + foreach (ObjectTranslation translation in tomCultureTarget.ObjectTranslations) + { + if (translation.Object is NamedMetadataObject && + ((NamedMetadataObject)translation.Object).Name == namedObjectSource.Name && + translation.Object.ObjectType == namedObjectSource.ObjectType && + translation.Property == translationSource.Property + ) + { + translationTarget = translation; + break; + } + } + + if (translationTarget != null) + { //Translation already exists in cultureTarget for this object, so just ensure values match + //Also decouple from object in model and reset coupling + translationTarget.Object = namedObjectTarget; + translationTarget.Value = translationSource.Value; + } + else + { //Translation does not exist in cultureTarget, so create it and add it to culture + translationTarget = new ObjectTranslation(); + translationTarget.Object = namedObjectTarget; + translationTarget.Property = translationSource.Property; + translationTarget.Value = translationSource.Value; + tomCultureTarget.ObjectTranslations.Add(translationTarget); + } + } + } + } + } + + #endregion + + #region Roles + + /// + /// Delete role associated with the TabularModel object. + /// + /// Name of the role to be deleted. + public void DeleteRole(string name) + { + if (_database.Model.Roles.Contains(name)) + { + _database.Model.Roles.Remove(name); + } + + // shell model + if (_roles.ContainsName(name)) + { + _roles.Remove(name); + } + } + + /// + /// Create role associated with the TabularModel object. + /// + /// Tabular Object Model ModelRole object from the source tabular model to be abstracted in the target. + public void CreateRole(ModelRole tomRoleSource) + { + ModelRole tomRoleTarget = new ModelRole(); + tomRoleSource.CopyTo(tomRoleTarget); + + List permissionNamesToDelete = new List(); + foreach (TablePermission permission in tomRoleTarget.TablePermissions) + { + if (_tables.ContainsName(permission.Table.Name)) + { + //decouple table permissions from from original table to the one in target model (if exists) + permission.Table = _tables.FindByName(permission.Table.Name).TomTable; + } + else + { + permissionNamesToDelete.Add(permission.Name); + } + } + foreach (string name in permissionNamesToDelete) + { + tomRoleTarget.TablePermissions.Remove(name); + } + + _database.Model.Roles.Add(tomRoleTarget); + + // shell model + _roles.Add(new Role(this, tomRoleTarget)); + } + + /// + /// Update role associated with the TabularModel object. + /// + /// Role object from the source tabular model to be updated in the target. + /// Role object in the target tabular model to be updated. + public void UpdateRole(Role roleSource, Role roleTarget) + { + DeleteRole(roleTarget.Name); + CreateRole(roleSource.TomRole); + } + + /// + /// Remove any role references to non-existing tables. + /// + public void RolesCleanup() + { + //Check for roles' table permissions referring to non-existing tables + foreach (ModelRole tomRole in _database.Model.Roles) + { + List permissionNamesToDelete = new List(); + + foreach (TablePermission permission in tomRole.TablePermissions) + { + if (permission.Table == null || !_tables.ContainsName(permission.Table.Name)) + { + permissionNamesToDelete.Add(permission.Name); + } + } + + foreach (string name in permissionNamesToDelete) + { + tomRole.TablePermissions.Remove(name); + } + } + } + + #endregion + + /// + /// Update target tabular model with changes resulting from the comparison. For database deployment, this will fire DeployDatabaseCallBack. + /// + /// Boolean indicating whether update was successful. + public bool Update() + { + FinalValidation(); + + if (_connectionInfo.UseProject) + { + UpdateProject(); + } + else + { //Database deployement + + if (_comparisonInfo.PromptForDatabaseProcessing) + { + //Call back to show deployment form + DatabaseDeploymentEventArgs args = new DatabaseDeploymentEventArgs(); + _parentComparison.OnDatabaseDeployment(args); + return args.DeploymentSuccessful; + } + else + { + //Simple update target without setting passwords or processing (mainly for command-line execution) + UpdateWithScript(); + } + } + + return true; + } + + private void UpdateProject() + { + UpdateWithScript(); + + if (_connectionInfo.Project != null) + { + //Running in VS + EnvDTE._DTE dte = _connectionInfo.Project.DTE; + + //check out bim file if necessary + if (dte.SourceControl.IsItemUnderSCC(_connectionInfo.BimFileFullName) && !dte.SourceControl.IsItemCheckedOut(_connectionInfo.BimFileFullName)) + { + dte.SourceControl.CheckOutItem(_connectionInfo.BimFileFullName); + } + } + + //Script out db and write to project file + + //serialize db to json + SerializeOptions options = new SerializeOptions(); + options.IgnoreInferredProperties = true; + options.IgnoreInferredObjects = true; + options.IgnoreTimestamps = true; + options.SplitMultilineStrings = true; + string json = JsonSerializer.SerializeDatabase(_database, options); + + //replace db name with "SemanticModel" + JObject jDb = JObject.Parse(json); + jDb["name"] = "SemanticModel"; + jDb["id"] = "SemanticModel"; + json = jDb.ToString(); + + File.WriteAllText(_connectionInfo.BimFileFullName, json); + } + + #region Database deployment and processing methods + + private const string _deployRowWorkItem = "Deploy metadata"; + private ProcessingTableCollection _tablesToProcess; + + /// + /// Perform database deployment and processing of required tables. + /// + /// Tables to process. + public void DatabaseDeployAndProcess(ProcessingTableCollection tablesToProcess) + { + try + { + _tablesToProcess = tablesToProcess; + + //For each impersonated account ... + // 1. Prompt for password and validate. If invalid back out of deployment. If valid, apply passwords. + // 2. Deploy with script (using UpdateWithScript), which will lose the passwords. + // 3. Re-apply the passwords. + //Above steps allow backing out of deployment. + + //Set passwords + foreach (Tom.DataSource dataSource in _database.Model.DataSources) + { + PasswordPromptEventArgs args = new PasswordPromptEventArgs(); + switch (dataSource.Type) + { + case DataSourceType.Structured: + + StructuredDataSource structuredDataSource = (StructuredDataSource)dataSource; + args.AuthenticationKind = structuredDataSource.Credential.AuthenticationKind; + + switch (structuredDataSource.Credential.AuthenticationKind) + { + case "Windows": + //Same as impersonate account + args.DataSourceName = dataSource.Name; + args.Username = structuredDataSource.Credential.Username; + _parentComparison.OnPasswordPrompt(args); + if (args.UserCancelled) + { + // Show cancelled for all rows + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Deployment has been cancelled.", DeploymentStatus.Cancel)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Cancelled", DeploymentStatus.Cancel)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Cancel, null)); + return; + } + structuredDataSource.Credential.Username = args.Username; + structuredDataSource.Credential.Password = args.Password; + structuredDataSource.Credential.PrivacySetting = args.PrivacyLevel; + break; + + case "UsernamePassword": + //Same as impersonate account + args.DataSourceName = dataSource.Name; + args.Username = structuredDataSource.Credential.Username; + _parentComparison.OnPasswordPrompt(args); + if (args.UserCancelled) + { + // Show cancelled for all rows + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Deployment has been cancelled.", DeploymentStatus.Cancel)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Cancelled", DeploymentStatus.Cancel)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Cancel, null)); + return; + } + structuredDataSource.Credential.Username = args.Username; + structuredDataSource.Credential.Password = args.Password; + structuredDataSource.Credential.PrivacySetting = args.PrivacyLevel; + break; + + case "Key": + BlobKeyEventArgs keyArgs = new BlobKeyEventArgs(); + + //Same as impersonate account + keyArgs.DataSourceName = dataSource.Name; + _parentComparison.OnBlobKeyPrompt(keyArgs); + if (keyArgs.UserCancelled) + { + // Show cancelled for all rows + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Deployment has been cancelled.", DeploymentStatus.Cancel)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Cancelled", DeploymentStatus.Cancel)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Cancel, null)); + return; + } + structuredDataSource.Credential[CredentialProperty.Key] = keyArgs.AccountKey; + structuredDataSource.Credential.PrivacySetting = keyArgs.PrivacyLevel; + break; + + default: + break; + } + break; + + case DataSourceType.Provider: + ProviderDataSource providerDataSource = (ProviderDataSource)dataSource; + + if (providerDataSource.ImpersonationMode == ImpersonationMode.ImpersonateAccount) + { + args.AuthenticationKind = "Windows"; + args.DataSourceName = dataSource.Name; + args.Username = providerDataSource.Account; + args.PrivacyLevel = "NA"; + _parentComparison.OnPasswordPrompt(args); + if (args.UserCancelled) + { + // Show cancelled for all rows + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Deployment has been cancelled.", DeploymentStatus.Cancel)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Cancelled", DeploymentStatus.Cancel)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Cancel, null)); + return; + } + providerDataSource.Account = args.Username; + providerDataSource.Password = args.Password; + } + break; + default: + break; + } + } + + UpdateWithScript(); + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Success. Metadata deployed.", DeploymentStatus.Success)); + + //Kick off processing + ProcessAsyncDelegate processAsyncCaller = new ProcessAsyncDelegate(Process); + processAsyncCaller.BeginInvoke(null, null); + + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Success, null)); + } + catch (Exception exc) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Error deploying metadata.", DeploymentStatus.Error)); + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Error", DeploymentStatus.Error)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + } + + private void UpdateWithScript() + { + //_database.Update(Amo.UpdateOptions.ExpandFull); //If make minor changes to table (e.g. display folder) without changes to the partition or column structure, this command will still lose the data due to TOM applying a full log of operations. So instead reconnect and run TMSL script. + + //includeRestrictedInformation only includes passwords in connections if they were added during this session (does not allow derivation of passwords from the server) + string tmslCommand = JsonScripter.ScriptCreateOrReplace(_database, includeRestrictedInformation: true); + + _server.Disconnect(); + _server = new Server(); + _server.Connect("Provider=MSOLAP;Data Source=" + _connectionInfo.ServerName); + Amo.XmlaResultCollection results = _server.Execute(tmslCommand); + if (results.ContainsErrors) + throw new Amo.OperationException(results); + + _database = _server.Databases.FindByName(_connectionInfo.DatabaseName); + + //FROM THIS POINT ONWARDS use only TOM as have not bothered re-hydrating the BismNorm object model + } + + private bool _stopProcessing; + string _sessionId; + internal delegate void ProcessAsyncDelegate(); + private void Process() + { + //string x13 = TraceColumn.ObjectName.ToString(); + //string x15 = TraceColumn.ObjectReference.ToString(); + //string x10 = TraceColumn.IntegerData.ToString(); + + Trace trace = null; + try + { + _stopProcessing = false; + RefreshType refreshType = _comparisonInfo.OptionsInfo.OptionProcessingOption == ProcessingOption.Default ? RefreshType.Automatic : RefreshType.Full; + + //Set up server trace to capture how many rows processed + _sessionId = _server.SessionID; + trace = _server.Traces.Add(); + TraceEvent traceEvent = trace.Events.Add(Amo.TraceEventClass.ProgressReportCurrent); + traceEvent.Columns.Add(Amo.TraceColumn.ObjectID); + traceEvent.Columns.Add(Amo.TraceColumn.ObjectName); + traceEvent.Columns.Add(Amo.TraceColumn.ObjectReference); + traceEvent.Columns.Add(Amo.TraceColumn.IntegerData); + traceEvent.Columns.Add(Amo.TraceColumn.SessionID); + traceEvent.Columns.Add(Amo.TraceColumn.Spid); + trace.Update(); + trace.OnEvent += new TraceEventHandler(Trace_OnEvent); + trace.Start(); + + if (_tablesToProcess.Count > 0) + { + foreach (ProcessingTable tableToProcess in _tablesToProcess) + { + Tom.Table table = _database.Model.Tables.Find(tableToProcess.Name); + if (table != null) + { + table.RequestRefresh(refreshType); + } + } + } + + //Need recalc even if created no tables in case of new relationships without tables + _database.Model.RequestRefresh(RefreshType.Calculate); + + _database.Model.SaveChanges(); + + // Show row count for each table + foreach (ProcessingTable table in _tablesToProcess) + { + int rowCount = _connectionInfo.DirectQuery ? 0 : Core.Comparison.FindRowCount(_server, table.Name, _database.Name); + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, $"Success. {String.Format("{0:#,###0}", rowCount)} rows transferred.", DeploymentStatus.Success)); + } + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Success, null)); + } + //catch (InvalidOperationException exc) when (exc.Message == "Operation is not valid due to the current state of the object.") + //{ //Azure AS sometimes loses connection - need extra filter for after save changes. + //} + catch (Exception exc) + { + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + finally + { + try + { + trace.Stop(); + trace.Drop(); + } + catch { } + } + } + + /// + /// Stop processing if possible. + /// + public void StopProcessing() + { + _stopProcessing = true; + + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + try + { + _server.RollbackTransaction(); + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, "Rolled back transaction.")); + } + catch (Exception exc) + { + if (exc is NullReferenceException || exc is InvalidOperationException) + { + return; + } + else + { + ShowErrorsForAllRows(); + _parentComparison.OnDeploymentComplete(new DeploymentCompleteEventArgs(DeploymentStatus.Error, exc.Message)); + } + } + } + } + + private void Trace_OnEvent(object sender, TraceEventArgs e) + { + if (e.ObjectName != null && e.ObjectReference != null && e.SessionID == _sessionId) + { + XmlDocument document = new XmlDocument(); + document.LoadXml(e.ObjectReference); + + XmlNodeList partitionNodeList = document.GetElementsByTagName("Partition"); + XmlNodeList tableNodeList = document.GetElementsByTagName("Table"); + + if (partitionNodeList != null && tableNodeList != null) + { + if (_tablesToProcess.ContainsId(tableNodeList[0].InnerText)) + { + ProcessingTable processingTable = _tablesToProcess.FindById(tableNodeList[0].InnerText); + + if (!processingTable.ContainsPartition(partitionNodeList[0].InnerText)) + { + processingTable.Partitions.Add(new PartitionRowCounter(partitionNodeList[0].InnerText)); + } + + PartitionRowCounter partition = processingTable.FindPartition(partitionNodeList[0].InnerText); + partition.RowCount = e.IntegerData; + + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(processingTable.Name, $"Retreived {String.Format("{0:#,###0}", processingTable.GetRowCount())} rows ...", DeploymentStatus.Deploying)); + } + } + + if (_stopProcessing && !_comparisonInfo.OptionsInfo.OptionTransaction) //transactions get cancelled in StopProcessing, not here + { + try + { + _server.CancelCommand(_sessionId); + } + catch { } + } + + ////Doesn't work with Spid, so doing sessionid above + //int spid; + //if (_stopProcessing && int.TryParse(e.Spid, out spid)) + //{ + // try + // { + // //_amoServer.CancelCommand(e.Spid); + // string commandStatement = String.Format("{0}1", e.Spid); + // System.Diagnostics.Debug.WriteLine(commandStatement); + // _amoServer.Execute(commandStatement); + // //_connectionInfo.ExecuteXmlaCommand(_amoServer, commandStatement); + // } + // catch { } + //} + } + } + + private void ShowErrorsForAllRows() + { + // Show error for each item + if (_comparisonInfo.OptionsInfo.OptionTransaction) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(_deployRowWorkItem, "Error", DeploymentStatus.Error)); + } + foreach (ProcessingTable table in _tablesToProcess) + { + _parentComparison.OnDeploymentMessage(new DeploymentMessageEventArgs(table.Name, "Error", DeploymentStatus.Error)); + } + } + + #endregion + + /// + /// Generate script containing full tabular model definition. + /// + /// JSON script of tabular model defintion. + public string ScriptDatabase() + { + FinalValidation(); + + //script db to json + string json = JsonScripter.ScriptCreateOrReplace(_database); + + if (_connectionInfo.UseProject) + { + //replace db/cube name/id with name of deploymnet db from the project file + JObject jScript = JObject.Parse(json); + JObject createOrReplace = (JObject)jScript["createOrReplace"]; + if (!String.IsNullOrEmpty(_connectionInfo.DeploymentServerDatabase)) + { + ((JObject)createOrReplace["object"])["database"] = _connectionInfo.DeploymentServerDatabase; + ((JObject)createOrReplace["database"])["name"] = _connectionInfo.DeploymentServerDatabase; + ((JObject)createOrReplace["database"])["id"] = _connectionInfo.DeploymentServerDatabase; + } + json = jScript.ToString(); + } + + return json; + } + + private void FinalValidation() + { + if (_connectionInfo.DirectQuery && _dataSources.Count > 1) + { + throw new InvalidOperationException("Target model contains multiple data sources, which are not allowed for Direct Query models. Re-run comparison and (considering changes) ensure there is a single connection in the target model."); + } + } + + public override string ToString() => this.GetType().FullName; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_server != null) + { + _server.Dispose(); + } + } + + _disposed = true; + } + } + + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularObject.cs b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularObject.cs new file mode 100644 index 0000000..59430a1 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/TabularMetadata/TabularObject.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AnalysisServices.Tabular; +using Tom=Microsoft.AnalysisServices.Tabular; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace BismNormalizer.TabularCompare.TabularMetadata +{ + /// + /// Represents a tabular object for comparison. This class handles JSON serialization. + /// + public class TabularObject + { + private string _objectDefinition; + private string _name; + + /// + /// Initializes a new instance of the TabularObject class. + /// + /// The Tabular Object Model supertype of the class being abstracted. + public TabularObject(NamedMetadataObject namedMetaDataObject) + { + _name = namedMetaDataObject.Name; + + //Serialize json + SerializeOptions options = new SerializeOptions(); + options.IgnoreInferredProperties = true; + options.IgnoreInferredObjects = true; + options.IgnoreTimestamps = true; + options.SplitMultilineStrings = true; + _objectDefinition = Tom.JsonSerializer.SerializeObject(namedMetaDataObject, options); + + //Remove annotations + { + JToken token = JToken.Parse(_objectDefinition); + RemovePropertyFromObjectDefinition(token, "annotations"); + _objectDefinition = token.ToString(Formatting.Indented); + } + + //Order table columns + if (namedMetaDataObject is Tom.Table) + { + _objectDefinition = SortArray(_objectDefinition, "columns"); + _objectDefinition = SortArray(_objectDefinition, "partitions"); + } + + //Hide privacy setting on structured data sources + if (namedMetaDataObject is Tom.StructuredDataSource) + { + JToken token = JToken.Parse(_objectDefinition); + RemovePropertyFromObjectDefinition(token, "PrivacySetting"); + _objectDefinition = token.ToString(Formatting.Indented); + } + } + + private string SortArray(string json, string arrayName) + { + JObject jObj = (JObject)JsonConvert.DeserializeObject(json); + + foreach (var prop in jObj.Properties()) + { + if (prop.Value.Type == JTokenType.Array && prop.Name == arrayName) + { + var vals = prop.Values() + .OfType() + .OrderBy(x => x.Property("name").Value.ToString()) + .ToList(); + prop.Value = JContainer.FromObject(vals); + } + } + + return jObj.ToString(Formatting.Indented); + } + + private void RemovePropertyFromObjectDefinition(JToken token, string propertyName) + { + //child object annotations + List removeList = new List(); + foreach (JToken childToken in token.Children()) + { + JProperty property = childToken as JProperty; + if (property != null && property.Name == propertyName) + { + removeList.Add(childToken); + } + RemovePropertyFromObjectDefinition(childToken, propertyName); + } + foreach (JToken tokenToRemove in removeList) + { + tokenToRemove.Remove(); + } + } + + /// + /// Explicitly remove a JSON property from definition. An example of this is removing parititions from table definitions. + /// + /// The property to remove + public void RemovePropertyFromObjectDefinition(string propertyName) + { + JObject jObj = JObject.Parse(_objectDefinition); + jObj.Remove(propertyName); + _objectDefinition = jObj.ToString(Formatting.Indented); + } + + /// + /// Retrieve a JSON property definition from the full object definition. An example is partitions. + /// + /// + /// Property definition retrieved. + public string RetrievePropertyFromObjectDefinition(string propertyToRetrieve) + { + JObject jObj = JObject.Parse(_objectDefinition); + JProperty property = jObj.Property(propertyToRetrieve); + return property.ToString(); + } + + /// + /// The serialized JSON definition of the tabular object. + /// + public string ObjectDefinition => _objectDefinition; + + /// + /// The name of the tabular object. Gets overriden by Relationship to show friendly name. + /// + public virtual string Name => _name; + + /// + /// The internal name of the tabular object. Gets overriden by Relationship to store the true name from TOM (GUID form). + /// + public virtual string InternalName => _name; + + } +} + diff --git a/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.Designer.cs b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.Designer.cs new file mode 100644 index 0000000..0c8e7f7 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.Designer.cs @@ -0,0 +1,183 @@ +namespace BismNormalizer.TabularCompare.UI +{ + partial class BlobCredentials + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.btnCancel = new System.Windows.Forms.Button(); + this.btnOK = new System.Windows.Forms.Button(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.cboPrivacyLevel = new System.Windows.Forms.ComboBox(); + this.lblPrivacyLevel = new System.Windows.Forms.Label(); + this.txtAccountKey = new System.Windows.Forms.TextBox(); + this.txtConnectionName = new System.Windows.Forms.TextBox(); + this.label3 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.groupBox1.SuspendLayout(); + this.SuspendLayout(); + // + // btnCancel + // + this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.btnCancel.Location = new System.Drawing.Point(346, 142); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(75, 23); + this.btnCancel.TabIndex = 4; + this.btnCancel.Text = "Cancel"; + this.btnCancel.UseVisualStyleBackColor = true; + // + // btnOK + // + this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK; + this.btnOK.Location = new System.Drawing.Point(265, 142); + this.btnOK.Name = "btnOK"; + this.btnOK.Size = new System.Drawing.Size(75, 23); + this.btnOK.TabIndex = 3; + this.btnOK.Text = "OK"; + this.btnOK.UseVisualStyleBackColor = true; + // + // groupBox1 + // + this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox1.Controls.Add(this.cboPrivacyLevel); + this.groupBox1.Controls.Add(this.lblPrivacyLevel); + this.groupBox1.Controls.Add(this.txtAccountKey); + this.groupBox1.Controls.Add(this.txtConnectionName); + this.groupBox1.Controls.Add(this.label3); + this.groupBox1.Controls.Add(this.label1); + this.groupBox1.Location = new System.Drawing.Point(12, 13); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(409, 123); + this.groupBox1.TabIndex = 24; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Credentials"; + // + // cboPrivacyLevel + // + this.cboPrivacyLevel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.cboPrivacyLevel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cboPrivacyLevel.Items.AddRange(new object[] { + "None", + "Public", + "Organizational", + "Private"}); + this.cboPrivacyLevel.Location = new System.Drawing.Point(118, 76); + this.cboPrivacyLevel.Name = "cboPrivacyLevel"; + this.cboPrivacyLevel.Size = new System.Drawing.Size(285, 21); + this.cboPrivacyLevel.TabIndex = 6; + // + // lblPrivacyLevel + // + this.lblPrivacyLevel.AutoSize = true; + this.lblPrivacyLevel.Location = new System.Drawing.Point(7, 79); + this.lblPrivacyLevel.Name = "lblPrivacyLevel"; + this.lblPrivacyLevel.Size = new System.Drawing.Size(74, 13); + this.lblPrivacyLevel.TabIndex = 7; + this.lblPrivacyLevel.Text = "Privacy Level:"; + // + // txtAccountKey + // + this.txtAccountKey.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtAccountKey.Location = new System.Drawing.Point(118, 50); + this.txtAccountKey.Name = "txtAccountKey"; + this.txtAccountKey.Size = new System.Drawing.Size(285, 20); + this.txtAccountKey.TabIndex = 2; + this.txtAccountKey.UseSystemPasswordChar = true; + // + // txtConnectionName + // + this.txtConnectionName.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtConnectionName.Location = new System.Drawing.Point(118, 24); + this.txtConnectionName.Name = "txtConnectionName"; + this.txtConnectionName.ReadOnly = true; + this.txtConnectionName.Size = new System.Drawing.Size(285, 20); + this.txtConnectionName.TabIndex = 3; + this.txtConnectionName.TabStop = false; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(7, 53); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(71, 13); + this.label3.TabIndex = 2; + this.label3.Text = "Account Key:"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(7, 27); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(101, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Data Source Name:"; + // + // BlobCredentials + // + this.AcceptButton = this.btnOK; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.btnCancel; + this.ClientSize = new System.Drawing.Size(433, 177); + this.Controls.Add(this.groupBox1); + this.Controls.Add(this.btnCancel); + this.Controls.Add(this.btnOK); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "BlobCredentials"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "Azure Blob Storage"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ImpersonationCredentials_FormClosing); + this.Load += new System.EventHandler(this.BlobCredentials_Load); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button btnCancel; + private System.Windows.Forms.Button btnOK; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox txtAccountKey; + private System.Windows.Forms.TextBox txtConnectionName; + private System.Windows.Forms.ComboBox cboPrivacyLevel; + private System.Windows.Forms.Label lblPrivacyLevel; + } +} \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.cs b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.cs new file mode 100644 index 0000000..6cee6ad --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.cs @@ -0,0 +1,84 @@ +using System; +using System.DirectoryServices.AccountManagement; +using System.Drawing; +using System.Windows.Forms; + +namespace BismNormalizer.TabularCompare.UI +{ + public partial class BlobCredentials : Form + { + private string _authenticationKind; + private string _connectionName; + private string _accountKey; + private string _privacyLevel; + private float _dpiScaleFactor; + + public string AuthenticationKind + { + get { return _authenticationKind; } + set { _authenticationKind = value; } + } + public string ConnectionName + { + get { return _connectionName; } + set { _connectionName = value; } + } + public string AccountKey + { + get { return _accountKey; } + set { _accountKey = value; } + } + public string PrivacyLevel + { + get { return _privacyLevel; } + set { _privacyLevel = value; } + } + public float DpiScaleFactor + { + get { return _dpiScaleFactor; } + set { _dpiScaleFactor = value; } + } + + public BlobCredentials() + { + InitializeComponent(); + } + + private void BlobCredentials_Load(object sender, EventArgs e) + { + if (_dpiScaleFactor > 1) + { + //DPI + float dpiScaleFactorFudged = _dpiScaleFactor * HighDPIUtils.PrimaryFudgeFactor; + + this.Scale(new SizeF(dpiScaleFactorFudged * 0.44f, dpiScaleFactorFudged * 0.38f)); + this.Width = Convert.ToInt32(this.Width * dpiScaleFactorFudged * 0.6f); + foreach (Control control in HighDPIUtils.GetChildInControl(this)) + { + control.Font = new Font(control.Font.FontFamily, + control.Font.Size * dpiScaleFactorFudged * HighDPIUtils.PrimaryFudgeFactor, + control.Font.Style); + } + } + + this.KeyPreview = true; + + txtConnectionName.Text = _connectionName; + txtAccountKey.Text = _accountKey; + cboPrivacyLevel.Text = "None"; + + this.ActiveControl = txtAccountKey; + } + + private void ImpersonationCredentials_FormClosing(object sender, FormClosingEventArgs e) + { + if (DialogResult != DialogResult.OK) + { + //User Cancelling, so do nothing + return; + } + _accountKey = txtAccountKey.Text; + _privacyLevel = cboPrivacyLevel.Text; + } + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.resx b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/UI/BlobCredentials.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.Designer.cs b/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.Designer.cs new file mode 100644 index 0000000..3a9fc13 --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.Designer.cs @@ -0,0 +1,592 @@ +namespace BismNormalizer.TabularCompare.UI +{ + partial class ComparisonControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ComparisonControl)); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); + this.pnlHeader = new System.Windows.Forms.Panel(); + this.spltSourceTarget = new System.Windows.Forms.SplitContainer(); + this.label1 = new System.Windows.Forms.Label(); + this.txtSource = new System.Windows.Forms.TextBox(); + this.txtTarget = new System.Windows.Forms.TextBox(); + this.label2 = new System.Windows.Forms.Label(); + this.toolStrip1 = new System.Windows.Forms.ToolStrip(); + this.btnCompareTabularModels = new System.Windows.Forms.ToolStripButton(); + this.ddSelectActions = new System.Windows.Forms.ToolStripDropDownButton(); + this.mnuHideSkipObjects = new System.Windows.Forms.ToolStripMenuItem(); + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuShowSkipObjects = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); + this.mnuSkipAllObjectsMissingInSource = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuDeleteAllObjectsMissingInSource = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuSkipAllObjectsMissingInTarget = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuCreateAllObjectsMissingInTarget = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuSkipAllObjectsWithDifferentDefinitions = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuUpdateAllObjectsWithDifferentDefinitions = new System.Windows.Forms.ToolStripMenuItem(); + this.btnValidateSelection = new System.Windows.Forms.ToolStripButton(); + this.btnUpdate = new System.Windows.Forms.ToolStripButton(); + this.btnGenerateScript = new System.Windows.Forms.ToolStripButton(); + this.toolStripButton1 = new System.Windows.Forms.ToolStripSeparator(); + this.btnOptions = new System.Windows.Forms.ToolStripButton(); + this.btnReportDifferences = new System.Windows.Forms.ToolStripButton(); + this.scDifferenceResults = new System.Windows.Forms.SplitContainer(); + this.pnlProgressBar = new System.Windows.Forms.Panel(); + this.progressBar = new System.Windows.Forms.ProgressBar(); + this.lblProgressBar = new System.Windows.Forms.Label(); + this.treeGridComparisonResults = new BismNormalizer.TabularCompare.UI.TreeGridViewComparison(); + this.TreeGridImageList = new System.Windows.Forms.ImageList(this.components); + this.scObjectDefinitions = new System.Windows.Forms.SplitContainer(); + this.txtSourceObjectDefinition = new BismNormalizer.TabularCompare.UI.SynchronizedScrollRichTextBox(); + this.label4 = new System.Windows.Forms.Label(); + this.txtTargetObjectDefinition = new BismNormalizer.TabularCompare.UI.SynchronizedScrollRichTextBox(); + this.label5 = new System.Windows.Forms.Label(); + this.pnlHeader.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.spltSourceTarget)).BeginInit(); + this.spltSourceTarget.Panel1.SuspendLayout(); + this.spltSourceTarget.Panel2.SuspendLayout(); + this.spltSourceTarget.SuspendLayout(); + this.toolStrip1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.scDifferenceResults)).BeginInit(); + this.scDifferenceResults.Panel1.SuspendLayout(); + this.scDifferenceResults.Panel2.SuspendLayout(); + this.scDifferenceResults.SuspendLayout(); + this.pnlProgressBar.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.treeGridComparisonResults)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.scObjectDefinitions)).BeginInit(); + this.scObjectDefinitions.Panel1.SuspendLayout(); + this.scObjectDefinitions.Panel2.SuspendLayout(); + this.scObjectDefinitions.SuspendLayout(); + this.SuspendLayout(); + // + // pnlHeader + // + this.pnlHeader.Controls.Add(this.spltSourceTarget); + this.pnlHeader.Controls.Add(this.toolStrip1); + this.pnlHeader.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlHeader.Location = new System.Drawing.Point(0, 0); + this.pnlHeader.Name = "pnlHeader"; + this.pnlHeader.Size = new System.Drawing.Size(653, 55); + this.pnlHeader.TabIndex = 46; + // + // spltSourceTarget + // + this.spltSourceTarget.Dock = System.Windows.Forms.DockStyle.Fill; + this.spltSourceTarget.IsSplitterFixed = true; + this.spltSourceTarget.Location = new System.Drawing.Point(0, 25); + this.spltSourceTarget.Name = "spltSourceTarget"; + // + // spltSourceTarget.Panel1 + // + this.spltSourceTarget.Panel1.Controls.Add(this.label1); + this.spltSourceTarget.Panel1.Controls.Add(this.txtSource); + // + // spltSourceTarget.Panel2 + // + this.spltSourceTarget.Panel2.Controls.Add(this.txtTarget); + this.spltSourceTarget.Panel2.Controls.Add(this.label2); + this.spltSourceTarget.Size = new System.Drawing.Size(653, 30); + this.spltSourceTarget.SplitterDistance = 321; + this.spltSourceTarget.TabIndex = 45; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(2, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(41, 13); + this.label1.TabIndex = 39; + this.label1.Text = "Source"; + // + // txtSource + // + this.txtSource.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtSource.BackColor = System.Drawing.SystemColors.Control; + this.txtSource.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.txtSource.Location = new System.Drawing.Point(49, 7); + this.txtSource.Name = "txtSource"; + this.txtSource.Size = new System.Drawing.Size(269, 20); + this.txtSource.TabIndex = 41; + this.txtSource.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txt_KeyDown); + // + // txtTarget + // + this.txtTarget.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtTarget.BackColor = System.Drawing.SystemColors.Control; + this.txtTarget.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.txtTarget.Location = new System.Drawing.Point(45, 7); + this.txtTarget.Name = "txtTarget"; + this.txtTarget.Size = new System.Drawing.Size(272, 20); + this.txtTarget.TabIndex = 42; + this.txtTarget.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txt_KeyDown); + // + // label2 + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(3, 8); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(38, 13); + this.label2.TabIndex = 40; + this.label2.Text = "Target"; + // + // toolStrip1 + // + this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.btnCompareTabularModels, + this.ddSelectActions, + this.btnValidateSelection, + this.btnUpdate, + this.btnGenerateScript, + this.toolStripButton1, + this.btnOptions, + this.btnReportDifferences}); + this.toolStrip1.Location = new System.Drawing.Point(0, 0); + this.toolStrip1.Name = "toolStrip1"; + this.toolStrip1.Size = new System.Drawing.Size(653, 25); + this.toolStrip1.TabIndex = 46; + this.toolStrip1.Text = "toolStrip1"; + // + // btnCompareTabularModels + // + this.btnCompareTabularModels.Image = ((System.Drawing.Image)(resources.GetObject("btnCompareTabularModels.Image"))); + this.btnCompareTabularModels.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnCompareTabularModels.Name = "btnCompareTabularModels"; + this.btnCompareTabularModels.Size = new System.Drawing.Size(85, 22); + this.btnCompareTabularModels.Text = "Compare..."; + this.btnCompareTabularModels.ToolTipText = "Compare (Shift+Alt+C)"; + this.btnCompareTabularModels.Click += new System.EventHandler(this.btnCompareTabularModels_Click); + // + // ddSelectActions + // + this.ddSelectActions.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.mnuHideSkipObjects, + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem, + this.mnuShowSkipObjects, + this.toolStripSeparator1, + this.mnuSkipAllObjectsMissingInSource, + this.mnuDeleteAllObjectsMissingInSource, + this.mnuSkipAllObjectsMissingInTarget, + this.mnuCreateAllObjectsMissingInTarget, + this.mnuSkipAllObjectsWithDifferentDefinitions, + this.mnuUpdateAllObjectsWithDifferentDefinitions}); + this.ddSelectActions.Enabled = false; + this.ddSelectActions.Image = ((System.Drawing.Image)(resources.GetObject("ddSelectActions.Image"))); + this.ddSelectActions.ImageTransparentColor = System.Drawing.Color.Magenta; + this.ddSelectActions.Name = "ddSelectActions"; + this.ddSelectActions.Size = new System.Drawing.Size(110, 22); + this.ddSelectActions.Text = "Select Actions"; + // + // mnuHideSkipObjects + // + this.mnuHideSkipObjects.Name = "mnuHideSkipObjects"; + this.mnuHideSkipObjects.Size = new System.Drawing.Size(303, 22); + this.mnuHideSkipObjects.Text = "Hide Skip Objects"; + this.mnuHideSkipObjects.Click += new System.EventHandler(this.mnuHideSkipObjects_Click); + // + // hideSkipObjectsWithSameDefinitionToolStripMenuItem + // + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem.Name = "hideSkipObjectsWithSameDefinitionToolStripMenuItem"; + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem.Text = "Hide Skip Objects with Same Definition"; + this.hideSkipObjectsWithSameDefinitionToolStripMenuItem.Click += new System.EventHandler(this.mnuHideSkipObjectsWithSameDefinition_Click); + // + // mnuShowSkipObjects + // + this.mnuShowSkipObjects.Name = "mnuShowSkipObjects"; + this.mnuShowSkipObjects.Size = new System.Drawing.Size(303, 22); + this.mnuShowSkipObjects.Text = "Show Skip Objects"; + this.mnuShowSkipObjects.Click += new System.EventHandler(this.mnuShowSkipObjects_Click); + // + // toolStripSeparator1 + // + this.toolStripSeparator1.Name = "toolStripSeparator1"; + this.toolStripSeparator1.Size = new System.Drawing.Size(300, 6); + // + // mnuSkipAllObjectsMissingInSource + // + this.mnuSkipAllObjectsMissingInSource.Name = "mnuSkipAllObjectsMissingInSource"; + this.mnuSkipAllObjectsMissingInSource.Size = new System.Drawing.Size(303, 22); + this.mnuSkipAllObjectsMissingInSource.Text = "Skip all objects Missing in Source"; + this.mnuSkipAllObjectsMissingInSource.Click += new System.EventHandler(this.mnuSkipAllObjectsMissingInSource_Click); + // + // mnuDeleteAllObjectsMissingInSource + // + this.mnuDeleteAllObjectsMissingInSource.Name = "mnuDeleteAllObjectsMissingInSource"; + this.mnuDeleteAllObjectsMissingInSource.Size = new System.Drawing.Size(303, 22); + this.mnuDeleteAllObjectsMissingInSource.Text = "Delete all objects Missing in Source"; + this.mnuDeleteAllObjectsMissingInSource.Click += new System.EventHandler(this.mnuDeleteAllObjectsMissingInSource_Click); + // + // mnuSkipAllObjectsMissingInTarget + // + this.mnuSkipAllObjectsMissingInTarget.Name = "mnuSkipAllObjectsMissingInTarget"; + this.mnuSkipAllObjectsMissingInTarget.Size = new System.Drawing.Size(303, 22); + this.mnuSkipAllObjectsMissingInTarget.Text = "Skip all objects Missing in Target"; + this.mnuSkipAllObjectsMissingInTarget.Click += new System.EventHandler(this.mnuSkipAllObjectsMissingInTarget_Click); + // + // mnuCreateAllObjectsMissingInTarget + // + this.mnuCreateAllObjectsMissingInTarget.Name = "mnuCreateAllObjectsMissingInTarget"; + this.mnuCreateAllObjectsMissingInTarget.Size = new System.Drawing.Size(303, 22); + this.mnuCreateAllObjectsMissingInTarget.Text = "Create all objects Missing in Target"; + this.mnuCreateAllObjectsMissingInTarget.Click += new System.EventHandler(this.mnuCreateAllObjectsMissingInTarget_Click); + // + // mnuSkipAllObjectsWithDifferentDefinitions + // + this.mnuSkipAllObjectsWithDifferentDefinitions.Name = "mnuSkipAllObjectsWithDifferentDefinitions"; + this.mnuSkipAllObjectsWithDifferentDefinitions.Size = new System.Drawing.Size(303, 22); + this.mnuSkipAllObjectsWithDifferentDefinitions.Text = "Skip all objects with Different Definitions"; + this.mnuSkipAllObjectsWithDifferentDefinitions.Click += new System.EventHandler(this.mnuSkipAllObjectsWithDifferentDefinitions_Click); + // + // mnuUpdateAllObjectsWithDifferentDefinitions + // + this.mnuUpdateAllObjectsWithDifferentDefinitions.Name = "mnuUpdateAllObjectsWithDifferentDefinitions"; + this.mnuUpdateAllObjectsWithDifferentDefinitions.Size = new System.Drawing.Size(303, 22); + this.mnuUpdateAllObjectsWithDifferentDefinitions.Text = "Update all objects with Different Definitions"; + this.mnuUpdateAllObjectsWithDifferentDefinitions.Click += new System.EventHandler(this.mnuUpdateAllObjectsWithDifferentDefinitions_Click); + // + // btnValidateSelection + // + this.btnValidateSelection.Enabled = false; + this.btnValidateSelection.Image = ((System.Drawing.Image)(resources.GetObject("btnValidateSelection.Image"))); + this.btnValidateSelection.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnValidateSelection.Name = "btnValidateSelection"; + this.btnValidateSelection.Size = new System.Drawing.Size(119, 22); + this.btnValidateSelection.Text = "Validate Selection"; + this.btnValidateSelection.Click += new System.EventHandler(this.btnValidateSelection_Click); + // + // btnUpdate + // + this.btnUpdate.Enabled = false; + this.btnUpdate.Image = ((System.Drawing.Image)(resources.GetObject("btnUpdate.Image"))); + this.btnUpdate.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnUpdate.Name = "btnUpdate"; + this.btnUpdate.Size = new System.Drawing.Size(65, 22); + this.btnUpdate.Text = "Update"; + this.btnUpdate.Click += new System.EventHandler(this.btnUpdate_Click); + // + // btnGenerateScript + // + this.btnGenerateScript.Enabled = false; + this.btnGenerateScript.Image = ((System.Drawing.Image)(resources.GetObject("btnGenerateScript.Image"))); + this.btnGenerateScript.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnGenerateScript.Name = "btnGenerateScript"; + this.btnGenerateScript.Size = new System.Drawing.Size(107, 22); + this.btnGenerateScript.Text = "Generate Script"; + this.btnGenerateScript.Click += new System.EventHandler(this.btnGenerateScript_Click); + // + // toolStripButton1 + // + this.toolStripButton1.Name = "toolStripButton1"; + this.toolStripButton1.Size = new System.Drawing.Size(6, 25); + // + // btnOptions + // + this.btnOptions.Image = ((System.Drawing.Image)(resources.GetObject("btnOptions.Image"))); + this.btnOptions.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnOptions.Name = "btnOptions"; + this.btnOptions.Size = new System.Drawing.Size(69, 22); + this.btnOptions.Text = "Options"; + this.btnOptions.Click += new System.EventHandler(this.btnOptions_Click); + // + // btnReportDifferences + // + this.btnReportDifferences.Enabled = false; + this.btnReportDifferences.Image = ((System.Drawing.Image)(resources.GetObject("btnReportDifferences.Image"))); + this.btnReportDifferences.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnReportDifferences.Name = "btnReportDifferences"; + this.btnReportDifferences.Size = new System.Drawing.Size(124, 20); + this.btnReportDifferences.Text = "Report Differences"; + this.btnReportDifferences.Click += new System.EventHandler(this.btnReportDifferences_Click); + // + // scDifferenceResults + // + this.scDifferenceResults.Dock = System.Windows.Forms.DockStyle.Fill; + this.scDifferenceResults.Location = new System.Drawing.Point(0, 55); + this.scDifferenceResults.Name = "scDifferenceResults"; + this.scDifferenceResults.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // scDifferenceResults.Panel1 + // + this.scDifferenceResults.Panel1.Controls.Add(this.pnlProgressBar); + this.scDifferenceResults.Panel1.Controls.Add(this.treeGridComparisonResults); + // + // scDifferenceResults.Panel2 + // + this.scDifferenceResults.Panel2.Controls.Add(this.scObjectDefinitions); + this.scDifferenceResults.Size = new System.Drawing.Size(653, 510); + this.scDifferenceResults.SplitterDistance = 371; + this.scDifferenceResults.TabIndex = 2; + // + // pnlProgressBar + // + this.pnlProgressBar.BackColor = System.Drawing.SystemColors.Control; + this.pnlProgressBar.Controls.Add(this.progressBar); + this.pnlProgressBar.Controls.Add(this.lblProgressBar); + this.pnlProgressBar.Location = new System.Drawing.Point(60, 21); + this.pnlProgressBar.Name = "pnlProgressBar"; + this.pnlProgressBar.Size = new System.Drawing.Size(280, 54); + this.pnlProgressBar.TabIndex = 1; + this.pnlProgressBar.Visible = false; + // + // progressBar + // + this.progressBar.Location = new System.Drawing.Point(120, 16); + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(137, 18); + this.progressBar.Step = 1; + this.progressBar.TabIndex = 1; + // + // lblProgressBar + // + this.lblProgressBar.AutoSize = true; + this.lblProgressBar.Location = new System.Drawing.Point(12, 16); + this.lblProgressBar.Name = "lblProgressBar"; + this.lblProgressBar.Size = new System.Drawing.Size(101, 13); + this.lblProgressBar.TabIndex = 0; + this.lblProgressBar.Text = "Generating report ..."; + // + // treeGridComparisonResults + // + this.treeGridComparisonResults.AllowUserToAddRows = false; + this.treeGridComparisonResults.AllowUserToDeleteRows = false; + this.treeGridComparisonResults.AllowUserToResizeRows = false; + this.treeGridComparisonResults.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells; + dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Control; + dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText; + dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; + this.treeGridComparisonResults.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; + this.treeGridComparisonResults.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.treeGridComparisonResults.Comparison = null; + dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight; + dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText; + dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.False; + this.treeGridComparisonResults.DefaultCellStyle = dataGridViewCellStyle2; + this.treeGridComparisonResults.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeGridComparisonResults.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter; + this.treeGridComparisonResults.ImageList = this.TreeGridImageList; + this.treeGridComparisonResults.Location = new System.Drawing.Point(0, 0); + this.treeGridComparisonResults.Name = "treeGridComparisonResults"; + this.treeGridComparisonResults.RowHeadersVisible = false; + this.treeGridComparisonResults.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect; + this.treeGridComparisonResults.Size = new System.Drawing.Size(653, 371); + this.treeGridComparisonResults.TabIndex = 0; + this.treeGridComparisonResults.Unloading = false; + this.treeGridComparisonResults.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.treeGridComparisonResults_DataError); + this.treeGridComparisonResults.MouseUp += new System.Windows.Forms.MouseEventHandler(this.treeGridComparisonResults_MouseUp); + // + // TreeGridImageList + // + this.TreeGridImageList.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("TreeGridImageList.ImageStream"))); + this.TreeGridImageList.TransparentColor = System.Drawing.Color.Transparent; + this.TreeGridImageList.Images.SetKeyName(0, "Connection.png"); + this.TreeGridImageList.Images.SetKeyName(1, "Table.png"); + this.TreeGridImageList.Images.SetKeyName(2, "Relationship.png"); + this.TreeGridImageList.Images.SetKeyName(3, "BismMeasure.png"); + this.TreeGridImageList.Images.SetKeyName(4, "KPI.png"); + this.TreeGridImageList.Images.SetKeyName(5, "DeleteAction.png"); + this.TreeGridImageList.Images.SetKeyName(6, "UpdateAction.png"); + this.TreeGridImageList.Images.SetKeyName(7, "CreateAction.png"); + this.TreeGridImageList.Images.SetKeyName(8, "SkipAction.png"); + this.TreeGridImageList.Images.SetKeyName(9, "Plus.png"); + this.TreeGridImageList.Images.SetKeyName(10, "Minus.png"); + this.TreeGridImageList.Images.SetKeyName(11, "Informational.png"); + this.TreeGridImageList.Images.SetKeyName(12, "Warning.png"); + this.TreeGridImageList.Images.SetKeyName(13, "WarningToolWindow.png"); + this.TreeGridImageList.Images.SetKeyName(14, "Role.png"); + this.TreeGridImageList.Images.SetKeyName(15, "Perspective.png"); + this.TreeGridImageList.Images.SetKeyName(16, "Action.png"); + this.TreeGridImageList.Images.SetKeyName(17, "CompareBismModels_Small.png"); + this.TreeGridImageList.Images.SetKeyName(18, "DeleteActionGrey.png"); + this.TreeGridImageList.Images.SetKeyName(19, "SkipActionGrey.png"); + this.TreeGridImageList.Images.SetKeyName(20, "CreateActionGrey.png"); + this.TreeGridImageList.Images.SetKeyName(21, "Culture.png"); + this.TreeGridImageList.Images.SetKeyName(22, "Expression.png"); + // + // scObjectDefinitions + // + this.scObjectDefinitions.Dock = System.Windows.Forms.DockStyle.Fill; + this.scObjectDefinitions.Location = new System.Drawing.Point(0, 0); + this.scObjectDefinitions.Name = "scObjectDefinitions"; + // + // scObjectDefinitions.Panel1 + // + this.scObjectDefinitions.Panel1.BackColor = System.Drawing.SystemColors.Control; + this.scObjectDefinitions.Panel1.Controls.Add(this.txtSourceObjectDefinition); + this.scObjectDefinitions.Panel1.Controls.Add(this.label4); + // + // scObjectDefinitions.Panel2 + // + this.scObjectDefinitions.Panel2.BackColor = System.Drawing.SystemColors.Control; + this.scObjectDefinitions.Panel2.Controls.Add(this.txtTargetObjectDefinition); + this.scObjectDefinitions.Panel2.Controls.Add(this.label5); + this.scObjectDefinitions.Size = new System.Drawing.Size(653, 135); + this.scObjectDefinitions.SplitterDistance = 331; + this.scObjectDefinitions.TabIndex = 0; + // + // txtSourceObjectDefinition + // + this.txtSourceObjectDefinition.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtSourceObjectDefinition.BackColor = System.Drawing.Color.White; + this.txtSourceObjectDefinition.Font = new System.Drawing.Font("Consolas", 9.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.txtSourceObjectDefinition.Location = new System.Drawing.Point(0, 16); + this.txtSourceObjectDefinition.Name = "txtSourceObjectDefinition"; + this.txtSourceObjectDefinition.ReadOnly = true; + this.txtSourceObjectDefinition.Size = new System.Drawing.Size(331, 119); + this.txtSourceObjectDefinition.TabIndex = 1; + this.txtSourceObjectDefinition.Text = ""; + this.txtSourceObjectDefinition.WordWrap = false; + this.txtSourceObjectDefinition.vScroll += new BismNormalizer.TabularCompare.UI.SynchronizedScrollRichTextBox.vScrollEventHandler(this.txtSourceObjectDefinition_vScroll); + this.txtSourceObjectDefinition.KeyUp += new System.Windows.Forms.KeyEventHandler(this.txtSourceObjectDefinition_KeyUp); + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(4, 1); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(122, 13); + this.label4.TabIndex = 0; + this.label4.Text = "Source Object Definition"; + // + // txtTargetObjectDefinition + // + this.txtTargetObjectDefinition.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtTargetObjectDefinition.BackColor = System.Drawing.Color.White; + this.txtTargetObjectDefinition.Font = new System.Drawing.Font("Consolas", 9.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.txtTargetObjectDefinition.Location = new System.Drawing.Point(0, 16); + this.txtTargetObjectDefinition.Name = "txtTargetObjectDefinition"; + this.txtTargetObjectDefinition.ReadOnly = true; + this.txtTargetObjectDefinition.Size = new System.Drawing.Size(313, 119); + this.txtTargetObjectDefinition.TabIndex = 2; + this.txtTargetObjectDefinition.Text = ""; + this.txtTargetObjectDefinition.WordWrap = false; + this.txtTargetObjectDefinition.vScroll += new BismNormalizer.TabularCompare.UI.SynchronizedScrollRichTextBox.vScrollEventHandler(this.txtTargetObjectDefinition_vScroll); + this.txtTargetObjectDefinition.KeyUp += new System.Windows.Forms.KeyEventHandler(this.txtTargetObjectDefinition_KeyUp); + // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(3, 1); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(119, 13); + this.label5.TabIndex = 1; + this.label5.Text = "Target Object Definition"; + // + // ComparisonControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.scDifferenceResults); + this.Controls.Add(this.pnlHeader); + this.Name = "ComparisonControl"; + this.Size = new System.Drawing.Size(653, 565); + this.Load += new System.EventHandler(this.BismNormalizer_Load); + this.pnlHeader.ResumeLayout(false); + this.pnlHeader.PerformLayout(); + this.spltSourceTarget.Panel1.ResumeLayout(false); + this.spltSourceTarget.Panel1.PerformLayout(); + this.spltSourceTarget.Panel2.ResumeLayout(false); + this.spltSourceTarget.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.spltSourceTarget)).EndInit(); + this.spltSourceTarget.ResumeLayout(false); + this.toolStrip1.ResumeLayout(false); + this.toolStrip1.PerformLayout(); + this.scDifferenceResults.Panel1.ResumeLayout(false); + this.scDifferenceResults.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.scDifferenceResults)).EndInit(); + this.scDifferenceResults.ResumeLayout(false); + this.pnlProgressBar.ResumeLayout(false); + this.pnlProgressBar.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.treeGridComparisonResults)).EndInit(); + this.scObjectDefinitions.Panel1.ResumeLayout(false); + this.scObjectDefinitions.Panel1.PerformLayout(); + this.scObjectDefinitions.Panel2.ResumeLayout(false); + this.scObjectDefinitions.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.scObjectDefinitions)).EndInit(); + this.scObjectDefinitions.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private TreeGridViewComparison treeGridComparisonResults; + private System.Windows.Forms.SplitContainer scDifferenceResults; + private System.Windows.Forms.SplitContainer scObjectDefinitions; + private SynchronizedScrollRichTextBox txtSourceObjectDefinition; + private System.Windows.Forms.Label label4; + private SynchronizedScrollRichTextBox txtTargetObjectDefinition; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.Panel pnlProgressBar; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.Label lblProgressBar; + private System.Windows.Forms.Panel pnlHeader; + public System.Windows.Forms.ImageList TreeGridImageList; + private System.Windows.Forms.SplitContainer spltSourceTarget; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox txtSource; + private System.Windows.Forms.TextBox txtTarget; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.ToolStrip toolStrip1; + private System.Windows.Forms.ToolStripButton btnCompareTabularModels; + private System.Windows.Forms.ToolStripDropDownButton ddSelectActions; + private System.Windows.Forms.ToolStripMenuItem mnuHideSkipObjects; + private System.Windows.Forms.ToolStripMenuItem mnuShowSkipObjects; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; + private System.Windows.Forms.ToolStripMenuItem mnuSkipAllObjectsMissingInSource; + private System.Windows.Forms.ToolStripMenuItem mnuDeleteAllObjectsMissingInSource; + private System.Windows.Forms.ToolStripMenuItem mnuSkipAllObjectsMissingInTarget; + private System.Windows.Forms.ToolStripMenuItem mnuCreateAllObjectsMissingInTarget; + private System.Windows.Forms.ToolStripMenuItem mnuSkipAllObjectsWithDifferentDefinitions; + private System.Windows.Forms.ToolStripMenuItem mnuUpdateAllObjectsWithDifferentDefinitions; + private System.Windows.Forms.ToolStripButton btnValidateSelection; + private System.Windows.Forms.ToolStripButton btnUpdate; + private System.Windows.Forms.ToolStripButton btnGenerateScript; + private System.Windows.Forms.ToolStripSeparator toolStripButton1; + private System.Windows.Forms.ToolStripButton btnOptions; + private System.Windows.Forms.ToolStripButton btnReportDifferences; + private System.Windows.Forms.ToolStripMenuItem hideSkipObjectsWithSameDefinitionToolStripMenuItem; + } +} diff --git a/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.cs b/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.cs new file mode 100644 index 0000000..e3ecd9c --- /dev/null +++ b/BismNormalizer/BismNormalizer/TabularCompare/UI/ComparisonControl.cs @@ -0,0 +1,1154 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using System.Windows.Forms; +using Microsoft.VisualStudio.CommandBars; +using EnvDTE; +using EnvDTE80; +using BismNormalizer.TabularCompare.Core; +using System.Runtime.InteropServices; +using System.Linq; + +namespace BismNormalizer.TabularCompare.UI +{ + public enum CompareState { NotCompared, Compared, Validated }; + + /// + /// The main BISM Normalizer comparison control, containing the differences grid, and source/target object definition text boxes. + /// + public partial class ComparisonControl : UserControl + { + #region Private variables + + private ComparisonInfo _comparisonInfo; + private Comparison _comparison; + private ContextMenu _menuComparisonGrid = new ContextMenu(); + + private BismNormalizerPackage _bismNormalizerPackage; + private EditorPane _editorPane; + + private const string _bismNormalizerCaption = "BISM Normalizer"; + private CompareState _compareState = CompareState.NotCompared; + + #endregion + + #region DiffVariables + + // this is the diff object; + DiffMatchPatch _diff = new DiffMatchPatch(); + + // these are the diffs + List _diffs; + + // chunks for formatting the two RTBs: + List _chunklistSource; + List _chunklistTarget; + + // color list: + Color[] _backColors = new Color[3] { ColorTranslator.FromHtml("#e2f6c5"), ColorTranslator.FromHtml("#ffd6d5"), Color.White, }; + Color[] _backColorsMerge = new Color[3] { ColorTranslator.FromHtml("#e2f6c5"), Color.LightGray, Color.White, }; + + public struct Chunk + { + public int StartPosition; + public int Length; + public Color BackColor; + } + + #endregion + + #region DPI + + private float _dpiScaleFactor = 1; + private void Rescale() + { + this._dpiScaleFactor = HighDPIUtils.GetDpiFactor(); + if (this._dpiScaleFactor == 1) return; + float fudgedDpiScaleFactor = _dpiScaleFactor * HighDPIUtils.PrimaryFudgeFactor; + + this.Scale(new SizeF(fudgedDpiScaleFactor, fudgedDpiScaleFactor)); + + this.Font = new Font(this.Font.FontFamily, + this.Font.Size * fudgedDpiScaleFactor, + this.Font.Style); + //foreach (Control c in HighDPIUtils.GetChildInControl(this)) //.OfType