417 lines
18 KiB
C#
417 lines
18 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 : SacsProcess
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// Configuration.
|
|
public new class Config : SacsProcess.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 interface IProcessNotifier : ISacsProcessNotifier
|
|
{
|
|
void OnSchemaComparisonItemAdded(SaDbProcess_CompareDatabaseSchemas proc);
|
|
}
|
|
public class SchemaComparisonItem
|
|
{
|
|
internal SchemaComparisonItem(String tableName, String columnName, int columnPosition, String indexName, String comment)
|
|
{
|
|
Id = ++SeedId;
|
|
TableName = tableName;
|
|
ColumnName = columnName;
|
|
ColumnPosition = columnPosition;
|
|
IndexName = indexName;
|
|
Comment = comment;
|
|
}
|
|
public static int SeedId { get; private set; }
|
|
public int Id { 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 IProcessNotifier processNotifier = null;
|
|
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";
|
|
public new IProcessNotifier ProcessNotifier
|
|
{
|
|
get { return processNotifier; }
|
|
set
|
|
{
|
|
processNotifier = value;
|
|
base.ProcessNotifier = value;
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// 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 SetProcessError("Source database must be supplied.");
|
|
|
|
CompareTables();
|
|
return OK;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
private void CompareTables()
|
|
{
|
|
// Database schemas.
|
|
List<String> sourceTables = (config.SourceDatabase?.GetTableNames(true) ?? config.SourceTables.Select(item => item.Name)).OrderBy(item => item).ToList();
|
|
List<String> targetTables = config.TargetDatabase.GetTableNames(true).OrderBy(item => item).ToList();
|
|
|
|
for (int sourceTableIdx = 0, targetTableIdx = 0; sourceTableIdx < sourceTables.Count || targetTableIdx < targetTables.Count;)
|
|
{
|
|
String sourceTableName = (sourceTableIdx < sourceTables.Count ? sourceTables[sourceTableIdx] : "");
|
|
String targetTableName = (targetTableIdx < targetTables.Count ? targetTables[targetTableIdx] : "");
|
|
if (StringUtil.IsEmpty(targetTableName) || targetTableName.CompareTo(sourceTableName) > 0)
|
|
{
|
|
// Target table missing.
|
|
AddSchemaComparisonItem(sourceTableName, "", 0, "", "Table is missing");
|
|
sourceTableIdx++;
|
|
continue;
|
|
}
|
|
if (StringUtil.IsEmpty(sourceTableName) || sourceTableName.CompareTo(targetTableName) > 0)
|
|
{
|
|
// Source table missing.
|
|
AddSchemaComparisonItem(targetTableName, "", 0, "", "Table is not required");
|
|
targetTableIdx++;
|
|
continue;
|
|
}
|
|
SetProgressInfo("Processing table: " + sourceTableName);
|
|
// Table is present in both databases.
|
|
if (!config.HideIdenticalObjects) AddSchemaComparisonItem(sourceTableName, "", 0, "", "Table OK");
|
|
#if DEBUG_X
|
|
if (sourceTableName.EqualsCI("address"))
|
|
Debugger.Break();
|
|
#endif
|
|
// Retrieve table definition from each database.
|
|
DbDfnTable sourceTableDfn = config.SourceDatabase?.GetTableDfn(sourceTableName, true) ?? config.SourceTables.GetTableByName(sourceTableName);
|
|
DbDfnTable targetTableDfn = config.TargetDatabase.GetTableDfn(targetTableName, true);
|
|
// 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;
|
|
#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(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(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(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 sTable = 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(sTable, "", 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(sTable, "", 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(sTable, "", 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(sTable, columnName, columnPos, sourceIndexName, "Index column is missing");
|
|
else if (sourceColumnIdx != targetIndexColumns.GetPosForName(columnName) - 1)
|
|
AddSchemaComparisonItem(sTable, 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(sTable, columnName, columnPos, targetIndexName, "Index column is not required");
|
|
}
|
|
|
|
// Move to next Index in BOTH lists.
|
|
sourceIndexIdx++;
|
|
targetIndexIdx++;
|
|
}
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
private void AddSchemaComparisonItem(String tableName, String columnName, int position, String indexName, String comment)
|
|
{
|
|
schemaComparisonItems.Add(new SchemaComparisonItem(tableName, columnName, position, indexName, comment));
|
|
if (ProcessNotifier != null) ProcessNotifier.OnSchemaComparisonItemAdded(this);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
|
|
//=======================================================================
|
|
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;
|
|
}
|
|
}
|
|
|
|
//=======================================================================
|
|
}
|