using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Procs; using Sarand.Net.Standard.Base.Utils; using Sarand.Net.Standard.Dbas.Base; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Sarand.Net.Standard.Dbas.Proc { //======================================================================= public class SaDbProcess_CompareDatabaseSchemas : SaProcess_Base { //------------------------------------------------------------------- // Configuration. public new class Config : SaProcess_Base.Config { public Config(ISacsDatabase sourceDatabase, ISacsDatabase targetDatabase) { SourceDatabase = sourceDatabase; TargetDatabase = targetDatabase; } public Config(List sourceTables, ISacsDatabase targetDatabase) { SourceTables = sourceTables; TargetDatabase = targetDatabase; } public ISacsDatabase SourceDatabase { get; } public List SourceTables { get; } public ISacsDatabase TargetDatabase { get; } public bool HideIdenticalObjects { get; set; } public bool IgnoreNullable { get; set; } public bool IgnorePosition { get; set; } public bool IgnoreSimilarCharTypes { get; set; } public bool IgnoreSimilarNumericTypes { get; set; } } //------------------------------------------------------------------- // Definitions. public class SchemaComparisonItem { internal SchemaComparisonItem(String schemaName, String tableName, String columnName, int columnPosition, String indexName, String comment) { Id = ++SeedId; SchemaName = schemaName; TableName = tableName; ColumnName = columnName; ColumnPosition = columnPosition; IndexName = indexName; Comment = comment; } public static int SeedId { get; private set; } public int Id { get; } public String SchemaName { get; } public String TableName { get; } public String ColumnName { get; } public int ColumnPosition { get; } public String IndexName { get; } public String Comment { get; } } //------------------------------------------------------------------- // Data members. private Config config; private readonly List schemaComparisonItems = new List(); //------------------------------------------------------------------- // Construction. public SaDbProcess_CompareDatabaseSchemas(Config config) : base(config) { this.config = config; } protected override void Dispose(bool disposing) { if (disposing) { } base.Dispose(disposing); } //------------------------------------------------------------------- // Properties. public override string ProcessDescription { get; } = "Database Schema Comparison"; //------------------------------------------------------------------- // Events. // Processing Completed. public class SchemaComparisonItemAddedEventArgs : EventArgs { } public event EventHandler OnSchemaComparisonItemAdded; protected virtual void InvokeSchemaComparisonItemAdded() { if (OnSchemaComparisonItemAdded != null) { SchemaComparisonItemAddedEventArgs args = new SchemaComparisonItemAddedEventArgs(); OnSchemaComparisonItemAdded(this, args); } } //------------------------------------------------------------------- // S c h e m a D i f f e r e n c e s //------------------------------------------------------------------- public List GetSchemaComparisonItems(int minId = 0) { return new List(schemaComparisonItems.FindAll(item => item.Id >= minId)); } //------------------------------------------------------------------- // P r o c e s s i n g //------------------------------------------------------------------- protected override bool Run() { schemaComparisonItems.Clear(); if (config.SourceDatabase == null && config.SourceTables == null) return AddErrorMessage("Source database must be supplied."); CompareTables(); return OK; } //------------------------------------------------------------------- private void CompareTables() { // Database schemas. HashSet sourceTables = (config.SourceDatabase?.GetTableNames(true) ?? config.SourceTables.Select(item => item.Name)).OrderBy(item => item).ToHashSet(); HashSet targetTables = config.TargetDatabase.GetTableNames(true).OrderBy(item => item).ToHashSet(); for (int sourceTableIdx = 0, targetTableIdx = 0; sourceTableIdx < sourceTables.Count || targetTableIdx < targetTables.Count;) { // Next tables in sequence. String sourceTableName = (sourceTableIdx < sourceTables.Count ? sourceTables.ElementAt(sourceTableIdx) : ""); String targetTableName = (targetTableIdx < targetTables.Count ? targetTables.ElementAt(targetTableIdx) : ""); // Retrieve table definition from each database. DbDfnTable sourceTableDfn = !StringUtil.IsBlank(sourceTableName) ? config.SourceDatabase?.GetTableDfn(sourceTableName, true) ?? config.SourceTables.GetTableByName(sourceTableName) : null; DbDfnTable targetTableDfn = !StringUtil.IsBlank(targetTableName) ? config.TargetDatabase.GetTableDfn(targetTableName, true) : null; if (!StringUtil.IsEmpty(sourceTableName) && (StringUtil.IsEmpty(targetTableName) || targetTableName.CompareTo(sourceTableName) > 0)) { // Target table missing. AddSchemaComparisonItem(sourceTableDfn?.Schema, sourceTableName, "", 0, "", "Table is missing"); sourceTableIdx++; continue; } if (StringUtil.IsEmpty(sourceTableName) || sourceTableName.CompareTo(targetTableName) > 0) { // Source table missing. AddSchemaComparisonItem(targetTableDfn?.Schema, targetTableName, "", 0, "", "Table is not required"); targetTableIdx++; continue; } SetProgressInfo("Processing table: " + sourceTableName); // Table is present in both databases. if (!config.HideIdenticalObjects) AddSchemaComparisonItem(sourceTableDfn?.Schema, sourceTableName, "", 0, "", "Table OK"); #if DEBUG_X if (sourceTableName.EqualsCI("address")) Debugger.Break(); #endif // Compare the columns and indices. CompareTableColumns(sourceTableDfn, targetTableDfn); CompareTableIndices(sourceTableDfn, targetTableDfn); // Move to next table in BOTH lists. sourceTableIdx++; targetTableIdx++; } } //------------------------------------------------------------------- private bool CompareTableColumns(DbDfnTable sourceTableDfn, DbDfnTable targetTableDfn) { String tableName = sourceTableDfn.Name; String schemaName = sourceTableDfn.Schema; #if DEBUG_X if (tableName.EqualsCI("company")) Debugger.Break(); #endif // Columns: List sourceColumns = sourceTableDfn.Columns; List targetColumns = targetTableDfn.Columns; List sourceColumnNames = sourceColumns.GetNames(); List targetColumnNames = targetColumns.GetNames(); sourceColumnNames.Sort(); targetColumnNames.Sort(); for (int sourceColumnIdx = 0, targetColumnIdx = 0; sourceColumnIdx < sourceColumns.Count && targetColumnIdx < targetColumns.Count;) { // Retrieve the Column names: int iPosition = 0; String sourceColumnName = sourceColumnIdx < sourceColumnNames.Count() ? sourceColumnNames[sourceColumnIdx] : ""; String targetColumnName = targetColumnIdx < targetColumnNames.Count() ? targetColumnNames[targetColumnIdx] : ""; // Retrieve Column definition from each database. DbDfnColumn sourceColumn = sourceColumns.GetColumnByName(sourceColumnName); if (sourceColumn != null) iPosition = sourceColumn.Pos; DbDfnColumn targetColumn = targetColumns.GetColumnByName(targetColumnName); // Compare Columns. if (StringUtil.IsEmpty(targetColumnName) || targetColumnName.CompareTo(sourceColumnName) > 0) { // Column is only present in SOURCE list: AddSchemaComparisonItem(schemaName, tableName, sourceColumnName, iPosition, "", "Column is missing"); // Move to next Column in SOURCE list: sourceColumnIdx++; continue; } else if (StringUtil.IsEmpty(sourceColumnName) || sourceColumnName.CompareTo(targetColumnName) > 0) { // Column is only present in TARGET list: AddSchemaComparisonItem(schemaName, tableName, targetColumnName, iPosition, "", "Column is not required"); // Move to next Column in TARGET list: targetColumnIdx++; continue; } else { // Column is present in both databases. List lDifferences = new List(); // Verify column attributes: if (!config.IgnorePosition) if (sourceColumn.Pos != targetColumn.Pos) lDifferences.Add("pos"); if (!sourceColumn.Name.EqualsCI(targetColumn.Name)) lDifferences.Add("name"); if (ColumnNativeDataTypesDiffer(sourceColumn, targetColumn)) lDifferences.Add((String)"native type (" + sourceColumn.DataTypeNameNative + ":" + targetColumn.DataTypeNameNative + ")"); else if (ColumnDataTypesDiffer(sourceColumn, targetColumn)) lDifferences.Add("data type (" + sourceColumn.DataTypeName + ":" + targetColumn.DataTypeName + ")"); if (ColumnSizesDiffer(sourceColumn, targetColumn)) lDifferences.Add((String)"size (" + sourceColumn.Size + ":" + targetColumn.Size + ")"); if (ColumnDecimalsDiffer(sourceColumn, targetColumn)) lDifferences.Add("decimals (" + sourceColumn.DecimalDigits + "," + targetColumn.DecimalDigits + ")"); if (!config.IgnoreNullable) { if (sourceColumn.Nullable != targetColumn.Nullable) lDifferences.Add("nullable"); if (sourceColumn.IsNullable != targetColumn.IsNullable) lDifferences.Add("is nullable"); } // Display results: if (!config.HideIdenticalObjects || !lDifferences.IsEmpty()) AddSchemaComparisonItem(schemaName, tableName, sourceColumnName, iPosition, "", (lDifferences.IsEmpty() ? "Column OK" : "Different: " + lDifferences.Concat(", "))); // Move to next Column in BOTH lists: sourceColumnIdx++; targetColumnIdx++; } } return OK; } private static long iLargeSizeThreshold = 4000; private bool ColumnSizesDiffer(DbDfnColumn sourceColumn, DbDfnColumn targetColumn) { if (sourceColumn.Size == targetColumn.Size) return false; // If both columns are large, treat then as equal: if (sourceColumn.Size >= iLargeSizeThreshold && targetColumn.Size >= iLargeSizeThreshold) return false; // If one column is large (eg. varchar max) and the other zero (eg. Access Large Text): if (sourceColumn.Size >= iLargeSizeThreshold && targetColumn.Size == 0) return false; if (sourceColumn.Size == 0 && targetColumn.Size >= iLargeSizeThreshold) return false; // Check for specific data types: String sourceDataType = sourceColumn.DataTypeNameNative; String targetDataType = targetColumn.DataTypeNameNative; // Ignore length check for numeric types. if (config.IgnoreSimilarNumericTypes) { // Floating point types. List numericTypes = StringListUtil.Split("Real,Float,Double,Decimal,Numeric", ","); if (numericTypes.ContainsCI(sourceDataType) && numericTypes.ContainsCI(targetDataType)) return false; } // DateTimes: List dateTimeTypes = StringListUtil.Split("DateTime", ","); if (dateTimeTypes.ContainsCI(sourceDataType) && dateTimeTypes.ContainsCI(targetDataType)) return false; return true; } private bool ColumnDecimalsDiffer(DbDfnColumn sourceColumn, DbDfnColumn targetColumn) { if (sourceColumn.DecimalDigits == targetColumn.DecimalDigits) return false; // Check for specific data types: String sourceDataType = sourceColumn.DataTypeNameNative; String targetDataType = targetColumn.DataTypeNameNative; // Numeric types - float/double columns report decimals as 0. List floatTypes = StringListUtil.Split("Real,Float,Double", ","); List decimalTypes = StringListUtil.Split("Decimal,Numeric", ","); if (sourceColumn.DecimalDigits == 0 && floatTypes.ContainsCI(sourceDataType) && targetColumn.DecimalDigits > 0 && decimalTypes.ContainsCI(targetDataType)) return false; if (sourceColumn.DecimalDigits > 0 && decimalTypes.ContainsCI(sourceDataType) && targetColumn.DecimalDigits == 0 && floatTypes.ContainsCI(targetDataType)) return false; // DateTimes: List dateTimeTypes = StringListUtil.Split("DateTime", ","); if (dateTimeTypes.ContainsCI(sourceDataType) && dateTimeTypes.ContainsCI(targetDataType)) return false; return true; } private bool ColumnDataTypesDiffer(DbDfnColumn sourceColumn, DbDfnColumn targetColumn) { if (sourceColumn.DataType == targetColumn.DataType) return false; // Check for specific data type matches: String sourceDataType = sourceColumn.DataTypeName; String targetDataType = targetColumn.DataTypeName; if (config.IgnoreSimilarCharTypes) { } if (config.IgnoreSimilarNumericTypes) { // Similar numeric type. List numericTypes = StringListUtil.Split("Real,Float,Double,Decimal,Numeric", ","); if (numericTypes.ContainsCI(sourceDataType) && numericTypes.ContainsCI(targetDataType)) return false; } return true; } private bool ColumnNativeDataTypesDiffer(DbDfnColumn sourceColumn, DbDfnColumn targetColumn) { if (sourceColumn.DataTypeNative == targetColumn.DataTypeNative) return false; // Check for specific data type matches: String sourceDataType = sourceColumn.DataTypeNameNative; String targetDataType = targetColumn.DataTypeNameNative; if (config.IgnoreSimilarCharTypes) { // Similar Char types. List varCharTypes = StringListUtil.Split("VarChar,LongVarChar,LongChar,NVarChar,VarWChar", ","); if (varCharTypes.ContainsCI(sourceDataType) && varCharTypes.ContainsCI(targetDataType)) return false; } if (config.IgnoreSimilarNumericTypes) { // Similar numeric types. List numericTypes = StringListUtil.Split("Real,Float,Double,Numeric,Decimal", ","); if (numericTypes.ContainsCI(sourceDataType) && numericTypes.ContainsCI(targetDataType)) return false; } return true; } //------------------------------------------------------------------- private bool CompareTableIndices(DbDfnTable sourceTableDfn, DbDfnTable targetTableDfn) { String schemaName = sourceTableDfn.Schema; String tableName = sourceTableDfn.Name; // Indices: List sourceIndices = sourceTableDfn.Indices; List targetIndices = targetTableDfn.Indices; List sourceIndexNames = sourceIndices.GetNames().OrderBy(item => item).ToList(); List targetIndexNames = targetIndices.GetNames().OrderBy(item => item).ToList(); for (int sourceIndexIdx = 0, targetIndexIdx = 0; OK && sourceIndexIdx < sourceIndexNames.Count && targetIndexIdx < targetIndexNames.Count;) { // Retrieve the Index names. String sourceIndexName = sourceIndexIdx < sourceIndexNames.Count ? sourceIndexNames[sourceIndexIdx] : ""; String targetIndexName = targetIndexIdx < targetIndexNames.Count ? targetIndexNames[targetIndexIdx] : ""; // Compare Indices. if (StringUtil.IsEmpty(targetIndexName) || targetIndexName.CompareTo(sourceIndexName) > 0) { // Index is only present in SOURCE list. AddSchemaComparisonItem(schemaName, tableName, "", 0, sourceIndexName, "Index is missing"); // Move to next Index in SOURCE list. sourceIndexIdx++; } else if (StringUtil.IsEmpty(sourceIndexName) || sourceIndexName.CompareTo(targetIndexName) > 0) { // Index is only present in TARGET list. AddSchemaComparisonItem(schemaName, tableName, "", 0, targetIndexName, "Index is not required"); // Move to next Index in TARGET list. targetIndexIdx++; } else { // Index is present in both databases. // Retrieve Index definition from each database. DbDfnIndex sourceIndex = sourceIndices.GetIndexByName(sourceIndexName); DbDfnIndex targetIndex = targetIndices.GetIndexByName(targetIndexName); List indexDifferences = new List(); // Verify Index attributes. if (!sourceIndex.Name.EqualsCI(targetIndex.Name)) indexDifferences.Add("name"); if (sourceIndex.Primary != targetIndex.Primary) indexDifferences.Add("primary"); if (sourceIndex.Unique != targetIndex.Unique) indexDifferences.Add("unique"); if (sourceIndex.Nullable != targetIndex.Nullable) indexDifferences.Add("nullable"); // Display results. if (!config.HideIdenticalObjects || indexDifferences.Count > 0) AddSchemaComparisonItem(schemaName, tableName, "", 0, sourceIndexName, (indexDifferences.IsEmpty() ? "Index OK" : "Different: " + indexDifferences.Concat(","))); // Compare index column lists: List sourceIndexColumns = sourceIndex.Columns; List targetIndexColumns = targetIndex.Columns; for (int sourceColumnIdx = 0; sourceColumnIdx < sourceIndexColumns.Count; sourceColumnIdx++) { String columnName = sourceIndexColumns[sourceColumnIdx].Name; int columnPos = sourceColumnIdx + 1; if (!targetIndexColumns.Exists(item => item.Name.EqualsCI(columnName))) AddSchemaComparisonItem(schemaName, tableName, columnName, columnPos, sourceIndexName, "Index column is missing"); else if (sourceColumnIdx != targetIndexColumns.GetPosForName(columnName) - 1) AddSchemaComparisonItem(schemaName, tableName, columnName, columnPos, sourceIndexName, "Index column in different position"); } for (int targetColumnIdx = 0; targetColumnIdx < targetIndexColumns.Count; targetColumnIdx++) { String columnName = targetIndexColumns[targetColumnIdx].Name; int columnPos = targetColumnIdx + 1; if (!sourceIndexColumns.Exists(item => item.Name.EqualsCI(columnName))) AddSchemaComparisonItem(schemaName, tableName, columnName, columnPos, targetIndexName, "Index column is not required"); } // Move to next Index in BOTH lists. sourceIndexIdx++; targetIndexIdx++; } } return OK; } //------------------------------------------------------------------- private void AddSchemaComparisonItem(String schemaName, String tableName, String columnName, int position, String indexName, String comment) { schemaComparisonItems.Add(new SchemaComparisonItem(schemaName, tableName, columnName, position, indexName, comment)); InvokeSchemaComparisonItemAdded(); } //------------------------------------------------------------------- } //======================================================================= public static class SchemaComparisonItemList { /// Identify the maximum Id value in the list. public static int GetMaxId(this IEnumerable values) { int maxId = 0; foreach (SaDbProcess_CompareDatabaseSchemas.SchemaComparisonItem schemaComparisonItem in values) maxId = Math.Max(maxId, schemaComparisonItem.Id); return maxId; } } //======================================================================= }