Files
Common-VS2019/SacsStandardDbas/Dbas/Proc/SaDbProcess_CompareDatabaseSchemas.cs
T
2025-02-11 21:03:58 +00:00

428 lines
19 KiB
C#

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<DbDfnTable> sourceTables, ISacsDatabase targetDatabase)
{
SourceTables = sourceTables;
TargetDatabase = targetDatabase;
}
public ISacsDatabase SourceDatabase { get; }
public List<DbDfnTable> 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<SchemaComparisonItem> schemaComparisonItems = new List<SchemaComparisonItem>();
//-------------------------------------------------------------------
// 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<SchemaComparisonItemAddedEventArgs> 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<SchemaComparisonItem> GetSchemaComparisonItems(int minId = 0)
{
return new List<SchemaComparisonItem>(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.
List<DbDfnTable> sourceTableDfns = config.SourceDatabase?.GetTableDfns(true) ?? config.SourceTables;
List<DbDfnTable> targetTableDfns = config.TargetDatabase.GetTableDfns(true) ?? new List<DbDfnTable>();
HashSet<String> sourceTables = sourceTableDfns.Select(item => item.Name).OrderBy(item => item).ToHashSet();
HashSet<String> targetTables = targetTableDfns.Select(item => item.Name).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 = sourceTableDfns.GetTableByName(sourceTableName);
DbDfnTable targetTableDfn = targetTableDfns.GetTableByName(sourceTableName);
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<DbDfnColumn> sourceColumns = sourceTableDfn.Columns;
List<DbDfnColumn> targetColumns = targetTableDfn.Columns;
List<String> sourceColumnNames = sourceColumns.GetNames();
List<String> 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<String> lDifferences = new List<string>();
// 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<String> numericTypes = StringListUtil.Split("Real,Float,Double,Decimal,Numeric", ",");
if (numericTypes.ContainsCI(sourceDataType) && numericTypes.ContainsCI(targetDataType)) return false;
}
// DateTimes:
List<String> 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<String> floatTypes = StringListUtil.Split("Real,Float,Double", ",");
List<String> 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<String> 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<String> 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<String> varCharTypes = StringListUtil.Split("VarChar,LongVarChar,LongChar,NVarChar,VarWChar", ",");
if (varCharTypes.ContainsCI(sourceDataType) && varCharTypes.ContainsCI(targetDataType)) return false;
}
if (config.IgnoreSimilarNumericTypes)
{
// Similar numeric types.
List<String> 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<DbDfnIndex> sourceIndices = sourceTableDfn.Indices;
List<DbDfnIndex> targetIndices = targetTableDfn.Indices;
List<String> sourceIndexNames = sourceIndices.GetNames().OrderBy(item => item).ToList();
List<String> 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<String> indexDifferences = new List<string>();
// 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<DbDfnIndexColumn> sourceIndexColumns = sourceIndex.Columns;
List<DbDfnIndexColumn> 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
{
/// <summary>Identify the maximum Id value in the list.</summary>
public static int GetMaxId(this IEnumerable<SaDbProcess_CompareDatabaseSchemas.SchemaComparisonItem> values)
{
int maxId = 0;
foreach (SaDbProcess_CompareDatabaseSchemas.SchemaComparisonItem schemaComparisonItem in values)
maxId = Math.Max(maxId, schemaComparisonItem.Id);
return maxId;
}
}
//=======================================================================
}