Analysis-Services/SetUpAggs/Program.cs

473 lines
18 KiB
C#
Raw Normal View History

2018-12-14 11:02:43 +08:00
using Microsoft.AnalysisServices.Tabular;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PowerArgs;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace SetUpAggs
{
#region JsonHelper Class
public static class JsonHelper
{
public static string FromClass<T>(T data, bool isEmptyToNull = false,
JsonSerializerSettings jsonSettings = null)
{
string response = string.Empty;
if (!EqualityComparer<T>.Default.Equals(data, default(T)))
response = JsonConvert.SerializeObject(data, jsonSettings);
return isEmptyToNull ? (response == "{}" ? "null" : response) : response;
}
public static T ToClass<T>(string data, JsonSerializerSettings jsonSettings = null)
{
var response = default(T);
if (!string.IsNullOrEmpty(data))
response = jsonSettings == null
? JsonConvert.DeserializeObject<T>(data)
: JsonConvert.DeserializeObject<T>(data, jsonSettings);
return response;
}
}
#endregion
#region Json Configuration Classes
public partial class AggsConfiguration
{
[JsonProperty("database")]
public Database Database { get; set; }
}
public partial class Database
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("tables")]
public Table[] Tables { get; set; }
}
public partial class Table
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("mode")]
public string Mode { get; set; }
[JsonProperty("refreshType")]
public string RefreshType { get; set; }
2018-12-14 11:02:43 +08:00
[JsonProperty("aggregationRules", NullValueHandling = NullValueHandling.Ignore)]
public AggregationRule[] AggregationRules { get; set; }
}
public partial class AggregationRule
{
[JsonProperty("aggTableColumn")]
public string AggTableColumn { get; set; }
[JsonProperty("summarization")]
public string Summarization { get; set; }
[JsonProperty("detailTable")]
public string DetailTable { get; set; }
[JsonProperty("detailTableColumn", NullValueHandling = NullValueHandling.Ignore)]
public string DetailTableColumn { get; set; }
}
public partial class Object
{
[JsonProperty("database")]
public string Database { get; set; }
}
public partial class AggsConfiguration
{
public static AggsConfiguration FromJson(string json) => JsonConvert.DeserializeObject<AggsConfiguration>(json, SetUpAggs.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this AggsConfiguration self) => JsonConvert.SerializeObject(self, SetUpAggs.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
#endregion
#region Command Line Utility Arguments Handling
[ArgExceptionBehavior(ArgExceptionPolicy.StandardExceptionHandling)]
public class AggConfigurationUtility
{
Microsoft.AnalysisServices.Tabular.Server server = new Microsoft.AnalysisServices.Tabular.Server();
Microsoft.AnalysisServices.Tabular.Database database = new Microsoft.AnalysisServices.Tabular.Database();
AggsConfiguration aggsConfig;
[HelpHook, ArgShortcut("-?"), ArgDescription("Shows this help")]
public bool Help { get; set; }
/// <summary>
/// Validate the configuration file against the model before applying changes
/// </summary>
/// <param name="tableConfig"></param>
private void ValidateConfigurationOfTable(Table tableConfig)
{
var table = database.Model.Tables.Find(tableConfig.Name);
if (table == null)
throw new Exception($"Could not find table [{tableConfig.Name}]");
try
{
Enum.Parse(typeof(ModeType), tableConfig.Mode);
}
catch(Exception)
{
throw new Exception($"Table [{tableConfig.Name}] has invalid mode type {tableConfig.Mode}, must be one of Default, DirectQuery, Dual, or Import");
}
if (table.Partitions.Count == 0)
throw new Exception($"No partitions found for table [{table.Name}]");
if(tableConfig.AggregationRules != null)
foreach (AggregationRule rule in tableConfig.AggregationRules)
{
if(rule.Summarization != "CountTableRows")
try
{
Enum.Parse(typeof(SummarizationType), rule.Summarization);
}
catch (Exception)
{
throw new Exception($"Aggregation rule summarization invalid [{rule.Summarization}], must be one of GroupBy, Sum, Min, Max, Count, or CountTableRows");
}
Microsoft.AnalysisServices.Tabular.Column aggColumn = table.Columns.Find(rule.AggTableColumn);
if (aggColumn == null)
throw new Exception($"Cound not find aggregation column [{table.Name}].[{rule.AggTableColumn}] in model");
var detailTable = database.Model.Tables.Find(rule.DetailTable);
if (detailTable == null)
throw new Exception($"Cound not find detail table [{rule.DetailTable}] in model");
var detailColumn = (rule.DetailTableColumn == null ? null : detailTable.Columns.Find(rule.DetailTableColumn));
bool isCountAggregate = rule.Summarization != "Count" || rule.Summarization != "CountTableRows";
if (detailColumn == null && !isCountAggregate)
throw new Exception($"Cound not find detail column [{rule.DetailTable}].[{rule.DetailTableColumn}] in model");
if (!isCountAggregate && aggColumn.DataType != detailColumn.DataType)
throw new Exception($"Data type mismatch for aggregation column [{table.Name}].[{rule.AggTableColumn}] and detail column [{rule.DetailTable}].[{rule.DetailTableColumn}]");
}
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"Validated aggregation rules for [{table.Name}], mode currently set to {table.Partitions[0].Mode.ToString()}");
}
/// <summary>
/// Applies the differences between the configuration file and the model
/// </summary>
/// <param name="tableConfig"></param>
private int ApplyConfigurationOfTable(Table tableConfig)
{
// Update mode of each partition of the table if a difference is found between the model and the configuration
var table = database.Model.Tables.Find(tableConfig.Name);
int partitionModesChanged = 0;
table.Partitions.ForEach(p =>
{
var mode = (ModeType)Enum.Parse(typeof(ModeType), tableConfig.Mode);
if (p.Mode != mode)
{
p.Mode = mode;
partitionModesChanged++;
}
}
);
// Remove all alternateOf definitions from the model that do not match an aggregation rule in the configuration
int alternateOfRemoved = 0;
table.Columns.ForEach(c =>
{
int ruleMatch = 0;
if(tableConfig.AggregationRules != null && c.AlternateOf != null)
ruleMatch = tableConfig.AggregationRules.Where(r =>
r.Summarization != "CountTableRows" &&
r.AggTableColumn == c.Name &&
r.DetailTable == c.AlternateOf.BaseColumn.Table.Name &&
r.DetailTableColumn == c.AlternateOf.BaseColumn.Name &&
(SummarizationType)Enum.Parse(typeof(SummarizationType), r.Summarization) == c.AlternateOf.Summarization
).Count() +
tableConfig.AggregationRules.Where(r =>
r.Summarization == "CountTableRows" &&
r.AggTableColumn == c.Name &&
r.DetailTable == c.AlternateOf.BaseTable.Name &&
c.AlternateOf.Summarization == SummarizationType.Count
).Count();
if (ruleMatch == 0 && c.AlternateOf != null)
{
var baseObjectName = c.AlternateOf.BaseColumn == null ?
String.Format("[{0}]", c.AlternateOf.BaseTable.Name) :
String.Format("[{0}].[{1}]", c.AlternateOf.BaseColumn.Table.Name, c.AlternateOf.BaseColumn.Name);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Removing aggregation rule from column [{c.Name}] with alternate of {baseObjectName} aggregation {c.AlternateOf.Summarization}");
c.AlternateOf = null;
alternateOfRemoved++;
}
}
);
// Add all alternateOf definitions from the configuration that do not match an alternateOf definition in the model
int alternateOfAdded = 0;
if(tableConfig.AggregationRules != null)
tableConfig.AggregationRules.ForEach(r =>
{
var column = table.Columns.First(c => c.Name == r.AggTableColumn);
var detailTable = database.Model.Tables.Find(r.DetailTable);
var detailColumn = (r.DetailTableColumn != null) ? detailTable.Columns.Find(r.DetailTableColumn) : null;
if (column.AlternateOf == null)
{
if (r.Summarization == "CountTableRows")
column.AlternateOf = new AlternateOf
{
Summarization = SummarizationType.Count,
BaseTable = detailTable
};
else
column.AlternateOf = new AlternateOf
{
Summarization = (SummarizationType)Enum.Parse(typeof(SummarizationType), r.Summarization),
BaseColumn = detailColumn
};
alternateOfAdded++;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"Adding aggregation rule for column [{column.Name}] with alternate of [{detailTable.Name + (detailColumn == null ? string.Empty : "].[" + detailColumn.Name)}] aggregation {column.AlternateOf.Summarization}");
}
}
);
return partitionModesChanged + alternateOfRemoved + alternateOfAdded;
}
/// <summary>
/// Connect to analysis services instance and database, throw if error is encountered
/// </summary>
/// <param name="serverName"></param>
private void Connect(string serverName)
{
// Changed this line to force login dialogs
2018-12-14 11:02:43 +08:00
//string connectionString = $"Provider=MSOLAP;Data Source={args.Server};";
string connectionString = $"Provider=MSOLAP;Data Source={serverName};Persist Security Info=True;";
2018-12-14 11:02:43 +08:00
server.Connect(connectionString);
database = server.Databases.FindByName(aggsConfig.Database.Name);
if (database == null)
{
throw new Microsoft.AnalysisServices.ConnectionException($"Could not find database [{aggsConfig.Database.Name}]");
}
}
[ArgActionMethod, ArgDescription("Applies the Aggs configuration to the model specified")]
public void Apply(ServerAndConfigurationArgs args)
{
List<Microsoft.AnalysisServices.Tabular.Table> updatedTableCollection = new List<Microsoft.AnalysisServices.Tabular.Table>();
// Read configuration file
aggsConfig = JsonHelper.ToClass<AggsConfiguration>(File.ReadAllText(args.ConfigFile));
#region Connect
Connect(args.Server);
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"Start: {DateTime.Now.ToString("hh:mm:ss tt")}");
Console.WriteLine($"Server: {args.Server}");
Console.WriteLine($"Database: {aggsConfig.Database.Name}");
#endregion
#region Validate Configuration Against Model
aggsConfig.Database.Tables.ForEach(x => ValidateConfigurationOfTable(x));
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Validation successful");
#endregion
#region Update Model Compatibility
//Set database compat level
if (database.CompatibilityLevel < 1465)
{
database.CompatibilityLevel = 1465;
database.Update();
}
#endregion
#region Apply Model Changes
var changesApplied = 0;
aggsConfig.Database.Tables.ForEach(x =>
{
var delta = ApplyConfigurationOfTable(x);
if (delta > 0)
updatedTableCollection.Add(database.Model.Tables.Find(x.Name));
changesApplied += delta;
}
);
if (changesApplied == 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"No changes applied to model");
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Applied model changes");
}
#endregion
#region Expand Full
if (changesApplied == 0)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Skipping write to database, no changes detected");
}
else
{
// Set database UpdateOptions to ExpandFull
try
{
Console.ForegroundColor = ConsoleColor.White;
Console.Write("Writing model changes to database ");
database.Update(Microsoft.AnalysisServices.UpdateOptions.ExpandFull);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("SUCCESS");
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("FAILED");
throw e;
}
}
#endregion
#region Refresh Tables as Required
if (changesApplied == 0)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Skipping table refresh, no changes detected");
}
else
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Refreshing tables - begin: " + DateTime.Now.ToString("hh:mm:ss tt"));
foreach (var tableObj in updatedTableCollection)
{
Console.ForegroundColor = ConsoleColor.White;
Console.Write($"Refreshing table [{tableObj.Name}] ");
Table tableConfig = aggsConfig.Database.Tables.First(x => x.Name == tableObj.Name);
var refreshType = (RefreshType)Enum.Parse(typeof(RefreshType), tableConfig.RefreshType);
tableObj.RequestRefresh(refreshType);
2018-12-14 11:02:43 +08:00
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("COMPLETE");
}
// Removed this line which was throwing errors on large models, and it seems to commit the changes without it
// database.Model.SaveChanges();
2018-12-14 11:02:43 +08:00
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Refreshing tables - end: " + DateTime.Now.ToString("hh:mm:ss tt"));
}
#endregion
}
}
public class ServerAndConfigurationArgs
{
[ArgRequired, ArgDescription("The URL to the Azure Analysis Services instance e.g.: asazure://host/instance"), ArgPosition(1)]
public string Server { get; set; }
[ArgRequired, ArgDescription("The file path to the JSON configuration file to apply to the database"), ArgPosition(2)]
public string ConfigFile { get; set; }
}
#endregion
class Program
{
static void Main(string[] args)
{
try
{
Args.InvokeAction<AggConfigurationUtility>(args);
}
catch (Exception exc)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("");
Console.WriteLine($"Exception occurred: {DateTime.Now.ToString("hh:mm:ss tt")}");
Console.WriteLine($"Exception message: {exc.Message}");
}
finally
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
}
}