You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

734 lines
30 KiB

using OpenDBDiff.Abstractions.Schema;
using OpenDBDiff.Abstractions.Schema.Attributes;
using OpenDBDiff.Abstractions.Schema.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenDBDiff.SqlServer.Schema.Model
{
public class Table : SQLServerSchemaBase, IComparable<Table>, ITable<Table>
{
private int dependenciesCount;
private List<ISchemaBase> dependencies;
private Boolean? hasFileStream;
public Table(ISchemaBase parent)
: base(parent, ObjectType.Table)
{
dependenciesCount = -1;
Columns = new Columns<Table>(this);
Constraints = new SchemaList<Constraint, Table>(this, ((Database)parent).AllObjects);
Options = new SchemaList<TableOption, Table>(this);
Triggers = new SchemaList<Trigger, Table>(this, ((Database)parent).AllObjects);
CLRTriggers = new SchemaList<CLRTrigger, Table>(this, ((Database)parent).AllObjects);
Indexes = new SchemaList<Index, Table>(this, ((Database)parent).AllObjects);
Partitions = new SchemaList<TablePartition, Table>(this, ((Database)parent).AllObjects);
FullTextIndex = new SchemaList<FullTextIndex, Table>(this);
}
public string CompressType { get; set; }
public string FileGroupText { get; set; }
public Boolean HasChangeDataCapture { get; set; }
public Boolean HasChangeTrackingTrackColumn { get; set; }
public Boolean HasChangeTracking { get; set; }
public string FileGroupStream { get; set; }
public Boolean HasClusteredIndex { get; set; }
public string FileGroup { get; set; }
public Table OriginalTable { get; set; }
[SchemaNode("Constraints")]
public SchemaList<Constraint, Table> Constraints { get; private set; }
[SchemaNode("Indexes", "Index")]
public SchemaList<Index, Table> Indexes { get; private set; }
[SchemaNode("CLR Triggers")]
public SchemaList<CLRTrigger, Table> CLRTriggers { get; private set; }
[SchemaNode("Triggers")]
public SchemaList<Trigger, Table> Triggers { get; private set; }
public SchemaList<FullTextIndex, Table> FullTextIndex { get; private set; }
public SchemaList<TablePartition, Table> Partitions { get; set; }
public SchemaList<TableOption, Table> Options { get; set; }
/// <summary>
/// Indica si la tabla tiene alguna columna que sea Identity.
/// </summary>
public Boolean HasIdentityColumn
{
get
{
foreach (Column col in Columns)
{
if (col.IsIdentity) return true;
}
return false;
}
}
public Boolean HasFileStream
{
get
{
if (hasFileStream == null)
{
hasFileStream = false;
foreach (Column col in Columns)
{
if (col.IsFileStream) hasFileStream = true;
}
}
return hasFileStream.Value;
}
}
public Boolean HasBlobColumn
{
get
{
foreach (Column col in Columns)
{
if (col.IsBLOB) return true;
}
return false;
}
}
/// <summary>
/// Indica la cantidad de Constraints dependientes de otra tabla (FK) que tiene
/// la tabla.
/// </summary>
public override int DependenciesCount
{
get
{
if (dependenciesCount == -1)
dependenciesCount = ((Database)Parent).Dependencies.DependenciesCount(Id,
ObjectType.Constraint);
return dependenciesCount;
}
}
#region IComparable<Table> Members
/// <summary>
/// Compara en primer orden por la operacion
/// (Primero van los Drops, luego los Create y finalesmente los Alter).
/// Si la operacion es la misma, ordena por cantidad de tablas dependientes.
/// </summary>
public int CompareTo(Table other)
{
if (other == null) throw new ArgumentNullException("other");
if (Status == other.Status)
return DependenciesCount.CompareTo(other.DependenciesCount);
return other.Status.CompareTo(Status);
}
#endregion IComparable<Table> Members
#region ITable<Table> Members
/// <summary>
/// Coleccion de campos de la tabla.
/// </summary>
[SchemaNode("Columns", "Column")]
public Columns<Table> Columns { get; set; }
#endregion ITable<Table> Members
/// <summary>
/// Clona el objeto Table en una nueva instancia.
/// </summary>
public override ISchemaBase Clone(ISchemaBase objectParent)
{
var table = new Table(objectParent)
{
Owner = Owner,
Name = Name,
Id = Id,
Guid = Guid,
Status = Status,
FileGroup = FileGroup,
FileGroupText = FileGroupText,
FileGroupStream = FileGroupStream,
HasClusteredIndex = HasClusteredIndex,
HasChangeTracking = HasChangeTracking,
HasChangeTrackingTrackColumn = HasChangeTrackingTrackColumn,
HasChangeDataCapture = HasChangeDataCapture,
dependenciesCount = DependenciesCount
};
table.Columns = Columns.Clone(table);
table.Options = Options.Clone(table);
table.CompressType = CompressType;
table.Triggers = Triggers.Clone(table);
table.Indexes = Indexes.Clone(table);
table.Partitions = Partitions.Clone(table);
table.Constraints = Constraints.Clone(table);
return table;
}
public override string ToSql()
{
return ToSql(true);
}
/// <summary>
/// Devuelve el schema de la tabla en formato SQL.
/// </summary>
public string ToSql(Boolean showFK)
{
Database database = null;
ISchemaBase current = this;
while (database == null && current.Parent != null)
{
database = current.Parent as Database;
current = current.Parent;
}
if (database == null)
return string.Empty;
var isAzure10 = database.Info.Version == DatabaseInfo.SQLServerVersion.SQLServerAzure10;
var sql = new StringBuilder();
string sqlPK = "";
string sqlUC = "";
string sqlFK = "";
if (Columns.Any())
{
sql.AppendLine("CREATE TABLE " + FullName + "\r\n(");
sql.Append(Columns.ToSql());
if (Constraints.Any())
{
sql.AppendLine(",");
Constraints.AsQueryable()
// Add the constraint if it's not in DropStatus
.Where(c => !c.HasState(ObjectStatus.Drop))
.ToList()
.ForEach(item =>
{
if (item.Type == Constraint.ConstraintType.PrimaryKey)
sqlPK += "\t" + item.ToSql() + ",\r\n";
if (item.Type == Constraint.ConstraintType.Unique)
sqlUC += "\t" + item.ToSql() + ",\r\n";
if (showFK && item.Type == Constraint.ConstraintType.ForeignKey)
sqlFK += "\t" + item.ToSql() + ",\r\n";
});
sql.Append(sqlPK + sqlUC + sqlFK);
sql = new StringBuilder(sql.ToString(0, sql.Length - 3)).AppendLine();
}
else
{
sql.AppendLine();
if (!String.IsNullOrEmpty(CompressType))
sql.AppendLine("WITH (DATA_COMPRESSION = " + CompressType + ")");
}
sql.Append(")");
if (!isAzure10)
{
if (!String.IsNullOrEmpty(FileGroup)) sql.Append(" ON [" + FileGroup + "]");
if (!String.IsNullOrEmpty(FileGroupText))
{
if (HasBlobColumn)
sql.Append(" TEXTIMAGE_ON [" + FileGroupText + "]");
}
if ((!String.IsNullOrEmpty(FileGroupStream)) && (HasFileStream))
sql.Append(" FILESTREAM_ON [" + FileGroupStream + "]");
}
sql.AppendLine();
sql.AppendLine("GO");
Constraints.ForEach(item =>
{
if (item.Type == Constraint.ConstraintType.Check)
sql.AppendLine(item.ToSqlAdd());
});
if (HasChangeTracking)
sql.Append(ToSqlChangeTracking());
sql.Append(Indexes.ToSql());
sql.Append(FullTextIndex.ToSql());
sql.Append(Options.ToSql());
sql.Append(Triggers.ToSql());
}
return sql.ToString();
}
private string ToSqlChangeTracking()
{
var sql = new StringBuilder();
if (HasChangeTracking)
{
sql.Append("ALTER TABLE " + FullName + " ENABLE CHANGE_TRACKING");
if (HasChangeTrackingTrackColumn)
sql.Append(" WITH(TRACK_COLUMNS_UPDATED = ON)");
}
else
sql.Append("ALTER TABLE " + FullName + " DISABLE CHANGE_TRACKING");
return sql.Append("\r\nGO\r\n").ToString();
}
public override string ToSqlAdd()
{
return ToSql();
}
public override string ToSqlDrop()
{
return "DROP TABLE " + FullName + "\r\nGO\r\n";
}
/*
private SQLScriptList BuildSQLFileGroup()
{
var listDiff = new SQLScriptList();
Boolean found = false;
Index clustered = Indexes.Find(item => item.Type == Index.IndexTypeEnum.Clustered);
if (clustered == null)
{
foreach (Constraint cons in Constraints)
{
if (cons.Index.Type == Index.IndexTypeEnum.Clustered)
{
listDiff.Add(cons.ToSqlDrop(FileGroup), dependenciesCount, ScripActionType.DropConstraint);
listDiff.Add(cons.ToSqlAdd(), dependenciesCount, ScripActionType.AddConstraint);
found = true;
}
}
if (!found)
{
Status = ObjectStatusType.RebuildStatus;
listDiff = ToSqlDiff();
}
}
else
{
listDiff.Add(clustered.ToSqlDrop(FileGroup), dependenciesCount, ScripActionType.DropIndex);
listDiff.Add(clustered.ToSqlAdd(), dependenciesCount, ScripActionType.AddIndex);
}
return listDiff;
}
*/
/// <summary>
/// Devuelve el schema de diferencias de la tabla en formato SQL.
/// </summary>
public override SQLScriptList ToSqlDiff(ICollection<ISchemaBase> schemas)
{
var listDiff = new SQLScriptList();
if (Status != ObjectStatus.Original)
{
if (((Database)Parent).Options.Ignore.FilterTable)
RootParent.ActionMessage.Add(this);
}
if (Status == ObjectStatus.Drop)
{
if (((Database)Parent).Options.Ignore.FilterTable)
{
listDiff.Add(ToSqlDrop(), dependenciesCount, ScriptAction.DropTable);
listDiff.AddRange(ToSQLDropFKBelow());
}
}
if (Status == ObjectStatus.Create)
{
var sql = new StringBuilder();
Constraints.ForEach(item =>
{
if (item.Type == Constraint.ConstraintType.ForeignKey)
sql.AppendLine(item.ToSqlAdd());
});
listDiff.Add(ToSql(false), dependenciesCount, ScriptAction.AddTable);
listDiff.Add(sql.ToString(), dependenciesCount, ScriptAction.AddConstraintFK);
}
if (HasState(ObjectStatus.RebuildDependencies))
{
GenerateDependencies();
listDiff.AddRange(ToSQLDropDependencies());
listDiff.AddRange(Columns.ToSqlDiff(schemas));
listDiff.AddRange(ToSQLCreateDependencies());
listDiff.AddRange(Constraints.ToSqlDiff());
listDiff.AddRange(Indexes.ToSqlDiff());
listDiff.AddRange(Options.ToSqlDiff());
listDiff.AddRange(Triggers.ToSqlDiff());
listDiff.AddRange(CLRTriggers.ToSqlDiff());
listDiff.AddRange(FullTextIndex.ToSqlDiff());
}
if (HasState(ObjectStatus.Alter))
{
listDiff.AddRange(Columns.ToSqlDiff(schemas));
listDiff.AddRange(Constraints.ToSqlDiff());
listDiff.AddRange(Indexes.ToSqlDiff());
listDiff.AddRange(Options.ToSqlDiff());
listDiff.AddRange(Triggers.ToSqlDiff());
listDiff.AddRange(CLRTriggers.ToSqlDiff());
listDiff.AddRange(FullTextIndex.ToSqlDiff());
}
if (HasState(ObjectStatus.Rebuild))
{
GenerateDependencies();
listDiff.AddRange(ToSQLRebuild());
listDiff.AddRange(Columns.ToSqlDiff());
listDiff.AddRange(Constraints.ToSqlDiff());
listDiff.AddRange(Indexes.ToSqlDiff());
listDiff.AddRange(Options.ToSqlDiff());
//Como recrea la tabla, solo pone los nuevos triggers, por eso va ToSQL y no ToSQLDiff
listDiff.Add(Triggers.ToSql(), dependenciesCount, ScriptAction.AddTrigger);
listDiff.Add(CLRTriggers.ToSql(), dependenciesCount, ScriptAction.AddTrigger);
listDiff.AddRange(FullTextIndex.ToSqlDiff());
}
if (HasState(ObjectStatus.Disabled))
{
listDiff.Add(ToSqlChangeTracking(), 0, ScriptAction.AlterTableChangeTracking);
}
return listDiff;
}
private string ToSQLTableRebuild()
{
var sql = new StringBuilder();
string tempTable = "Temp" + Name;
Boolean IsIdentityNew = false;
var columnNamesStringBuilder = new StringBuilder();
var valuesStringBuilder = new StringBuilder();
foreach (Column column in Columns)
{
if (column.Status != ObjectStatus.Drop &&
!(column.Status == ObjectStatus.Create && column.IsNullable) &&
!column.IsComputed && !column.Type.ToLower().Equals("timestamp"))
{
/*Si la nueva columna a agregar es XML, no se inserta ese campo y debe ir a la coleccion de Warnings*/
/*Si la nueva columna a agregar es Identity, tampoco se debe insertar explicitamente*/
if (
!(column.Status == ObjectStatus.Create &&
(column.Type.ToLower().Equals("xml") || column.IsIdentity)))
{
columnNamesStringBuilder.Append("[");
columnNamesStringBuilder.Append(column.Name);
columnNamesStringBuilder.Append("],");
if (column.HasToForceValue)
{
if (column.HasState(ObjectStatus.Update))
{
valuesStringBuilder.Append("ISNULL([");
valuesStringBuilder.Append(column.Name);
valuesStringBuilder.Append("],");
valuesStringBuilder.Append(column.DefaultForceValue);
valuesStringBuilder.Append("),");
}
else
{
valuesStringBuilder.Append(column.DefaultForceValue);
valuesStringBuilder.Append(",");
}
}
else
{
valuesStringBuilder.Append("[");
valuesStringBuilder.Append(column.Name);
valuesStringBuilder.Append("],");
}
}
else
{
if (column.IsIdentity) IsIdentityNew = true;
}
}
}
if (columnNamesStringBuilder.Length > 0)
{
var listColumns = columnNamesStringBuilder.ToString(0, columnNamesStringBuilder.Length - 1);
var listValues = valuesStringBuilder.ToString(0, valuesStringBuilder.Length - 1);
sql.AppendLine(ToSQLTemp(tempTable));
if ((HasIdentityColumn) && (!IsIdentityNew))
sql.AppendLine("SET IDENTITY_INSERT [" + Owner + "].[" + tempTable + "] ON");
sql.AppendLine("INSERT INTO [" + Owner + "].[" + tempTable + "] (" + listColumns + ")" + " SELECT " +
listValues + " FROM " + FullName );
if ((HasIdentityColumn) && (!IsIdentityNew))
sql.AppendLine("SET IDENTITY_INSERT [" + Owner + "].[" + tempTable + "] OFF\r\nGO\r\n");
sql.AppendLine("DROP TABLE " + FullName + "\r\nGO");
if (HasFileStream)
{
Constraints.ForEach(item =>
{
if (item.Type == Constraint.ConstraintType.Unique &&
item.Status != ObjectStatus.Drop)
{
sql.AppendLine("EXEC sp_rename N'[" + Owner + "].[Temp_XX_" + item.Name +
"]',N'" + item.Name + "', 'OBJECT'\r\nGO");
}
});
}
sql.AppendLine("EXEC sp_rename N'[" + Owner + "].[" + tempTable + "]',N'" + Name +
"', 'OBJECT'\r\nGO\r\n");
sql.Append(OriginalTable.Options.ToSql());
}
else
sql = new StringBuilder();
return sql.ToString();
}
private SQLScriptList ToSQLRebuild()
{
var listDiff = new SQLScriptList();
listDiff.AddRange(ToSQLDropDependencies());
listDiff.Add(ToSQLTableRebuild(), dependenciesCount, ScriptAction.RebuildTable);
listDiff.AddRange(ToSQLCreateDependencies());
return listDiff;
}
private string ToSQLTemp(String TableName)
{
var sql = new StringBuilder();
// Drop constraints first, to avoid duplicate constraints created in temp table
foreach (var column in Columns.Where(c => !string.IsNullOrWhiteSpace(c.DefaultConstraint?.Name)))
{
sql.Append($"ALTER TABLE {this.FullName} DROP CONSTRAINT [{column.DefaultConstraint.Name}]\r\n");
}
if (!string.IsNullOrWhiteSpace(sql.ToString()))
sql.AppendLine();
sql.AppendLine("CREATE TABLE [" + Owner + "].[" + TableName + "]\r\n(");
Columns.Sort();
for (int index = 0; index < Columns.Count; index++)
{
if (Columns[index].Status != ObjectStatus.Drop)
{
sql.Append("\t" + Columns[index].ToSql(true));
if (index != Columns.Count - 1)
sql.Append(",");
sql.AppendLine();
}
}
if (HasFileStream)
{
sql = new StringBuilder(sql.ToString(0, sql.Length - 2));
sql.AppendLine(",");
Constraints.ForEach(item =>
{
if (item.Type == Constraint.ConstraintType.Unique &&
item.Status != ObjectStatus.Drop)
{
item.Name = "Temp_XX_" + item.Name;
sql.AppendLine("\t" + item.ToSql() + ",");
item.SetWasInsertInDiffList(ScriptAction.AddConstraint);
item.Name = item.Name.Substring(8, item.Name.Length - 8);
}
});
sql = new StringBuilder(sql.ToString(0, sql.Length - 3)).AppendLine();
}
else
{
sql.AppendLine();
if (!String.IsNullOrEmpty(CompressType))
sql.AppendLine("WITH (DATA_COMPRESSION = " + CompressType + ")");
}
sql.Append(")");
if (!String.IsNullOrEmpty(FileGroup)) sql.Append(" ON [" + FileGroup + "]");
if (!String.IsNullOrEmpty(FileGroupText) && HasBlobColumn)
sql.Append(" TEXTIMAGE_ON [" + FileGroupText + "]");
if (!String.IsNullOrEmpty(FileGroupStream) && HasFileStream)
sql.Append(" FILESTREAM_ON [" + FileGroupStream + "]");
sql.AppendLine();
sql.AppendLine("GO");
return sql.ToString();
}
private void GenerateDependencies()
{
List<ISchemaBase> myDependencies;
/*Si el estado es AlterRebuildDependeciesStatus, busca las dependencias solamente en las columnas que fueron modificadas*/
if (Status == ObjectStatus.RebuildDependencies)
{
myDependencies = new List<ISchemaBase>();
for (int ic = 0; ic < Columns.Count; ic++)
{
if ((Columns[ic].Status == ObjectStatus.RebuildDependencies) ||
(Columns[ic].Status == ObjectStatus.Alter))
myDependencies.AddRange(((Database)Parent).Dependencies.Find(Id, 0, Columns[ic].DataUserTypeId));
}
/*Si no encuentra ninguna, toma todas las de la tabla*/
if (myDependencies.Count == 0)
myDependencies.AddRange(((Database)Parent).Dependencies.Find(Id));
}
else
myDependencies = ((Database)Parent).Dependencies.Find(Id);
dependencies = new List<ISchemaBase>();
for (int j = 0; j < myDependencies.Count; j++)
{
ISchemaBase item = null;
if (myDependencies[j].ObjectType == ObjectType.Index)
item = Indexes[myDependencies[j].FullName];
if (myDependencies[j].ObjectType == ObjectType.Constraint)
item =
((Database)Parent).Tables[myDependencies[j].Parent.FullName].Constraints[
myDependencies[j].FullName];
if (myDependencies[j].ObjectType == ObjectType.Default)
item = Columns[myDependencies[j].FullName].DefaultConstraint;
if (myDependencies[j].ObjectType == ObjectType.View)
item = ((Database)Parent).Views[myDependencies[j].FullName];
if (myDependencies[j].ObjectType == ObjectType.Function)
item = ((Database)Parent).Functions[myDependencies[j].FullName];
if (item != null)
dependencies.Add(item);
}
}
/// <summary>
/// Genera una lista de FK que deben ser eliminadas previamente a la eliminacion de la tablas.
/// Esto pasa porque para poder eliminar una tabla, hay que eliminar antes todas las constraints asociadas.
/// </summary>
private SQLScriptList ToSQLDropFKBelow()
{
var listDiff = new SQLScriptList();
Constraints.ForEach(constraint =>
{
if ((constraint.Type == Constraint.ConstraintType.ForeignKey) &&
(((Table)constraint.Parent).DependenciesCount <= DependenciesCount))
{
/*Si la FK pertenece a la misma tabla, no se debe explicitar el DROP CONSTRAINT antes de hacer el DROP TABLE*/
if (constraint.Parent.Id != constraint.RelationalTableId)
{
listDiff.Add(constraint.Drop());
}
}
});
return listDiff;
}
/// <summary>
/// Genera una lista de script de DROPS de todas los constraints dependientes de la tabla.
/// Se usa cuando se quiere reconstruir una tabla y todos sus objectos dependientes.
/// </summary>
private SQLScriptList ToSQLDropDependencies()
{
bool addDependency = true;
var listDiff = new SQLScriptList();
//Se buscan todas las table constraints.
for (int index = 0; index < dependencies.Count; index++)
{
if ((dependencies[index].Status == ObjectStatus.Original) ||
(dependencies[index].Status == ObjectStatus.Drop))
{
addDependency = true;
if (dependencies[index].ObjectType == ObjectType.Constraint)
{
if ((((Constraint)dependencies[index]).Type == Constraint.ConstraintType.Unique) &&
((HasFileStream) || (OriginalTable.HasFileStream)))
addDependency = false;
if ((((Constraint)dependencies[index]).Type != Constraint.ConstraintType.ForeignKey) &&
(dependencies[index].Status == ObjectStatus.Drop))
addDependency = false;
}
if (addDependency)
listDiff.Add(dependencies[index].Drop());
}
}
//Se buscan todas las columns constraints.
Columns.ForEach(column =>
{
if (column.DefaultConstraint != null)
{
if (((column.DefaultConstraint.Status == ObjectStatus.Original) ||
(column.DefaultConstraint.Status == ObjectStatus.Drop) ||
(column.DefaultConstraint.Status == ObjectStatus.Alter)) &&
(column.Status != ObjectStatus.Create))
listDiff.Add(column.DefaultConstraint.Drop());
}
});
return listDiff;
}
private SQLScriptList ToSQLCreateDependencies()
{
bool addDependency = true;
var listDiff = new SQLScriptList();
//Las constraints de deben recorrer en el orden inverso.
for (int index = dependencies.Count - 1; index >= 0; index--)
{
if ((dependencies[index].Status == ObjectStatus.Original) &&
(dependencies[index].Parent.Status != ObjectStatus.Drop))
{
addDependency = true;
if (dependencies[index].ObjectType == ObjectType.Constraint)
{
if ((((Constraint)dependencies[index]).Type == Constraint.ConstraintType.Unique) &&
(HasFileStream))
addDependency = false;
}
if (addDependency)
listDiff.Add(dependencies[index].Create());
}
}
//Se buscan todas las columns constraints.
for (int index = Columns.Count - 1; index >= 0; index--)
{
if (Columns[index].DefaultConstraint != null)
{
if ((Columns[index].DefaultConstraint.CanCreate) &&
(Columns.Parent.Status != ObjectStatus.Rebuild))
listDiff.Add(Columns[index].DefaultConstraint.Create());
}
}
return listDiff;
}
/// <summary>
/// Compara dos tablas y devuelve true si son iguales, caso contrario, devuelve false.
/// </summary>
public static Boolean CompareFileGroup(Table origin, Table destination)
{
if (destination == null) throw new ArgumentNullException("destination");
if (origin == null) throw new ArgumentNullException("origin");
if ((!String.IsNullOrEmpty(destination.FileGroup) && (!String.IsNullOrEmpty(origin.FileGroup))))
if (!destination.FileGroup.Equals(origin.FileGroup))
return false;
return true;
}
/// <summary>
/// Compara dos tablas y devuelve true si son iguales, caso contrario, devuelve false.
/// </summary>
public static Boolean CompareFileGroupText(Table origin, Table destination)
{
if (destination == null) throw new ArgumentNullException("destination");
if (origin == null) throw new ArgumentNullException("origin");
if ((!String.IsNullOrEmpty(destination.FileGroupText) && (!String.IsNullOrEmpty(origin.FileGroupText))))
if (!destination.FileGroupText.Equals(origin.FileGroupText))
return false;
return true;
}
}
}