diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ad50406..a956205 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2021.3.0-eap10", + "version": "2021.3.1", "commands": [ "jb" ] diff --git a/Library/Models/Database.cs b/Library/Models/Database.cs index a6cc6e1..945c558 100644 --- a/Library/Models/Database.cs +++ b/Library/Models/Database.cs @@ -76,7 +76,7 @@ where p.Script() != p2.Script() //get deleted foreign keys foreach (var fk in db.ForeignKeys - .Where(fk => FindForeignKey(fk.Name, fk.Table.Owner) == null)) + .Where(fk => FindForeignKey(fk.Name, fk.Table.Owner) == null)) diff.ForeignKeysDeleted.Add(fk); //get added and compare mutual assemblies @@ -1274,86 +1274,6 @@ from sys.databases #region Script - public string ScriptCreate() { - var text = new StringBuilder(); - - text.AppendFormat("CREATE DATABASE {0}", Name); - text.AppendLine(); - text.AppendLine("GO"); - text.AppendFormat("USE {0}", Name); - text.AppendLine(); - text.AppendLine("GO"); - text.AppendLine(); - - if (Props.Count > 0) { - text.Append(ScriptPropList(Props)); - text.AppendLine("GO"); - text.AppendLine(); - } - - foreach (var schema in Schemas) { - text.AppendLine(schema.ScriptCreate()); - text.AppendLine("GO"); - text.AppendLine(); - } - - foreach (var t in Tables.Concat(TableTypes)) { - text.AppendLine(t.ScriptCreate()); - - foreach (var constraint in t.Constraints.Where( - c => c.Type == "CHECK" && !c.ScriptInline)) - text.AppendLine(constraint.ScriptCreate()); - - var defaults = ( - from c in t.Columns.Items - where c.Default != null - select c.Default) - .ToArray(); - foreach (var def in defaults) text.AppendLine(def.ScriptCreate()); - } - - text.AppendLine(); - text.AppendLine("GO"); - - foreach (var fk in ForeignKeys.OrderBy(f => f, ForeignKeyComparer.Instance)) - text.AppendLine(fk.ScriptCreate()); - - text.AppendLine(); - text.AppendLine("GO"); - - foreach (var r in Routines) { - text.AppendLine(r.ScriptCreate()); - text.AppendLine(); - text.AppendLine("GO"); - } - - foreach (var a in Assemblies) { - text.AppendLine(a.ScriptCreate()); - text.AppendLine(); - text.AppendLine("GO"); - } - - foreach (var u in Users) { - text.AppendLine(u.ScriptCreate()); - text.AppendLine(); - text.AppendLine("GO"); - } - - foreach (var c in ViewIndexes) { - text.AppendLine(c.ScriptCreate()); - text.AppendLine(); - text.AppendLine("GO"); - } - - foreach (var s in Synonyms) { - text.AppendLine(s.ScriptCreate()); - text.AppendLine(); - text.AppendLine("GO"); - } - - return text.ToString(); - } - public void ScriptToDir(string tableHint = null, Action log = null) { if (log == null) log = (tl, s) => { }; @@ -1705,350 +1625,5 @@ from sys.databases return scripts; } - public void ExecCreate(bool dropIfExists) { - var conStr = new SqlConnectionStringBuilder(Connection); - var dbName = conStr.InitialCatalog; - conStr.InitialCatalog = "master"; - if (DBHelper.DbExists(Connection)) { - if (dropIfExists) - DBHelper.DropDb(Connection); - else - throw new ApplicationException( - $"Database {conStr.DataSource} {dbName} already exists."); - } - - DBHelper.ExecBatchSql(conStr.ToString(), ScriptCreate()); - } - #endregion } - -public class DatabaseDiff { - public List AssembliesAdded = new(); - public List AssembliesDeleted = new(); - public List AssembliesDiff = new(); - public Database Db; - public List ForeignKeysAdded = new(); - public List ForeignKeysDeleted = new(); - public List ForeignKeysDiff = new(); - public List PermissionsAdded = new(); - public List PermissionsDeleted = new(); - public List PermissionsDiff = new(); - public List PropsChanged = new(); - - public List RoutinesAdded = new(); - public List RoutinesDeleted = new(); - public List RoutinesDiff = new(); - public List SynonymsAdded = new(); - public List SynonymsDeleted = new(); - public List SynonymsDiff = new(); - public List TablesAdded = new(); - public List
TablesDeleted = new(); - public List TablesDiff = new(); - public List
TableTypesDiff = new(); - public List UsersAdded = new(); - public List UsersDeleted = new(); - public List UsersDiff = new(); - public List ViewIndexesAdded = new(); - public List ViewIndexesDeleted = new(); - public List ViewIndexesDiff = new(); - - public bool IsDiff => PropsChanged.Count > 0 - || TablesAdded.Count > 0 - || TablesDiff.Count > 0 - || TableTypesDiff.Count > 0 - || TablesDeleted.Count > 0 - || RoutinesAdded.Count > 0 - || RoutinesDiff.Count > 0 - || RoutinesDeleted.Count > 0 - || ForeignKeysAdded.Count > 0 - || ForeignKeysDiff.Count > 0 - || ForeignKeysDeleted.Count > 0 - || AssembliesAdded.Count > 0 - || AssembliesDiff.Count > 0 - || AssembliesDeleted.Count > 0 - || UsersAdded.Count > 0 - || UsersDiff.Count > 0 - || UsersDeleted.Count > 0 - || ViewIndexesAdded.Count > 0 - || ViewIndexesDiff.Count > 0 - || ViewIndexesDeleted.Count > 0 - || SynonymsAdded.Count > 0 - || SynonymsDiff.Count > 0 - || SynonymsDeleted.Count > 0 - || PermissionsAdded.Count > 0 - || PermissionsDiff.Count > 0 - || PermissionsDeleted.Count > 0; - - private static string Summarize(bool includeNames, List changes, string caption) { - if (changes.Count == 0) return string.Empty; - return changes.Count + "x " + caption + - (includeNames ? "\r\n\t" + string.Join("\r\n\t", changes.ToArray()) : string.Empty) + - "\r\n"; - } - - public string SummarizeChanges(bool includeNames) { - var sb = new StringBuilder(); - sb.Append( - Summarize( - includeNames, - AssembliesAdded.Select(o => o.Name).ToList(), - "assemblies in source but not in target")); - sb.Append( - Summarize( - includeNames, - AssembliesDeleted.Select(o => o.Name).ToList(), - "assemblies not in source but in target")); - sb.Append( - Summarize( - includeNames, - AssembliesDiff.Select(o => o.Name).ToList(), - "assemblies altered")); - sb.Append( - Summarize( - includeNames, - ForeignKeysAdded.Select(o => o.Name).ToList(), - "foreign keys in source but not in target")); - sb.Append( - Summarize( - includeNames, - ForeignKeysDeleted.Select(o => o.Name).ToList(), - "foreign keys not in source but in target")); - sb.Append( - Summarize( - includeNames, - ForeignKeysDiff.Select(o => o.Name).ToList(), - "foreign keys altered")); - sb.Append( - Summarize( - includeNames, - PropsChanged.Select(o => o.Name).ToList(), - "properties changed")); - sb.Append( - Summarize( - includeNames, - RoutinesAdded.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}") - .ToList(), - "routines in source but not in target")); - sb.Append( - Summarize( - includeNames, - RoutinesDeleted.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}") - .ToList(), - "routines not in source but in target")); - sb.Append( - Summarize( - includeNames, - RoutinesDiff.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}").ToList(), - "routines altered")); - sb.Append( - Summarize( - includeNames, - TablesAdded.Where(o => !o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), - "tables in source but not in target")); - sb.Append( - Summarize( - includeNames, - TablesDeleted.Where(o => !o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), - "tables not in source but in target")); - sb.Append( - Summarize( - includeNames, - TablesDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), - "tables altered")); - sb.Append( - Summarize( - includeNames, - TablesAdded.Where(o => o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), - "table types in source but not in target")); - sb.Append( - Summarize( - includeNames, - TablesDeleted.Where(o => o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), - "table types not in source but in target")); - sb.Append( - Summarize( - includeNames, - TableTypesDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), - "table types altered")); - sb.Append( - Summarize( - includeNames, - UsersAdded.Select(o => o.Name).ToList(), - "users in source but not in target")); - sb.Append( - Summarize( - includeNames, - UsersDeleted.Select(o => o.Name).ToList(), - "users not in source but in target")); - sb.Append( - Summarize( - includeNames, - UsersDiff.Select(o => o.Name).ToList(), - "users altered")); - sb.Append( - Summarize( - includeNames, - ViewIndexesAdded.Select(o => o.Name).ToList(), - "view indexes in source but not in target")); - sb.Append( - Summarize( - includeNames, - ViewIndexesDeleted.Select(o => o.Name).ToList(), - "view indexes not in source but in target")); - sb.Append( - Summarize( - includeNames, - ViewIndexesDiff.Select(o => o.Name).ToList(), - "view indexes altered")); - sb.Append( - Summarize( - includeNames, - SynonymsAdded.Select(o => $"{o.Owner}.{o.Name}").ToList(), - "synonyms in source but not in target")); - sb.Append( - Summarize( - includeNames, - SynonymsDeleted.Select(o => $"{o.Owner}.{o.Name}").ToList(), - "synonyms not in source but in target")); - sb.Append( - Summarize( - includeNames, - SynonymsDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), - "synonyms altered")); - sb.Append( - Summarize( - includeNames, - PermissionsAdded.Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") - .ToList(), - "permissions in source but not in target")); - sb.Append( - Summarize( - includeNames, - PermissionsDeleted - .Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") - .ToList(), - "permissions not in source but in target")); - sb.Append( - Summarize( - includeNames, - PermissionsDiff.Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") - .ToList(), - "permissions altered")); - return sb.ToString(); - } - - public string Script() { - var text = new StringBuilder(); - //alter database props - //TODO need to check dependencies for collation change - //TODO how can collation be set to null at the server level? - if (PropsChanged.Count > 0) { - text.Append(Database.ScriptPropList(PropsChanged)); - text.AppendLine("GO"); - text.AppendLine(); - } - - //delete foreign keys - if (ForeignKeysDeleted.Count + ForeignKeysDiff.Count > 0) { - foreach (var fk in ForeignKeysDeleted) text.AppendLine(fk.ScriptDrop()); - - //delete modified foreign keys - foreach (var fk in ForeignKeysDiff) text.AppendLine(fk.ScriptDrop()); - - text.AppendLine("GO"); - } - - //delete tables - if (TablesDeleted.Count + TableTypesDiff.Count > 0) { - foreach (var t in TablesDeleted.Concat(TableTypesDiff)) text.AppendLine(t.ScriptDrop()); - - text.AppendLine("GO"); - } - // TODO: table types drop will fail if anything references them... try to find a workaround? - - //modify tables - if (TablesDiff.Count > 0) { - foreach (var t in TablesDiff) text.Append(t.Script()); - - text.AppendLine("GO"); - } - - //add tables - if (TablesAdded.Count + TableTypesDiff.Count > 0) { - foreach (var t in TablesAdded.Concat(TableTypesDiff)) text.Append(t.ScriptCreate()); - - text.AppendLine("GO"); - } - - //add foreign keys - if (ForeignKeysAdded.Count + ForeignKeysDiff.Count > 0) { - foreach (var fk in ForeignKeysAdded) text.AppendLine(fk.ScriptCreate()); - - //add modified foreign keys - foreach (var fk in ForeignKeysDiff) text.AppendLine(fk.ScriptCreate()); - - text.AppendLine("GO"); - } - - //add & delete procs, functions, & triggers - foreach (var r in RoutinesAdded) { - text.AppendLine(r.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var r in RoutinesDiff) // script alter if possible, otherwise drop and (re)create - try { - text.AppendLine(r.ScriptAlter(Db)); - text.AppendLine("GO"); - } catch { - text.AppendLine(r.ScriptDrop()); - text.AppendLine("GO"); - text.AppendLine(r.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var r in RoutinesDeleted) { - text.AppendLine(r.ScriptDrop()); - text.AppendLine("GO"); - } - - //add & delete synonyms - foreach (var s in SynonymsAdded) { - text.AppendLine(s.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var s in SynonymsDiff) { - text.AppendLine(s.ScriptDrop()); - text.AppendLine("GO"); - text.AppendLine(s.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var s in SynonymsDeleted) { - text.AppendLine(s.ScriptDrop()); - text.AppendLine("GO"); - } - - //add & delete permissions - foreach (var p in PermissionsAdded) { - text.AppendLine(p.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var p in PermissionsDiff) { - text.AppendLine(p.ScriptDrop()); - text.AppendLine("GO"); - text.AppendLine(p.ScriptCreate()); - text.AppendLine("GO"); - } - - foreach (var p in PermissionsDeleted) { - text.AppendLine(p.ScriptDrop()); - text.AppendLine("GO"); - } - - return text.ToString(); - } -} diff --git a/Library/Models/DatabaseDiff.cs b/Library/Models/DatabaseDiff.cs new file mode 100644 index 0000000..44a6485 --- /dev/null +++ b/Library/Models/DatabaseDiff.cs @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SchemaZen.Library.Models; + +public class DatabaseDiff { + public List AssembliesAdded = new(); + public List AssembliesDeleted = new(); + public List AssembliesDiff = new(); + public Database Db; + public List ForeignKeysAdded = new(); + public List ForeignKeysDeleted = new(); + public List ForeignKeysDiff = new(); + public List PermissionsAdded = new(); + public List PermissionsDeleted = new(); + public List PermissionsDiff = new(); + public List PropsChanged = new(); + + public List RoutinesAdded = new(); + public List RoutinesDeleted = new(); + public List RoutinesDiff = new(); + public List SynonymsAdded = new(); + public List SynonymsDeleted = new(); + public List SynonymsDiff = new(); + public List
TablesAdded = new(); + public List
TablesDeleted = new(); + public List TablesDiff = new(); + public List
TableTypesDiff = new(); + public List UsersAdded = new(); + public List UsersDeleted = new(); + public List UsersDiff = new(); + public List ViewIndexesAdded = new(); + public List ViewIndexesDeleted = new(); + public List ViewIndexesDiff = new(); + + public bool IsDiff => PropsChanged.Count > 0 + || TablesAdded.Count > 0 + || TablesDiff.Count > 0 + || TableTypesDiff.Count > 0 + || TablesDeleted.Count > 0 + || RoutinesAdded.Count > 0 + || RoutinesDiff.Count > 0 + || RoutinesDeleted.Count > 0 + || ForeignKeysAdded.Count > 0 + || ForeignKeysDiff.Count > 0 + || ForeignKeysDeleted.Count > 0 + || AssembliesAdded.Count > 0 + || AssembliesDiff.Count > 0 + || AssembliesDeleted.Count > 0 + || UsersAdded.Count > 0 + || UsersDiff.Count > 0 + || UsersDeleted.Count > 0 + || ViewIndexesAdded.Count > 0 + || ViewIndexesDiff.Count > 0 + || ViewIndexesDeleted.Count > 0 + || SynonymsAdded.Count > 0 + || SynonymsDiff.Count > 0 + || SynonymsDeleted.Count > 0 + || PermissionsAdded.Count > 0 + || PermissionsDiff.Count > 0 + || PermissionsDeleted.Count > 0; + + private static string Summarize(bool includeNames, List changes, string caption) { + if (changes.Count == 0) return string.Empty; + return changes.Count + "x " + caption + + (includeNames ? "\r\n\t" + string.Join("\r\n\t", changes.ToArray()) : string.Empty) + + "\r\n"; + } + + public string SummarizeChanges(bool includeNames) { + var sb = new StringBuilder(); + sb.Append( + Summarize( + includeNames, + AssembliesAdded.Select(o => o.Name).ToList(), + "assemblies in source but not in target")); + sb.Append( + Summarize( + includeNames, + AssembliesDeleted.Select(o => o.Name).ToList(), + "assemblies not in source but in target")); + sb.Append( + Summarize( + includeNames, + AssembliesDiff.Select(o => o.Name).ToList(), + "assemblies altered")); + sb.Append( + Summarize( + includeNames, + ForeignKeysAdded.Select(o => o.Name).ToList(), + "foreign keys in source but not in target")); + sb.Append( + Summarize( + includeNames, + ForeignKeysDeleted.Select(o => o.Name).ToList(), + "foreign keys not in source but in target")); + sb.Append( + Summarize( + includeNames, + ForeignKeysDiff.Select(o => o.Name).ToList(), + "foreign keys altered")); + sb.Append( + Summarize( + includeNames, + PropsChanged.Select(o => o.Name).ToList(), + "properties changed")); + sb.Append( + Summarize( + includeNames, + RoutinesAdded.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}") + .ToList(), + "routines in source but not in target")); + sb.Append( + Summarize( + includeNames, + RoutinesDeleted.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}") + .ToList(), + "routines not in source but in target")); + sb.Append( + Summarize( + includeNames, + RoutinesDiff.Select(o => $"{o.RoutineType.ToString()} {o.Owner}.{o.Name}").ToList(), + "routines altered")); + sb.Append( + Summarize( + includeNames, + TablesAdded.Where(o => !o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), + "tables in source but not in target")); + sb.Append( + Summarize( + includeNames, + TablesDeleted.Where(o => !o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), + "tables not in source but in target")); + sb.Append( + Summarize( + includeNames, + TablesDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), + "tables altered")); + sb.Append( + Summarize( + includeNames, + TablesAdded.Where(o => o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), + "table types in source but not in target")); + sb.Append( + Summarize( + includeNames, + TablesDeleted.Where(o => o.IsType).Select(o => $"{o.Owner}.{o.Name}").ToList(), + "table types not in source but in target")); + sb.Append( + Summarize( + includeNames, + TableTypesDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), + "table types altered")); + sb.Append( + Summarize( + includeNames, + UsersAdded.Select(o => o.Name).ToList(), + "users in source but not in target")); + sb.Append( + Summarize( + includeNames, + UsersDeleted.Select(o => o.Name).ToList(), + "users not in source but in target")); + sb.Append( + Summarize( + includeNames, + UsersDiff.Select(o => o.Name).ToList(), + "users altered")); + sb.Append( + Summarize( + includeNames, + ViewIndexesAdded.Select(o => o.Name).ToList(), + "view indexes in source but not in target")); + sb.Append( + Summarize( + includeNames, + ViewIndexesDeleted.Select(o => o.Name).ToList(), + "view indexes not in source but in target")); + sb.Append( + Summarize( + includeNames, + ViewIndexesDiff.Select(o => o.Name).ToList(), + "view indexes altered")); + sb.Append( + Summarize( + includeNames, + SynonymsAdded.Select(o => $"{o.Owner}.{o.Name}").ToList(), + "synonyms in source but not in target")); + sb.Append( + Summarize( + includeNames, + SynonymsDeleted.Select(o => $"{o.Owner}.{o.Name}").ToList(), + "synonyms not in source but in target")); + sb.Append( + Summarize( + includeNames, + SynonymsDiff.Select(o => $"{o.Owner}.{o.Name}").ToList(), + "synonyms altered")); + sb.Append( + Summarize( + includeNames, + PermissionsAdded.Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") + .ToList(), + "permissions in source but not in target")); + sb.Append( + Summarize( + includeNames, + PermissionsDeleted + .Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") + .ToList(), + "permissions not in source but in target")); + sb.Append( + Summarize( + includeNames, + PermissionsDiff.Select(o => $"{o.ObjectName}: {o.PermissionType} TO {o.UserName}") + .ToList(), + "permissions altered")); + return sb.ToString(); + } + + public string Script() { + var text = new StringBuilder(); + //alter database props + //TODO need to check dependencies for collation change + //TODO how can collation be set to null at the server level? + if (PropsChanged.Count > 0) { + text.Append(Database.ScriptPropList(PropsChanged)); + text.AppendLine("GO"); + text.AppendLine(); + } + + //delete foreign keys + if (ForeignKeysDeleted.Count + ForeignKeysDiff.Count > 0) { + foreach (var fk in ForeignKeysDeleted) text.AppendLine(fk.ScriptDrop()); + + //delete modified foreign keys + foreach (var fk in ForeignKeysDiff) text.AppendLine(fk.ScriptDrop()); + + text.AppendLine("GO"); + } + + //delete tables + if (TablesDeleted.Count + TableTypesDiff.Count > 0) { + foreach (var t in TablesDeleted.Concat(TableTypesDiff)) text.AppendLine(t.ScriptDrop()); + + text.AppendLine("GO"); + } + // TODO: table types drop will fail if anything references them... try to find a workaround? + + //modify tables + if (TablesDiff.Count > 0) { + foreach (var t in TablesDiff) text.Append(t.Script()); + + text.AppendLine("GO"); + } + + //add tables + if (TablesAdded.Count + TableTypesDiff.Count > 0) { + foreach (var t in TablesAdded.Concat(TableTypesDiff)) text.Append(t.ScriptCreate()); + + text.AppendLine("GO"); + } + + //add foreign keys + if (ForeignKeysAdded.Count + ForeignKeysDiff.Count > 0) { + foreach (var fk in ForeignKeysAdded) text.AppendLine(fk.ScriptCreate()); + + //add modified foreign keys + foreach (var fk in ForeignKeysDiff) text.AppendLine(fk.ScriptCreate()); + + text.AppendLine("GO"); + } + + //add & delete procs, functions, & triggers + foreach (var r in RoutinesAdded) { + text.AppendLine(r.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var r in RoutinesDiff) // script alter if possible, otherwise drop and (re)create + try { + text.AppendLine(r.ScriptAlter(Db)); + text.AppendLine("GO"); + } catch { + text.AppendLine(r.ScriptDrop()); + text.AppendLine("GO"); + text.AppendLine(r.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var r in RoutinesDeleted) { + text.AppendLine(r.ScriptDrop()); + text.AppendLine("GO"); + } + + //add & delete synonyms + foreach (var s in SynonymsAdded) { + text.AppendLine(s.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var s in SynonymsDiff) { + text.AppendLine(s.ScriptDrop()); + text.AppendLine("GO"); + text.AppendLine(s.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var s in SynonymsDeleted) { + text.AppendLine(s.ScriptDrop()); + text.AppendLine("GO"); + } + + //add & delete permissions + foreach (var p in PermissionsAdded) { + text.AppendLine(p.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var p in PermissionsDiff) { + text.AppendLine(p.ScriptDrop()); + text.AppendLine("GO"); + text.AppendLine(p.ScriptCreate()); + text.AppendLine("GO"); + } + + foreach (var p in PermissionsDeleted) { + text.AppendLine(p.ScriptDrop()); + text.AppendLine("GO"); + } + + return text.ToString(); + } +} diff --git a/Library/Models/Schema.cs b/Library/Models/Schema.cs new file mode 100644 index 0000000..726d8b9 --- /dev/null +++ b/Library/Models/Schema.cs @@ -0,0 +1,24 @@ +namespace SchemaZen.Library.Models; + +public class Schema : INameable, IHasOwner, IScriptable { + public Schema(string name, string owner) { + Owner = owner; + Name = name; + } + + public string Owner { get; set; } + public string Name { get; set; } + + public string ScriptCreate() { + // todo - determine if the check for existing schema and user is necessary + // ideally this would simply return: + // + // create schema [{Name}] authorization [{Owner}] + return $@" +if not exists(select s.schema_id from sys.schemas s where s.name = '{Name}') + and exists(select p.principal_id from sys.database_principals p where p.name = '{Owner}') begin + exec sp_executesql N'create schema [{Name}] authorization [{Owner}]' +end +"; + } +} diff --git a/Library/Models/Table.cs b/Library/Models/Table.cs index 5532c22..e5c08f5 100644 --- a/Library/Models/Table.cs +++ b/Library/Models/Table.cs @@ -9,25 +9,6 @@ namespace SchemaZen.Library.Models; -public class Schema : INameable, IHasOwner, IScriptable { - public Schema(string name, string owner) { - Owner = owner; - Name = name; - } - - public string Owner { get; set; } - public string Name { get; set; } - - public string ScriptCreate() { - return $@" -if not exists(select s.schema_id from sys.schemas s where s.name = '{Name}') - and exists(select p.principal_id from sys.database_principals p where p.name = '{Owner}') begin - exec sp_executesql N'create schema [{Name}] authorization [{Owner}]' -end -"; - } -} - public class Table : INameable, IHasOwner, IScriptable { private const string _tab = "\t"; private const string _escapeTab = "--SchemaZenTAB--"; diff --git a/Test/Integration/CheckTestSchemas.cs b/Test/Integration/CheckTestSchemas.cs index 8d472c7..869b97f 100644 --- a/Test/Integration/CheckTestSchemas.cs +++ b/Test/Integration/CheckTestSchemas.cs @@ -72,8 +72,11 @@ public class CheckTestSchemas { var copy = new Database(destDbName); copy.Connection = _dbHelper.GetConnString(sourceDbName); copy.Load(); - var scripted = copy.ScriptCreate(); - await _dbHelper.ExecBatchSqlAsync(scripted); + + // script to dir and create from dir to simulate what happens from the cli + copy.Dir = destDbName; + copy.ScriptToDir(); + copy.CreateFromDir(true); //compare the dbs to make sure they are the same var source = new Database(sourceDbName) { diff --git a/Test/Integration/DatabaseTest.cs b/Test/Integration/DatabaseTest.cs index b3f6a57..d4f5ccd 100644 --- a/Test/Integration/DatabaseTest.cs +++ b/Test/Integration/DatabaseTest.cs @@ -26,11 +26,10 @@ public class DatabaseTester { await testDb.ExecSqlAsync(@"create nonclustered index MyIndex on MyTable (Id desc)"); var db = new Database("test") { Connection = testDb.GetConnString() }; db.Load(); - var result = db.ScriptCreate(); - Assert.Contains( - "CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] ([Id] DESC)", - result); + var index = db.FindConstraint("MyIndex"); + Assert.NotNull(index); + Assert.True(index.Columns[0].Desc); } [Fact] @@ -45,11 +44,10 @@ public class DatabaseTester { var db = new Database("TEST") { Connection = testDb.GetConnString() }; db.Load(); - var result = db.ScriptCreate(); - Assert.Contains( - "CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] ([Id]) WHERE ([EndDate] IS NULL)", - result); + var index = db.FindConstraint("MyIndex"); + Assert.NotNull(index); + Assert.Equal("([EndDate] IS NULL)", index.Filter); } [Fact] @@ -68,11 +66,10 @@ public class DatabaseTester { var db = new Database("TEST") { Connection = testDb.GetConnString() }; db.Load(); - var result = db.ScriptCreate(); - Assert.Contains( - "CREATE UNIQUE CLUSTERED INDEX [MyIndex] ON [dbo].[MyView] ([Id], [Name])", - result); + + var index = db.FindViewIndex("MyIndex"); + Assert.NotNull(index); } [Fact] @@ -139,13 +136,16 @@ public class DatabaseTester { db.Load(); Assert.Single(db.TableTypes); - Assert.Equal(250, db.TableTypes[0].Columns.Items[0].Length); - Assert.Equal(1, db.TableTypes[0].Columns.Items[1].Scale); - Assert.Equal(5, db.TableTypes[0].Columns.Items[1].Precision); - Assert.Equal(-1, db.TableTypes[0].Columns.Items[2].Length); - Assert.Equal("MyTableType", db.TableTypes[0].Name); + var tt = db.TableTypes.FirstOrDefault(); + if (tt == null) throw new Exception("impossible due to Assert.Single"); - var result = db.ScriptCreate(); + Assert.Equal(250, tt.Columns.Items[0].Length); + Assert.Equal(1, tt.Columns.Items[1].Scale); + Assert.Equal(5, tt.Columns.Items[1].Precision); + Assert.Equal(-1, tt.Columns.Items[2].Length); + Assert.Equal("MyTableType", tt.Name); + + var result = tt.ScriptCreate(); Assert.Contains( "CREATE TYPE [dbo].[MyTableType] AS TABLE", result); @@ -171,12 +171,14 @@ public class DatabaseTester { db.Load(); Assert.Single(db.TableTypes); - Assert.Single(db.TableTypes[0].PrimaryKey.Columns); - Assert.Equal("ID", db.TableTypes[0].PrimaryKey.Columns[0].ColumnName); - Assert.Equal(50, db.TableTypes[0].Columns.Items[1].Length); - Assert.Equal("MyTableType", db.TableTypes[0].Name); - - var result = db.ScriptCreate(); + var tt = db.TableTypes.FirstOrDefault(); + if (tt == null) throw new Exception("impossible due to Assert.Single"); + Assert.Single(tt.PrimaryKey.Columns); + Assert.Equal("ID", tt.PrimaryKey.Columns[0].ColumnName); + Assert.Equal(50, tt.Columns.Items[1].Length); + Assert.Equal("MyTableType", tt.Name); + + var result = tt.ScriptCreate(); Assert.Contains("PRIMARY KEY", result); } @@ -195,10 +197,12 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) db.Load(); Assert.Single(db.TableTypes); - Assert.Equal(3, db.TableTypes[0].Columns.Items.Count()); - Assert.Equal("ComputedValue", db.TableTypes[0].Columns.Items[2].Name); - Assert.Equal("([VALUE1]+[VALUE2])", db.TableTypes[0].Columns.Items[2].ComputedDefinition); - Assert.Equal("MyTableType", db.TableTypes[0].Name); + var tt = db.TableTypes.FirstOrDefault(); + if (tt == null) throw new Exception("impossible due to Assert.Single"); + Assert.Equal(3, tt.Columns.Items.Count()); + Assert.Equal("ComputedValue", tt.Columns.Items[2].Name); + Assert.Equal("([VALUE1]+[VALUE2])", tt.Columns.Items[2].ComputedDefinition); + Assert.Equal("MyTableType", tt.Name); } [Fact] @@ -216,8 +220,10 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) db.Load(); Assert.Single(db.TableTypes); - Assert.Single(db.TableTypes[0].Constraints); - var constraint = db.TableTypes[0].Constraints.First(); + var tt = db.TableTypes.FirstOrDefault(); + if (tt == null) throw new Exception("impossible due to Assert.Single"); + Assert.Single(tt.Constraints); + var constraint = tt.Constraints.First(); Assert.Equal("([Value]>(0))", constraint.CheckConstraintExpression); Assert.Equal("MyTableType", db.TableTypes[0].Name); } @@ -278,9 +284,6 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) var db = new Database("test") { Connection = testDb.GetConnString() }; db.Load(); - // Required in order to expose the exception - db.ScriptCreate(); - Assert.Equal(2, db.ForeignKeys.Count()); Assert.Equal(db.ForeignKeys[0].Name, db.ForeignKeys[1].Name); Assert.NotEqual(db.ForeignKeys[0].Table.Owner, db.ForeignKeys[1].Table.Owner); @@ -323,9 +326,6 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) var db = new Database("test") { Connection = testDb.GetConnString() }; db.Load(); - // Required in order to expose the exception - db.ScriptCreate(); - var triggers = db.Routines.Where(x => x.RoutineType == Routine.RoutineKind.Trigger) .ToList(); @@ -366,7 +366,8 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) db.FindProp("QUOTED_IDENTIFIER").Value = "ON"; db.FindProp("ANSI_NULLS").Value = "ON"; - var script = db.ScriptCreate(); + var trigger = db.FindRoutine("TR_1", "dbo"); + var script = trigger.ScriptCreate(); Assert.DoesNotContain( "INSERTEDENABLE", @@ -588,8 +589,6 @@ [ComputedValue] AS ([VALUE1]+[VALUE2]) await _dbHelper.DropDbAsync(db.Name); await using var testDb = await _dbHelper.CreateTestDb(db); - db.Connection = testDb.GetConnString(); - db.Dir = db.Name; db.Load(); if (Directory.Exists(db.Dir)) diff --git a/Test/Integration/Helpers/TestDbHelper.cs b/Test/Integration/Helpers/TestDbHelper.cs index b0254c4..a0b8d77 100644 --- a/Test/Integration/Helpers/TestDbHelper.cs +++ b/Test/Integration/Helpers/TestDbHelper.cs @@ -24,7 +24,17 @@ public class TestDbHelper { } public async Task CreateTestDb(Database db) { - await ExecBatchSqlAsync(db.ScriptCreate()); + if (string.IsNullOrEmpty(db.Dir)) db.Dir = db.Name; + if (string.IsNullOrEmpty(db.Connection)) + db.Connection = GetConnString(db.Name); + + // todo make async ScriptToDir and get rid of Task.Run + await Task.Run( + () => { + db.ScriptToDir(); + db.CreateFromDir(false); // no overwrite - db should not exist + }); + return new TestDb(db.Name, this); } diff --git a/Test/Integration/UserTest.cs b/Test/Integration/UserTest.cs new file mode 100644 index 0000000..e1a1de5 --- /dev/null +++ b/Test/Integration/UserTest.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Logging; +using SchemaZen.Library.Models; +using Test.Integration.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace Test.Integration; + +[Trait("Category", "Integration")] +public class UserTest { + private readonly TestDbHelper _dbHelper; + + private readonly ILogger _logger; + + public UserTest(ITestOutputHelper output, TestDbHelper dbHelper) { + _logger = output.BuildLogger(); + _dbHelper = dbHelper; + } + + [Fact] + public async Task TestScriptUserAssignedToRole() { + var testSchema = @" +CREATE VIEW a_view AS SELECT 1 AS N +GO + +CREATE ROLE [MyRole] +GO + +GRANT SELECT ON [dbo].[a_view] TO [MyRole] +GO + +IF SUSER_ID('usr') IS NULL BEGIN +CREATE LOGIN usr WITH PASSWORD = 0x0100A92164F026C6EFC652DE59D9DEF79AC654E4E8EFA8E01A9B HASHED END +CREATE USER [usr] FOR LOGIN usr WITH DEFAULT_SCHEMA = dbo +exec sp_addrolemember 'MyRole', 'usr' +exec sp_addrolemember 'db_datareader', 'usr' +GO + + "; + + await using var testDb = await _dbHelper.CreateTestDbAsync(); + + await testDb.ExecBatchSqlAsync(testSchema); + + var db = new Database(_dbHelper.MakeTestDbName()); + db.Connection = testDb.GetConnString(); + db.Load(); + db.Dir = db.Name; + db.ScriptToDir(); + + db.Load(); + + var ex = Record.Exception(() => db.CreateFromDir(true)); + Assert.Null(ex); + } + + [Fact] + public async Task TestScriptUserAssignedToSchema() { + var testSchema = @" +CREATE VIEW a_view AS SELECT 1 AS N +GO + +CREATE ROLE [MyRole] +GO + +GRANT SELECT ON [dbo].[a_view] TO [MyRole] +GO + +IF SUSER_ID('usr') IS NULL BEGIN +CREATE LOGIN usr WITH PASSWORD = 0x0100A92164F026C6EFC652DE59D9DEF79AC654E4E8EFA8E01A9B HASHED END +CREATE USER [usr] FOR LOGIN usr WITH DEFAULT_SCHEMA = dbo +exec sp_addrolemember 'MyRole', 'usr' +exec sp_addrolemember 'db_datareader', 'usr' +GO + + "; + + await using var testDb = await _dbHelper.CreateTestDbAsync(); + + await testDb.ExecBatchSqlAsync(testSchema); + + var db = new Database(_dbHelper.MakeTestDbName()); + db.Connection = testDb.GetConnString(); + db.Load(); + db.Dir = db.Name; + db.ScriptToDir(); + + db.Load(); + + var ex = Record.Exception(() => db.CreateFromDir(true)); + Assert.Null(ex); + } +}