From ec264cc60f6ef7b116d43f796c0e797893c87f69 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:01:30 +0100 Subject: [PATCH 1/9] Add the CreateEmptyDatabaseAsync() and DeleteDatabaseAsync() methods. --- src/Directory.Build.props | 4 +++ src/Testing.Databases.SqlServer/SqlServer.cs | 28 +++++++++++++++++++ .../SqlServerTest.cs | 22 +++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2689e88..6c9d262 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,6 +11,10 @@ README.md MIT + 2.2.0 + - Add SqlServer.CreateEmptyDatabaseAsync() method. + - Add SqlServer.DeleteDatabaseAsync() method. + 2.1.0 - PosInformatique.Testing.Databases.SqlServer target the .NET Standard 2.0 platform. - PosInformatique.Testing.Databases.SqlServer.Dac target the .NET Core 6.0 and .NET Framework 4.6.2 diff --git a/src/Testing.Databases.SqlServer/SqlServer.cs b/src/Testing.Databases.SqlServer/SqlServer.cs index 4351791..c848b12 100644 --- a/src/Testing.Databases.SqlServer/SqlServer.cs +++ b/src/Testing.Databases.SqlServer/SqlServer.cs @@ -51,6 +51,21 @@ public SqlServerDatabase CreateEmptyDatabase(string name) return this.GetDatabase(name); } + /// + /// Creates an empty database asynchronously in the SQL Server instance with the specified . + /// If the database already exists, it will be delete. + /// + /// Name of the database to create. + /// used to cancel the asynchronous operation. + /// A which represents the asynchronous operation and contains an instance of which allows to execute SQL commands/queries. + public async Task CreateEmptyDatabaseAsync(string name, CancellationToken cancellationToken = default) + { + await this.DeleteDatabaseAsync(name, cancellationToken); + await this.Master.ExecuteNonQueryAsync($"CREATE DATABASE [{name}]", cancellationToken); + + return this.GetDatabase(name); + } + /// /// Deletes a database in the SQL server instance with the specified . /// If the database does not exists, no exception is thrown. @@ -62,6 +77,19 @@ public void DeleteDatabase(string name) this.Master.ExecuteNonQuery($"IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = '{name}') DROP DATABASE [{name}]"); } + /// + /// Deletes a database asynchronously in the SQL server instance with the specified . + /// If the database does not exists, no exception is thrown. + /// + /// Name of the database to delete. + /// used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public async Task DeleteDatabaseAsync(string name, CancellationToken cancellationToken = default) + { + await this.Master.ExecuteNonQueryAsync($"IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = '{name}') ALTER DATABASE [{name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", cancellationToken); + await this.Master.ExecuteNonQueryAsync($"IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = '{name}') DROP DATABASE [{name}]", cancellationToken); + } + /// /// Gets an instance of the for the database specified with the . /// diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 09c53c4..e4824da 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -6,8 +6,11 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests { + [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerTest { + private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlServerTest)}; Integrated Security=True"; + [Theory] [InlineData("Data Source=TheServer; Initial Catalog=TheDB; User ID=TheID; Password=ThePassword", "Data Source=TheServer;Initial Catalog=master;User ID=TheID;Password=ThePassword")] [InlineData("Data Source=TheServer; Initial Catalog=TheDB; Integrated Security=True", "Data Source=TheServer;Initial Catalog=master;Integrated Security=True")] @@ -18,5 +21,24 @@ public void Constructor(string connectionString, string expectedMasterConnection server.Master.ConnectionString.Should().Be(expectedMasterConnectionString); server.Master.Server.Should().BeSameAs(server); } + + [Fact] + public async Task CreateAndDeleteAsync() + { + var server = new SqlServer(ConnectionString); + + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB;Integrated Security=True"); + + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); + table.Rows.Should().HaveCount(1); + + // Delete the database + await server.DeleteDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); + table.Rows.Should().BeEmpty(); + } } } \ No newline at end of file From 4e94659c095c2197410df7be2740dbbeba5769f0 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:09:45 +0100 Subject: [PATCH 2/9] Add SqlServer.ExecuteScriptAsync() method. --- src/Directory.Build.props | 1 + .../SqlServerDatabaseExtensions.cs | 38 ++++++ .../SqlServerDatabaseExtensionsTest.cs | 129 ++++++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6c9d262..ffa6118 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -14,6 +14,7 @@ 2.2.0 - Add SqlServer.CreateEmptyDatabaseAsync() method. - Add SqlServer.DeleteDatabaseAsync() method. + - Add SqlServer.ExecuteScriptAsync() method. 2.1.0 - PosInformatique.Testing.Databases.SqlServer target the .NET Standard 2.0 platform. diff --git a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs index 2026d2a..94656f1 100644 --- a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs @@ -164,6 +164,44 @@ public static void ExecuteScript(this SqlServerDatabase database, StringReader s } } + /// + /// Execute an T-SQL script on the . + /// + /// where the will be executed. + /// T-SQL script to execute. + /// used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static async Task ExecuteScriptAsync(this SqlServerDatabase database, string script, CancellationToken cancellationToken = default) + { + using var stringReader = new StringReader(script); + + await ExecuteScriptAsync(database, stringReader); + } + + /// + /// Execute an T-SQL script on the asynchronously. + /// + /// where the will be executed. + /// which contains the T-SQL script to execute. + /// used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static async Task ExecuteScriptAsync(this SqlServerDatabase database, StringReader script, CancellationToken cancellationToken = default) + { + var parser = new SqlServerScriptParser(script); + + var block = parser.ReadNextBlock(); + + while (block is not null) + { + for (var i = 0; i < block.Count; i++) + { + await database.ExecuteNonQueryAsync(block.Code, cancellationToken); + } + + block = parser.ReadNextBlock(); + } + } + private sealed class SqlInsertStatementBuilder { private readonly string tableName; diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs index ffdfdde..4718aec 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs @@ -219,5 +219,134 @@ GO 10 table.Rows[0]["Id"].Should().Be(10); } + + [Fact] + public async Task ExecuteScriptAsync_String() + { + var server = new SqlServer(ConnectionString); + + var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); + + await database.ExecuteScriptAsync(@" + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) + + GO + GO + + INSERT INTO [TableTest] ([Id]) VALUES (0) + + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 + + GO 10"); + + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + + table.Rows.Should().HaveCount(1); + + table.Rows[0]["Id"].Should().Be(10); + } + + [Fact] + public async Task ExecuteScriptAsync_String_WithEmptyLinesAtTheEnd() + { + var server = new SqlServer(ConnectionString); + + var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); + + await database.ExecuteScriptAsync(@" + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) + + GO + GO + + INSERT INTO [TableTest] ([Id]) VALUES (0) + + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 + + GO 10 + + "); + + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + + table.Rows.Should().HaveCount(1); + + table.Rows[0]["Id"].Should().Be(10); + } + + [Fact] + public async Task ExecuteScriptAsync_StringReader() + { + var server = new SqlServer(ConnectionString); + + var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); + + await database.ExecuteScriptAsync(new StringReader(@" + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) + + GO + GO + + INSERT INTO [TableTest] ([Id]) VALUES (0) + + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 + + GO 10")); + + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + + table.Rows.Should().HaveCount(1); + + table.Rows[0]["Id"].Should().Be(10); + } + + [Fact] + public async Task ExecuteScriptAsync_StringReader_WithEmptyLinesAtTheEnd() + { + var server = new SqlServer(ConnectionString); + + var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); + + await database.ExecuteScriptAsync(new StringReader(@" + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) + + GO + GO + + INSERT INTO [TableTest] ([Id]) VALUES (0) + + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 + + GO 10 + + + ")); + + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + + table.Rows.Should().HaveCount(1); + + table.Rows[0]["Id"].Should().Be(10); + } } } \ No newline at end of file From bd43454272c821e27c6b02088fb585bb28189ef4 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:18:27 +0100 Subject: [PATCH 3/9] Add SqlServerDatabase.InsertIntoAsync() method. --- src/Directory.Build.props | 1 + .../SqlServerDatabaseExtensions.cs | 107 ++++++++++++------ .../SqlServerDatabaseInitializerTest.cs | 42 +++++++ 3 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ffa6118..f5b089c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,6 +15,7 @@ - Add SqlServer.CreateEmptyDatabaseAsync() method. - Add SqlServer.DeleteDatabaseAsync() method. - Add SqlServer.ExecuteScriptAsync() method. + - Add SqlServer.InsertIntoAsync() method. 2.1.0 - PosInformatique.Testing.Databases.SqlServer target the .NET Standard 2.0 platform. diff --git a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs index 94656f1..6bafa09 100644 --- a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs @@ -55,43 +55,43 @@ public static int InsertInto(this SqlServerDatabase database, string tableNam /// The number of the rows inserted. public static int InsertInto(this SqlServerDatabase database, string tableName, bool disableIdentityInsert, params T[] objects) { - var builder = new SqlInsertStatementBuilder(tableName); - var properties = typeof(T).GetProperties(); - - foreach (var property in properties) - { - builder.AddColumn(property.Name); - } - - foreach (var @object in objects) - { - foreach (var property in properties) - { - _ = property.PropertyType switch - { - _ when property.GetValue(@object) == null => builder.AddValue("NULL"), - _ when property.PropertyType == typeof(bool) => builder.AddValue(Convert.ToString(Convert.ToInt32((bool)property.GetValue(@object)!), CultureInfo.InvariantCulture)!), - _ when property.PropertyType == typeof(bool?) => builder.AddValue(Convert.ToString(Convert.ToInt32(((bool?)property.GetValue(@object)!).Value), CultureInfo.InvariantCulture)!), - _ when property.PropertyType == typeof(byte[]) => builder.AddValue((byte[])property.GetValue(@object)!), - Type t when Array.Exists(AuthorizedNonStringTypes, at => at == t) => builder.AddValue(Convert.ToString(property.GetValue(@object), CultureInfo.InvariantCulture)!), - _ => builder.AddValueWithQuotes((string)property.GetValue(@object)!), - }; - } + var statement = BuildInsertStatement(tableName, disableIdentityInsert, objects); - if (!@object!.Equals(objects[objects.Length - 1])) - { - builder.NewRecord(); - } - } + return database.ExecuteNonQuery(statement); + } - var statement = builder.ToString(); + /// + /// Insert data into the table asynchronously specified by the argument. The row + /// to insert are represents by objects (or anonymous objects) which the property name must match the + /// the column name. + /// + /// Type of the object which contains the data to insert in the table. + /// SQL Server database which contains the table where the data will be inserted. + /// SQL table where the data will be inserted. + /// Set of object which represents the row to insert. Each object must have property which are mapped to the column to insert. + /// A which represents the asynchronous operation and contains the number of the rows inserted. + public static async Task InsertIntoAsync(this SqlServerDatabase database, string tableName, params T[] objects) + { + return await InsertIntoAsync(database, tableName, false, objects); + } - if (disableIdentityInsert) - { - statement = $"SET IDENTITY_INSERT [{tableName}] ON;" + statement + $"SET IDENTITY_INSERT [{tableName}] OFF;"; - } + /// + /// Insert data into the table asynchronously specified by the argument. The row + /// to insert are represents by objects (or anonymous objects) which the property name must match the + /// the column name. + /// + /// Type of the object which contains the data to insert in the table. + /// SQL Server database which contains the table where the data will be inserted. + /// SQL table where the data will be inserted. + /// to disable auto incrementation of the IDENTITY column. In this case, the object must contains explicitely the value of the IDENTITY + /// column to insert. + /// Set of object which represents the row to insert. Each object must have property which are mapped to the column to insert. + /// A which represents the asynchronous operation and contains the number of the rows inserted. + public static Task InsertIntoAsync(this SqlServerDatabase database, string tableName, bool disableIdentityInsert, params T[] objects) + { + var statement = BuildInsertStatement(tableName, disableIdentityInsert, objects); - return database.ExecuteNonQuery(statement); + return database.ExecuteNonQueryAsync(statement); } /// @@ -202,6 +202,47 @@ public static async Task ExecuteScriptAsync(this SqlServerDatabase database, Str } } + private static string BuildInsertStatement(string tableName, bool disableIdentityInsert, params T[] objects) + { + var builder = new SqlInsertStatementBuilder(tableName); + var properties = typeof(T).GetProperties(); + + foreach (var property in properties) + { + builder.AddColumn(property.Name); + } + + foreach (var @object in objects) + { + foreach (var property in properties) + { + _ = property.PropertyType switch + { + _ when property.GetValue(@object) == null => builder.AddValue("NULL"), + _ when property.PropertyType == typeof(bool) => builder.AddValue(Convert.ToString(Convert.ToInt32((bool)property.GetValue(@object)!), CultureInfo.InvariantCulture)!), + _ when property.PropertyType == typeof(bool?) => builder.AddValue(Convert.ToString(Convert.ToInt32(((bool?)property.GetValue(@object)!).Value), CultureInfo.InvariantCulture)!), + _ when property.PropertyType == typeof(byte[]) => builder.AddValue((byte[])property.GetValue(@object)!), + Type t when Array.Exists(AuthorizedNonStringTypes, at => at == t) => builder.AddValue(Convert.ToString(property.GetValue(@object), CultureInfo.InvariantCulture)!), + _ => builder.AddValueWithQuotes((string)property.GetValue(@object)!), + }; + } + + if (!@object!.Equals(objects[objects.Length - 1])) + { + builder.NewRecord(); + } + } + + var statement = builder.ToString(); + + if (disableIdentityInsert) + { + statement = $"SET IDENTITY_INSERT [{tableName}] ON;" + statement + $"SET IDENTITY_INSERT [{tableName}] OFF;"; + } + + return statement; + } + private sealed class SqlInsertStatementBuilder { private readonly string tableName; diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs index 57da907..44c72e0 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs @@ -66,5 +66,47 @@ public void Test2() // Insert a row which should not be use in other tests. this.database.InsertInto("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); } + + [Fact] + public async Task Test1Async() + { + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + await this.database.InsertIntoAsync("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } + + [Fact] + public async Task Test2Async() + { + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + await this.database.InsertIntoAsync("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } } } \ No newline at end of file From 683c435a67ef8f0e7610b6b58afae47c8fc5d096 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:25:38 +0100 Subject: [PATCH 4/9] Add EntityFrameworkSqlServerExtensions.CreateDatabase() method. --- src/Directory.Build.props | 1 + .../EntityFrameworkSqlServerExtensions.cs | 24 +++++++- .../EntityFrameworkSqlServerExtensionsTest.cs | 56 ++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f5b089c..8616fe1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,6 +12,7 @@ MIT 2.2.0 + - Add SqlServer.CreateDatabase() method to create database from an Entity Framework DbContext. - Add SqlServer.CreateEmptyDatabaseAsync() method. - Add SqlServer.DeleteDatabaseAsync() method. - Add SqlServer.ExecuteScriptAsync() method. diff --git a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs index 73eb0d3..ace72cc 100644 --- a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs +++ b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs @@ -15,13 +15,33 @@ namespace PosInformatique.Testing.Databases.SqlServer public static class EntityFrameworkSqlServerExtensions { /// - /// Deploy a database using a DACPAC file. + /// Creates a database using the specified Entity Framework . /// - /// If a database already exists, it will be deleted. + /// If the database already exists, it will be deleted. /// instance where the database will be created. /// Name of the database to create. /// which represents the database to create. /// An instance of the which represents the deployed database. + public static SqlServerDatabase CreateDatabase(this SqlServer server, string name, DbContext context) + { + var database = server.GetDatabase(name); + + context.Database.SetConnectionString(database.ConnectionString); + + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + return database; + } + + /// + /// Creates a database using the specified Entity Framework . + /// + /// If the database already exists, it will be deleted. + /// instance where the database will be created. + /// Name of the database to create. + /// which represents the database to create. + /// A which represents the asynchronous operation and contains an instance of the that represents the deployed database. public static async Task CreateDatabaseAsync(this SqlServer server, string name, DbContext context) { var database = server.GetDatabase(name); diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs index 232eb0b..1321b05 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs @@ -24,7 +24,7 @@ public async Task Create_WithNoExistingDatabase() var server = new SqlServer(ConnectionString); server.DeleteDatabase(nameof(EntityFrameworkSqlServerExtensionsTest)); - var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); + var database = server.CreateDatabase(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); @@ -52,6 +52,60 @@ public async Task Create_WithAlreadyExistingDatabase() emptyDatabase.ExecuteNonQuery("CREATE TABLE [MustBeDeleted] ([Id] INT)"); + var database = server.CreateDatabase(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + + var tables = await database.GetTablesAsync(); + + tables.Should().HaveCount(1); + + tables[0].Name.Should().Be("Entity"); + + tables[0].Columns.Should().HaveCount(2); + tables[0].Columns[0].Name.Should().Be("Id"); + tables[0].Columns[1].Name.Should().Be("Name"); + } + + [Fact] + public async Task CreateAsync_WithNoExistingDatabase() + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Data Source=OtherServer;"); + + using var dbContext = new DbContextTest(optionsBuilder.Options); + + var server = new SqlServer(ConnectionString); + await server.DeleteDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest)); + + var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + + var tables = await database.GetTablesAsync(); + + tables.Should().HaveCount(1); + + tables[0].Name.Should().Be("Entity"); + + tables[0].Columns.Should().HaveCount(2); + tables[0].Columns[0].Name.Should().Be("Id"); + tables[0].Columns[1].Name.Should().Be("Name"); + } + + [Fact] + public async Task CreateAsync_WithAlreadyExistingDatabase() + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Data Source=OtherServer;"); + + using var dbContext = new DbContextTest(optionsBuilder.Options); + + var server = new SqlServer(ConnectionString); + var emptyDatabase = await server.CreateEmptyDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest)); + + await emptyDatabase.ExecuteNonQueryAsync("CREATE TABLE [MustBeDeleted] ([Id] INT)"); + var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); From 35930a957e2ce2cc97f5bbebc3f3f49b079a7949 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:33:25 +0100 Subject: [PATCH 5/9] Add the SqlServerDatabase.ClearData() method. --- src/Directory.Build.props | 5 +- .../SqlServerDatabaseExtensions.cs | 72 ++++++++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8616fe1..26a5ca9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,11 +12,12 @@ MIT 2.2.0 + - Add SqlServerDatabase.ClearDataAsync() method. - Add SqlServer.CreateDatabase() method to create database from an Entity Framework DbContext. - Add SqlServer.CreateEmptyDatabaseAsync() method. - Add SqlServer.DeleteDatabaseAsync() method. - - Add SqlServer.ExecuteScriptAsync() method. - - Add SqlServer.InsertIntoAsync() method. + - Add SqlServerDatabase.ExecuteScriptAsync() method. + - Add SqlServerDatabase.InsertIntoAsync() method. 2.1.0 - PosInformatique.Testing.Databases.SqlServer target the .NET Standard 2.0 platform. diff --git a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs index 6bafa09..d70e371 100644 --- a/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer/SqlServerDatabaseExtensions.cs @@ -100,34 +100,24 @@ public static Task InsertIntoAsync(this SqlServerDatabase database, stri /// SQL Server database which the data have to be deleted. public static void ClearAllData(this SqlServerDatabase database) { - database.ExecuteNonQuery("EXEC sp_msforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT all'"); - - database.ExecuteNonQuery("EXEC sp_msforeachtable 'SET QUOTED_IDENTIFIER ON; DELETE FROM ?'"); - - // Re-initialize the seed of the IDENTITY columns. - // For each table which contains an IDENTITY column, execute the following SQL statement: - // DBCC CHECKIDENT ('[].[]', RESEED, ) - database.ExecuteNonQuery(@" - DECLARE @sqlcmd VARCHAR(MAX); - - SET @sqlcmd = ( - SELECT STRING_AGG(CAST('DBCC CHECKIDENT (''[' + [s].[name] + '].[' + [t].[name] + ']'', RESEED, ' + CAST([ic].[seed_value] AS VARCHAR(20)) + ')' AS NVARCHAR(MAX)),';' + CHAR(10)) WITHIN GROUP (ORDER BY [t].[name]) - FROM - [sys].[schemas] AS [s], - [sys].[tables] AS [t], - [sys].[columns] AS [c], - [sys].[identity_columns] AS [ic] - WHERE - [s].[schema_id] = [t].[schema_id] - AND [t].[object_id] = [c].[object_id] - AND [c].[is_identity] = 1 - AND [c].[object_id] = [ic].[object_id] - AND [c].[column_id] = [ic].[column_id] - ) - - EXEC (@sqlcmd)"); + foreach (var statement in GetClearDataStatements()) + { + database.ExecuteNonQuery(statement); + } + } - database.ExecuteNonQuery("EXEC sp_msforeachtable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all'"); + /// + /// Clear all in the database asynchronously. + /// + /// SQL Server database which the data have to be deleted. + /// used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static async Task ClearAllDataAsync(this SqlServerDatabase database, CancellationToken cancellationToken = default) + { + foreach (var statement in GetClearDataStatements()) + { + await database.ExecuteNonQueryAsync(statement, cancellationToken); + } } /// @@ -243,6 +233,34 @@ Type t when Array.Exists(AuthorizedNonStringTypes, at => at == t) => builder.Add return statement; } + private static string[] GetClearDataStatements() + { + return + [ + "EXEC sp_msforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT all'", + "EXEC sp_msforeachtable 'SET QUOTED_IDENTIFIER ON; DELETE FROM ?'", + @"DECLARE @sqlcmd VARCHAR(MAX); + + SET @sqlcmd = ( + SELECT STRING_AGG(CAST('DBCC CHECKIDENT (''[' + [s].[name] + '].[' + [t].[name] + ']'', RESEED, ' + CAST([ic].[seed_value] AS VARCHAR(20)) + ')' AS NVARCHAR(MAX)),';' + CHAR(10)) WITHIN GROUP (ORDER BY [t].[name]) + FROM + [sys].[schemas] AS [s], + [sys].[tables] AS [t], + [sys].[columns] AS [c], + [sys].[identity_columns] AS [ic] + WHERE + [s].[schema_id] = [t].[schema_id] + AND [t].[object_id] = [c].[object_id] + AND [c].[is_identity] = 1 + AND [c].[object_id] = [ic].[object_id] + AND [c].[column_id] = [ic].[column_id] + ) + + EXEC (@sqlcmd)", + "EXEC sp_msforeachtable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all'", + ]; + } + private sealed class SqlInsertStatementBuilder { private readonly string tableName; From 9a5546972a19d97933dd319382669cd735d3df3b Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 18 Nov 2024 16:34:05 +0100 Subject: [PATCH 6/9] Upgrade version --- .github/workflows/github-actions-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 9a2fb15..667c94c 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 2.1.0 + default: 2.2.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) From df8cd3f0a335fe07e651de702c40a6329ccece63 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 5 Sep 2025 11:23:52 +0200 Subject: [PATCH 7/9] Fix a bug when comparing the primary key which have different names on the same table (fixes: #8). --- ...lDatabaseComparisonResultsTextGenerator.cs | 8 +---- .../Comparer/SqlObjectComparer.cs | 11 +++--- .../Comparer/SqlPrimaryKeyDifferences.cs | 6 ++++ .../Comparer/SqlTableDifferences.cs | 6 ++-- ...erverDatabaseComparerTest.CompareAsync.txt | 3 +- .../SqlServerDatabaseComparerTest.cs | 35 ++++++++++--------- 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs index b41b17c..8bc8acc 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs @@ -98,13 +98,7 @@ public void Visit(SqlTableDifferences differences) this.Generate(differences.Indexes, "Indexes"); - if (differences.PrimaryKey is not null) - { - this.Indent(); - this.WriteLine($"------ Primary key ------"); - differences.PrimaryKey.Accept(this); - this.Unindent(); - } + this.Generate(differences.PrimaryKeys, "Primary keys"); this.Generate(differences.Triggers, "Triggers"); diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs index 31b2ccf..e22bc92 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs @@ -61,7 +61,7 @@ public static IList> Compare(IReadO public static IList Compare(IReadOnlyList source, IReadOnlyList target) { - return Compare(source, target, t => t.Name, diff => new SqlTableDifferences(diff) { PrimaryKey = null }); + return Compare(source, target, t => t.Name, diff => new SqlTableDifferences(diff)); } public SqlObjectDifferences? Visit(SqlCheckConstraint checkConstraint) @@ -200,7 +200,7 @@ public static IList Compare(IReadOnlyList source, var indexesDifferences = Compare(sourceTable.Indexes, table.Indexes, i => i.Name, diff => new SqlIndexDifferences(diff)); // Compare the primary key - var primaryKeyDifferences = (SqlPrimaryKeyDifferences?)Compare(CreateArray(sourceTable.PrimaryKey), CreateArray(table.PrimaryKey), pk => pk.Name).SingleOrDefault(); + var primaryKeyDifferences = Compare(CreateArray(sourceTable.PrimaryKey), CreateArray(table.PrimaryKey), pk => pk.Name, diff => new SqlPrimaryKeyDifferences(diff)); // Compare the triggers var triggersDifferences = Compare(sourceTable.Triggers, table.Triggers, tr => tr.Name); @@ -208,12 +208,9 @@ public static IList Compare(IReadOnlyList source, // Compare the unique constraints var uniqueConstraintsDifferences = Compare(sourceTable.UniqueConstraints, table.UniqueConstraints, uc => uc.Name, diff => new SqlUniqueConstraintDifferences(diff)); - if (columnsDifferences.Count + triggersDifferences.Count + checkConstraintDifferences.Count + indexesDifferences.Count + foreignKeysDifferences.Count + uniqueConstraintsDifferences.Count > 0 || primaryKeyDifferences is not null) + if (columnsDifferences.Count + triggersDifferences.Count + checkConstraintDifferences.Count + indexesDifferences.Count + foreignKeysDifferences.Count + uniqueConstraintsDifferences.Count + primaryKeyDifferences.Count > 0) { - return new SqlTableDifferences(sourceTable, table, SqlObjectDifferenceType.Different, [], columnsDifferences, triggersDifferences, checkConstraintDifferences, indexesDifferences, foreignKeysDifferences, uniqueConstraintsDifferences) - { - PrimaryKey = primaryKeyDifferences, - }; + return new SqlTableDifferences(sourceTable, table, SqlObjectDifferenceType.Different, [], primaryKeyDifferences, columnsDifferences, triggersDifferences, checkConstraintDifferences, indexesDifferences, foreignKeysDifferences, uniqueConstraintsDifferences); } return this.CreateDifferences(table); diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlPrimaryKeyDifferences.cs b/src/Testing.Databases.SqlServer/Comparer/SqlPrimaryKeyDifferences.cs index 4225313..f114647 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlPrimaryKeyDifferences.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlPrimaryKeyDifferences.cs @@ -24,6 +24,12 @@ internal SqlPrimaryKeyDifferences( this.Columns = new ReadOnlyCollection>(columns); } + internal SqlPrimaryKeyDifferences( + SqlObjectDifferences differences) + : this(differences.Source, differences.Target, differences.Type, differences.Properties, []) + { + } + /// /// Gets the difference of the columns in the primary key compared. /// diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs index 02dad79..2addd99 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs @@ -18,6 +18,7 @@ internal SqlTableDifferences( SqlTable? target, SqlObjectDifferenceType type, IReadOnlyList? properties, + IList primaryKeys, IList> columns, IList> triggers, IList> checkConstraints, @@ -26,6 +27,7 @@ internal SqlTableDifferences( IList uniqueConstraints) : base(source, target, type, properties) { + this.PrimaryKeys = new ReadOnlyCollection(primaryKeys); this.Columns = new ReadOnlyCollection>(columns); this.Triggers = new ReadOnlyCollection>(triggers); this.CheckConstraints = new ReadOnlyCollection>(checkConstraints); @@ -36,7 +38,7 @@ internal SqlTableDifferences( internal SqlTableDifferences( SqlObjectDifferences differences) - : this(differences.Source, differences.Target, differences.Type, differences.Properties, [], [], [], [], [], []) + : this(differences.Source, differences.Target, differences.Type, differences.Properties, [], [], [], [], [], [], []) { } @@ -58,7 +60,7 @@ internal SqlTableDifferences( /// /// Gets the primary key differences between the two SQL tables. /// - public SqlPrimaryKeyDifferences? PrimaryKey { get; internal set; } + public ReadOnlyCollection PrimaryKeys { get; } /// /// Gets the foreign keys differences between the two SQL tables. diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt index b7f510d..94eb956 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt @@ -99,7 +99,8 @@ * Position: Source: 1 Target: 2 - ------ Primary key ------ + ------ Primary keys ------ + - PrimaryKeyDifference * Type: Source: NONCLUSTERED Target: CLUSTERED diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index 79b6593..3232e87 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -535,21 +535,22 @@ public async Task CompareAsync() differences.Tables[0].Target.PrimaryKey.Columns[1].Name.Should().Be("Type"); differences.Tables[0].Target.PrimaryKey.Columns[1].Position.Should().Be(2); - differences.Tables[0].PrimaryKey.Columns.Should().HaveCount(2); - differences.Tables[0].PrimaryKey.Columns[0].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[1]); - differences.Tables[0].PrimaryKey.Columns[0].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[0]); - differences.Tables[0].PrimaryKey.Columns[0].Type.Should().Be(SqlObjectDifferenceType.Different); - differences.Tables[0].PrimaryKey.Columns[1].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[0]); - differences.Tables[0].PrimaryKey.Columns[1].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[1]); - differences.Tables[0].PrimaryKey.Columns[1].Type.Should().Be(SqlObjectDifferenceType.Different); - - differences.Tables[0].PrimaryKey.Properties.Should().HaveCount(1); - differences.Tables[0].PrimaryKey.Properties[0].Name.Should().Be("Type"); - differences.Tables[0].PrimaryKey.Properties[0].Source.Should().Be("NONCLUSTERED"); - differences.Tables[0].PrimaryKey.Properties[0].Target.Should().Be("CLUSTERED"); - differences.Tables[0].PrimaryKey.Source.Should().Be(differences.Tables[0].Source.PrimaryKey); - differences.Tables[0].PrimaryKey.Target.Should().Be(differences.Tables[0].Target.PrimaryKey); - differences.Tables[0].PrimaryKey.Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].PrimaryKeys.Should().HaveCount(1); + differences.Tables[0].PrimaryKeys[0].Columns.Should().HaveCount(2); + differences.Tables[0].PrimaryKeys[0].Columns[0].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[1]); + differences.Tables[0].PrimaryKeys[0].Columns[0].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[0]); + differences.Tables[0].PrimaryKeys[0].Columns[0].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].PrimaryKeys[0].Columns[1].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[0]); + differences.Tables[0].PrimaryKeys[0].Columns[1].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[1]); + differences.Tables[0].PrimaryKeys[0].Columns[1].Type.Should().Be(SqlObjectDifferenceType.Different); + + differences.Tables[0].PrimaryKeys[0].Properties.Should().HaveCount(1); + differences.Tables[0].PrimaryKeys[0].Properties[0].Name.Should().Be("Type"); + differences.Tables[0].PrimaryKeys[0].Properties[0].Source.Should().Be("NONCLUSTERED"); + differences.Tables[0].PrimaryKeys[0].Properties[0].Target.Should().Be("CLUSTERED"); + differences.Tables[0].PrimaryKeys[0].Source.Should().Be(differences.Tables[0].Source.PrimaryKey); + differences.Tables[0].PrimaryKeys[0].Target.Should().Be(differences.Tables[0].Target.PrimaryKey); + differences.Tables[0].PrimaryKeys[0].Type.Should().Be(SqlObjectDifferenceType.Different); // Tables / Triggers differences.Tables[0].Source.Triggers.Should().HaveCount(1); @@ -607,7 +608,7 @@ public async Task CompareAsync() // Missing tables differences.Tables[1].Columns.Should().BeEmpty(); differences.Tables[1].Indexes.Should().BeEmpty(); - differences.Tables[1].PrimaryKey.Should().BeNull(); + differences.Tables[1].PrimaryKeys.Should().BeEmpty(); differences.Tables[1].Source.Should().BeNull(); differences.Tables[1].UniqueConstraints.Should().BeEmpty(); differences.Tables[1].Target.CheckConstraints.Should().HaveCount(1); @@ -667,7 +668,7 @@ public async Task CompareAsync() differences.Tables[2].Columns.Should().BeEmpty(); differences.Tables[2].Indexes.Should().BeEmpty(); - differences.Tables[2].PrimaryKey.Should().BeNull(); + differences.Tables[2].PrimaryKeys.Should().BeEmpty(); differences.Tables[2].UniqueConstraints.Should().BeEmpty(); differences.Tables[2].Source.CheckConstraints.Should().HaveCount(1); differences.Tables[2].Source.CheckConstraints[0].Name.Should().Be("CheckConstraintSource"); From d3ee00d8b582b7f9ccd58a24ed0341743ff34468 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 5 Sep 2025 15:03:04 +0200 Subject: [PATCH 8/9] Add the support to compare the default constraints (fixes #7). --- .../Comparer/ISqlObjectDifferencesVisitor.cs | 2 + .../Comparer/SqlColumnDifferences.cs | 41 +++++ ...lDatabaseComparisonResultsTextGenerator.cs | 18 ++ .../Comparer/SqlObjectComparer.cs | 62 +++++-- .../Comparer/SqlTableDifferences.cs | 6 +- .../ObjectModel/ISqlObjectVisitor.cs | 7 + .../ObjectModel/SqlColumn.cs | 5 + .../ObjectModel/SqlDefaultConstraint.cs | 39 +++++ .../SqlServerDatabaseObjectExtensions.cs | 46 +++++- .../Tables/TableDifference.sql | 3 + .../Tables/TableIdentical.sql | 2 +- .../Tables/TableSource.sql | 2 +- .../Tables/TableDifference.sql | 3 + .../Tables/TableTarget.sql | 2 +- .../ObjectModel/SqlDefaultConstraintTest.cs | 19 +++ ...erverDatabaseComparerTest.CompareAsync.txt | 15 ++ .../SqlServerDatabaseComparerTest.cs | 156 +++++++++++++++++- 17 files changed, 394 insertions(+), 34 deletions(-) create mode 100644 src/Testing.Databases.SqlServer/Comparer/SqlColumnDifferences.cs create mode 100644 src/Testing.Databases.SqlServer/ObjectModel/SqlDefaultConstraint.cs create mode 100644 tests/Testing.Databases.SqlServer.Tests/ObjectModel/SqlDefaultConstraintTest.cs diff --git a/src/Testing.Databases.SqlServer/Comparer/ISqlObjectDifferencesVisitor.cs b/src/Testing.Databases.SqlServer/Comparer/ISqlObjectDifferencesVisitor.cs index f912b8f..7987556 100644 --- a/src/Testing.Databases.SqlServer/Comparer/ISqlObjectDifferencesVisitor.cs +++ b/src/Testing.Databases.SqlServer/Comparer/ISqlObjectDifferencesVisitor.cs @@ -11,6 +11,8 @@ internal interface ISqlObjectDifferencesVisitor void Visit(SqlObjectDifferences differences) where TSqlObject : SqlObject; + void Visit(SqlColumnDifferences differences); + void Visit(SqlForeignKeyDifferences differences); void Visit(SqlIndexDifferences differences); diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlColumnDifferences.cs b/src/Testing.Databases.SqlServer/Comparer/SqlColumnDifferences.cs new file mode 100644 index 0000000..7090e53 --- /dev/null +++ b/src/Testing.Databases.SqlServer/Comparer/SqlColumnDifferences.cs @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases +{ + /// + /// Represents the differences of a between two databases. + /// + public class SqlColumnDifferences : SqlObjectDifferences + { + internal SqlColumnDifferences( + SqlColumn? source, + SqlColumn? target, + SqlObjectDifferenceType type, + IReadOnlyList? properties, + SqlObjectDifferences? defaultConstraint) + : base(source, target, type, properties) + { + this.DefaultConstraint = defaultConstraint; + } + + internal SqlColumnDifferences( + SqlObjectDifferences differences) + : this(differences.Source, differences.Target, differences.Type, differences.Properties, null) + { + } + + /// + /// Gets the difference of the columns in the foreign key compared. + /// + public SqlObjectDifferences? DefaultConstraint { get; } + + internal override void Accept(ISqlObjectDifferencesVisitor visitor) + { + visitor.Visit(this); + } + } +} diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs index 8bc8acc..67656fe 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs @@ -64,6 +64,13 @@ public void Visit(SqlObjectDifferences differences) } } + public void Visit(SqlColumnDifferences differences) + { + this.Visit(differences); + + this.Generate(differences.DefaultConstraint, "Default constraint"); + } + public void Visit(SqlForeignKeyDifferences differences) { this.WriteProperties(differences.Properties); @@ -115,6 +122,17 @@ public void Visit(SqlUniqueConstraintDifferences differences) this.Generate(differences.Columns, "Columns"); } + private void Generate(SqlObjectDifferences? difference, string typeName) + where TSqlObject : SqlObject + { + if (difference is null) + { + return; + } + + this.Generate([difference], typeName); + } + private void Generate(IEnumerable> differences, string typeName) where TSqlObject : SqlObject { diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs index e22bc92..b68a284 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs @@ -26,20 +26,11 @@ public static IList> Compare(IReadO var keyValue = keySelector(targetObject); var sourceObject = Find(source, keySelector, keyValue); - if (sourceObject is null) - { - // Missing in the source. - differences.Add(new SqlObjectDifferences(null, targetObject, SqlObjectDifferenceType.MissingInSource, null)); - } - else - { - // Compare the object using visitor pattern. - var difference = Compare(sourceObject, targetObject); + var difference = Compare(sourceObject, targetObject); - if (difference is not null) - { - differences.Add(difference); - } + if (difference is not null) + { + differences.Add(difference); } } @@ -73,8 +64,10 @@ public static IList Compare(IReadOnlyList source, public SqlObjectDifferences? Visit(SqlColumn column) { - return this.CreateDifferences( - column, + var sourceColumn = (SqlColumn)this.source; + + // Compare the properties + var differenceProperties = GetPropertyDifferences( this.CompareProperty(column, t => t.Position, nameof(column.Position)), this.CompareProperty(column, t => t.MaxLength, nameof(column.MaxLength)), this.CompareProperty(column, t => t.Precision, nameof(column.Precision)), @@ -84,6 +77,24 @@ public static IList Compare(IReadOnlyList source, this.CompareProperty(column, t => t.CollationName, nameof(column.CollationName)), this.CompareProperty(column, t => t.IsComputed, nameof(column.IsComputed)), this.CompareProperty(column, t => TsqlCodeHelper.RemoveNotUsefulCharacters(t.ComputedExpression), nameof(column.ComputedExpression), t => t.ComputedExpression)); + + // Compare the default constraint + var defaultConstraintDifference = Compare(sourceColumn.DefaultConstraint, column.DefaultConstraint); + + if (differenceProperties.Count > 0 || defaultConstraintDifference != null) + { + return new SqlColumnDifferences((SqlColumn)this.source, column, SqlObjectDifferenceType.Different, differenceProperties, defaultConstraintDifference); + } + + return null; + } + + public SqlObjectDifferences? Visit(SqlDefaultConstraint defaultConstraint) + { + return this.CreateDifferences( + defaultConstraint, + this.CompareProperty(defaultConstraint, df => df.Name, nameof(defaultConstraint.Name)), + this.CompareProperty(defaultConstraint, df => TsqlCodeHelper.RemoveNotUsefulCharacters(df.Expression), nameof(defaultConstraint.Expression), df => df.Expression)); } public SqlObjectDifferences? Visit(SqlForeignKey foreignKey) @@ -191,7 +202,7 @@ public static IList Compare(IReadOnlyList source, var checkConstraintDifferences = Compare(sourceTable.CheckConstraints, table.CheckConstraints, tr => tr.Name); // Compare the columns - var columnsDifferences = Compare(sourceTable.Columns, table.Columns, c => c.Name); + var columnsDifferences = Compare(sourceTable.Columns, table.Columns, c => c.Name, diff => new SqlColumnDifferences(diff)); // Compare the foreign keys var foreignKeysDifferences = Compare(sourceTable.ForeignKeys, table.ForeignKeys, fk => fk.Name, diff => new SqlForeignKeyDifferences(diff)); @@ -259,9 +270,26 @@ public static IList Compare(IReadOnlyList source, this.CompareProperty(view, v => TsqlCodeHelper.RemoveNotUsefulCharacters(v.Code), nameof(view.Code), v => v.Code)); } - private static SqlObjectDifferences? Compare(TSqlObject source, TSqlObject target) + private static SqlObjectDifferences? Compare(TSqlObject? source, TSqlObject? target) where TSqlObject : SqlObject { + if (source is null) + { + if (target is null) + { + return null; + } + + return new SqlObjectDifferences(null, target, SqlObjectDifferenceType.MissingInSource, null); + } + else + { + if (target is null) + { + return new SqlObjectDifferences(source, null, SqlObjectDifferenceType.MissingInTarget, null); + } + } + var visitor = new SqlObjectComparer(source); return (SqlObjectDifferences?)target.Accept(visitor); diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs index 2addd99..19d69f4 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs @@ -19,7 +19,7 @@ internal SqlTableDifferences( SqlObjectDifferenceType type, IReadOnlyList? properties, IList primaryKeys, - IList> columns, + IList columns, IList> triggers, IList> checkConstraints, IList indexes, @@ -28,7 +28,7 @@ internal SqlTableDifferences( : base(source, target, type, properties) { this.PrimaryKeys = new ReadOnlyCollection(primaryKeys); - this.Columns = new ReadOnlyCollection>(columns); + this.Columns = new ReadOnlyCollection(columns); this.Triggers = new ReadOnlyCollection>(triggers); this.CheckConstraints = new ReadOnlyCollection>(checkConstraints); this.Indexes = new ReadOnlyCollection(indexes); @@ -50,7 +50,7 @@ internal SqlTableDifferences( /// /// Gets the columns differences between the two SQL tables. /// - public ReadOnlyCollection> Columns { get; } + public ReadOnlyCollection Columns { get; } /// /// Gets the indexes differences between the two SQL tables. diff --git a/src/Testing.Databases.SqlServer/ObjectModel/ISqlObjectVisitor.cs b/src/Testing.Databases.SqlServer/ObjectModel/ISqlObjectVisitor.cs index ed564f8..2ceb0b3 100644 --- a/src/Testing.Databases.SqlServer/ObjectModel/ISqlObjectVisitor.cs +++ b/src/Testing.Databases.SqlServer/ObjectModel/ISqlObjectVisitor.cs @@ -26,6 +26,13 @@ public interface ISqlObjectVisitor /// The result of the visit. TResult Visit(SqlColumn column); + /// + /// Visits the specified . + /// + /// to visit. + /// The result of the visit. + TResult Visit(SqlDefaultConstraint defaultConstraint); + /// /// Visits the specified . /// diff --git a/src/Testing.Databases.SqlServer/ObjectModel/SqlColumn.cs b/src/Testing.Databases.SqlServer/ObjectModel/SqlColumn.cs index 5577349..e30a345 100644 --- a/src/Testing.Databases.SqlServer/ObjectModel/SqlColumn.cs +++ b/src/Testing.Databases.SqlServer/ObjectModel/SqlColumn.cs @@ -82,6 +82,11 @@ internal SqlColumn( /// public string? ComputedExpression { get; internal set; } + /// + /// Gets the default constraint of the column. + /// + public SqlDefaultConstraint? DefaultConstraint { get; internal set; } + /// public override TResult Accept(ISqlObjectVisitor visitor) => visitor.Visit(this); diff --git a/src/Testing.Databases.SqlServer/ObjectModel/SqlDefaultConstraint.cs b/src/Testing.Databases.SqlServer/ObjectModel/SqlDefaultConstraint.cs new file mode 100644 index 0000000..361d255 --- /dev/null +++ b/src/Testing.Databases.SqlServer/ObjectModel/SqlDefaultConstraint.cs @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases +{ + /// + /// Represents a default constraint of a . + /// + public class SqlDefaultConstraint : SqlObject + { + internal SqlDefaultConstraint(string name, string expression) + { + this.Name = name; + this.Expression = expression; + } + + /// + /// Gets the name of the default constraint. + /// + public string Name { get; } + + /// + /// Gets the expression of the default constraint. + /// + public string Expression { get; } + + /// + public override TResult Accept(ISqlObjectVisitor visitor) => visitor.Visit(this); + + /// + public override string ToString() + { + return this.Name; + } + } +} diff --git a/src/Testing.Databases.SqlServer/SqlServerDatabaseObjectExtensions.cs b/src/Testing.Databases.SqlServer/SqlServerDatabaseObjectExtensions.cs index 68d234e..746e134 100644 --- a/src/Testing.Databases.SqlServer/SqlServerDatabaseObjectExtensions.cs +++ b/src/Testing.Databases.SqlServer/SqlServerDatabaseObjectExtensions.cs @@ -75,6 +75,9 @@ ORDER BY // Gets the check constraints var allCheckConstraints = GetCheckConstraintsAsync(database, cancellationToken); + // Gets the default constraints + var allDefaultConstraints = GetDefaultConstraintsAsync(database, cancellationToken); + // Gets the indexes var allForeignKeys = GetForeignKeysAsync(database, cancellationToken); @@ -90,7 +93,7 @@ ORDER BY // Gets the unique constraints var allUniqueConstraints = GetUniqueConstraintsAsync(database, cancellationToken); - await Task.WhenAll(allColumns, allCheckConstraints, allForeignKeys, allIndexes, allPrimaryKeys, allTriggers, allUniqueConstraints); + await Task.WhenAll(allColumns, allCheckConstraints, allDefaultConstraints, allForeignKeys, allIndexes, allPrimaryKeys, allTriggers, allUniqueConstraints); // Builds the SqlTable object foreach (var table in result.Rows.Cast()) @@ -108,12 +111,17 @@ ORDER BY var columnsTable = allColumns.Result[(int)table["Id"]]; var columns = new List(); + var defaultConstraintsTable = allDefaultConstraints.Result[(int)table["Id"]]; + foreach (var column in columnsTable.OrderBy(r => r["Position"])) { - columns.Add(ToColumn(column)); + var position = Convert.ToInt32(column["Position"], CultureInfo.InvariantCulture); + var defaultConstraint = defaultConstraintsTable.SingleOrDefault(r => (int)r["ColumnId"] == position); + + columns.Add(ToColumn(column, defaultConstraint)); } - // Indexes + // Foreign keys var foreignKeysTable = allForeignKeys.Result[(int)table["Id"]]; var foreignKeys = new List(); @@ -308,6 +316,28 @@ [sys].[types] AS [ty] return result.Rows.Cast().ToLookup(c => (int)c["TableId"]); } + private static async Task> GetDefaultConstraintsAsync(SqlServerDatabase database, CancellationToken cancellationToken) + { + const string sql = @" + SELECT + [t].[object_id] AS [TableId], + [df].[parent_column_id] AS [ColumnId], + [df].[name] AS [Name], + [df].[definition] AS [Expression] + FROM + [sys].[default_constraints] AS [df], + [sys].[tables] AS [t] + WHERE + [df].[parent_object_id] = [t].[object_id] + ORDER BY + [t].[name], + [df].[name]"; + + var result = await database.ExecuteQueryAsync(sql, cancellationToken); + + return result.Rows.Cast().ToLookup(row => (int)row["TableId"]); + } + private static async Task> GetForeignKeysAsync(SqlServerDatabase database, CancellationToken cancellationToken) { const string sql = @" @@ -468,7 +498,7 @@ private static SqlCheckConstraint ToCheckConstraint(DataRow row) return new SqlCheckConstraint((string)row["Name"], (string)row["Code"]); } - private static SqlColumn ToColumn(DataRow row) + private static SqlColumn ToColumn(DataRow row, DataRow? defaultConstraintRow) { return new SqlColumn( (string)row["Name"], @@ -480,12 +510,20 @@ private static SqlColumn ToColumn(DataRow row) { CollationName = NullIfDbNull(row["CollationName"]), ComputedExpression = NullIfDbNull(row["ComputedExpression"]), + DefaultConstraint = defaultConstraintRow != null ? ToDefaultConstraint(defaultConstraintRow) : null, IsComputed = (bool)row["IsComputed"], IsIdentity = (bool)row["IsIdentity"], IsNullable = (bool)row["IsNullable"], }; } + private static SqlDefaultConstraint ToDefaultConstraint(DataRow row) + { + return new SqlDefaultConstraint( + (string)row["Name"], + (string)row["Expression"]); + } + private static SqlForeignKey ToForeignKey(DataRow row, IList columns) { return new SqlForeignKey((string)row["ForeignKeyName"], (string)row["ReferencedTableName"], (string)row["UpdateAction"], (string)row["DeleteAction"], columns); diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableDifference.sql b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableDifference.sql index 7c38350..0c44ec2 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableDifference.sql +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableDifference.sql @@ -10,4 +10,7 @@ [Computed] AS [Scale] + [Precision], [SourceColumn] INT NOT NULL, [IdenticalColumn] INT NOT NULL, + [ColumnWithDefaultConstraint] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithDefaultConstraint DEFAULT 'Source expression', + [ColumnWithMissingDefaultConstraint] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithMissingDefaultConstraint DEFAULT 'Default value', + [ColumnWithOtherDefaultConstraintName] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithOtherDefaultConstraintName DEFAULT 'Same expression', ) diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableIdentical.sql b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableIdentical.sql index fed9652..95b8acc 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableIdentical.sql +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableIdentical.sql @@ -1,6 +1,6 @@ CREATE TABLE [dbo].[TableIdentical] ( [Id] INT NOT NULL, - [ForeignKeyId] INT NOT NULL, + [ForeignKeyId] INT NOT NULL CONSTRAINT DF_TableIdentical_ForeignKeyId DEFAULT (1 + 2 + 3), [IncludeColumn] INT NOT NULL, ) diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableSource.sql b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableSource.sql index 8ac53e6..5c40f93 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableSource.sql +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableSource.sql @@ -1,6 +1,6 @@ CREATE TABLE [dbo].[TableSource] ( [Id] INT NOT NULL, - [SourceName] VARCHAR(50), + [SourceName] VARCHAR(50) CONSTRAINT DF_TableSource_SourceName DEFAULT 'Source', [SourceForeignKeyId] INT NOT NULL, ) diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableDifference.sql b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableDifference.sql index 5eebfed..fafa34e 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableDifference.sql +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableDifference.sql @@ -10,4 +10,7 @@ [Computed] AS [Scale] - [Precision], [TargetColumn] INT NOT NULL, [IdenticalColumn] INT NOT NULL, + [ColumnWithDefaultConstraint] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithDefaultConstraint DEFAULT 'Target expression', + [ColumnWithMissingDefaultConstraint] VARCHAR(20) NOT NULL, + [ColumnWithOtherDefaultConstraintName] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_WrongName DEFAULT 'Same expression', ) diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableTarget.sql b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableTarget.sql index 18ce924..93f9aa3 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableTarget.sql +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableTarget.sql @@ -1,6 +1,6 @@ CREATE TABLE [dbo].[TableTarget] ( [Id] INT NOT NULL, - [TargetName] VARCHAR(50), + [TargetName] VARCHAR(50) CONSTRAINT DF_TableTarget_TargetName DEFAULT 'Target', [TargetForeignKeyId] INT NOT NULL, ) diff --git a/tests/Testing.Databases.SqlServer.Tests/ObjectModel/SqlDefaultConstraintTest.cs b/tests/Testing.Databases.SqlServer.Tests/ObjectModel/SqlDefaultConstraintTest.cs new file mode 100644 index 0000000..a15bd43 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests/ObjectModel/SqlDefaultConstraintTest.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.Tests +{ + public class SqlDefaultConstraintTest + { + [Fact] + public void ToStringTest() + { + var defaultConstraint = new SqlDefaultConstraint("The name", default); + + defaultConstraint.ToString().Should().Be("The name"); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt index 94eb956..9883303 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt @@ -53,6 +53,21 @@ Source: ([Scale]+[Precision]) Target: ([Scale]-[Precision]) - TargetColumn (Missing in the source) + - ColumnWithDefaultConstraint + ------ Default constraint ------ + - DF_TableDifference_ColumnWithDefaultConstraint + * Expression: + Source: ('Source expression') + Target: ('Target expression') + - ColumnWithMissingDefaultConstraint + ------ Default constraint ------ + - DF_TableDifference_ColumnWithMissingDefaultConstraint (Missing in the target) + - ColumnWithOtherDefaultConstraintName + ------ Default constraint ------ + - DF_TableDifference_ColumnWithOtherDefaultConstraintName + * Name: + Source: DF_TableDifference_ColumnWithOtherDefaultConstraintName + Target: DF_TableDifference_WrongName - SourceColumn (Missing in the target) ------ Foreign keys ------ - ForeignKeyDifference diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index 3232e87..0578d8d 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -78,10 +78,11 @@ public async Task CompareAsync() differences.Tables[0].CheckConstraints[0].Type.Should().Be(SqlObjectDifferenceType.Different); // Tables / Columns - differences.Tables[0].Source.Columns.Should().HaveCount(10); + differences.Tables[0].Source.Columns.Should().HaveCount(13); differences.Tables[0].Source.Columns[0].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[0].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[0].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[0].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[0].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[0].IsNullable.Should().BeFalse(); @@ -94,6 +95,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[1].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); differences.Tables[0].Source.Columns[1].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[1].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[1].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[1].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[1].IsNullable.Should().BeTrue(); @@ -106,6 +108,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[2].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); differences.Tables[0].Source.Columns[2].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[2].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[2].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[2].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[2].IsNullable.Should().BeFalse(); @@ -118,6 +121,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[3].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[3].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[3].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[3].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[3].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[3].IsNullable.Should().BeFalse(); @@ -130,6 +134,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[4].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[4].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[4].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[4].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[4].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[4].IsNullable.Should().BeFalse(); @@ -142,6 +147,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[5].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[5].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[5].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[5].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[5].IsIdentity.Should().BeTrue(); differences.Tables[0].Source.Columns[5].IsNullable.Should().BeFalse(); @@ -154,6 +160,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[6].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[6].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[6].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[6].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[6].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[6].IsNullable.Should().BeTrue(); @@ -166,6 +173,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[7].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[7].ComputedExpression.Should().Be("([Scale]+[Precision])"); + differences.Tables[0].Source.Columns[7].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[7].IsComputed.Should().BeTrue(); differences.Tables[0].Source.Columns[7].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[7].IsNullable.Should().BeTrue(); @@ -178,6 +186,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[8].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[8].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[8].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[8].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[8].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[8].IsNullable.Should().BeFalse(); @@ -190,6 +199,7 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[9].CollationName.Should().BeNull(); differences.Tables[0].Source.Columns[9].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[9].DefaultConstraint.Should().BeNull(); differences.Tables[0].Source.Columns[9].IsComputed.Should().BeFalse(); differences.Tables[0].Source.Columns[9].IsIdentity.Should().BeFalse(); differences.Tables[0].Source.Columns[9].IsNullable.Should().BeFalse(); @@ -200,13 +210,56 @@ public async Task CompareAsync() differences.Tables[0].Source.Columns[9].Scale.Should().Be(0); differences.Tables[0].Source.Columns[9].TypeName.Should().Be("int"); + differences.Tables[0].Source.Columns[10].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Source.Columns[10].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[10].DefaultConstraint.Expression.Should().Be("('Source expression')"); + differences.Tables[0].Source.Columns[10].DefaultConstraint.Name.Should().Be("DF_TableDifference_ColumnWithDefaultConstraint"); + differences.Tables[0].Source.Columns[10].IsComputed.Should().BeFalse(); + differences.Tables[0].Source.Columns[10].IsIdentity.Should().BeFalse(); + differences.Tables[0].Source.Columns[10].IsNullable.Should().BeFalse(); + differences.Tables[0].Source.Columns[10].MaxLength.Should().Be(20); + differences.Tables[0].Source.Columns[10].Name.Should().Be("ColumnWithDefaultConstraint"); + differences.Tables[0].Source.Columns[10].Position.Should().Be(11); + differences.Tables[0].Source.Columns[10].Precision.Should().Be(0); + differences.Tables[0].Source.Columns[10].Scale.Should().Be(0); + differences.Tables[0].Source.Columns[10].TypeName.Should().Be("varchar"); + + differences.Tables[0].Source.Columns[11].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Source.Columns[11].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[11].DefaultConstraint.Expression.Should().Be("('Default value')"); + differences.Tables[0].Source.Columns[11].DefaultConstraint.Name.Should().Be("DF_TableDifference_ColumnWithMissingDefaultConstraint"); + differences.Tables[0].Source.Columns[11].IsComputed.Should().BeFalse(); + differences.Tables[0].Source.Columns[11].IsIdentity.Should().BeFalse(); + differences.Tables[0].Source.Columns[11].IsNullable.Should().BeFalse(); + differences.Tables[0].Source.Columns[11].MaxLength.Should().Be(20); + differences.Tables[0].Source.Columns[11].Name.Should().Be("ColumnWithMissingDefaultConstraint"); + differences.Tables[0].Source.Columns[11].Position.Should().Be(12); + differences.Tables[0].Source.Columns[11].Precision.Should().Be(0); + differences.Tables[0].Source.Columns[11].Scale.Should().Be(0); + differences.Tables[0].Source.Columns[11].TypeName.Should().Be("varchar"); + + differences.Tables[0].Source.Columns[12].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Source.Columns[12].ComputedExpression.Should().BeNull(); + differences.Tables[0].Source.Columns[12].DefaultConstraint.Expression.Should().Be("('Same expression')"); + differences.Tables[0].Source.Columns[12].DefaultConstraint.Name.Should().Be("DF_TableDifference_ColumnWithOtherDefaultConstraintName"); + differences.Tables[0].Source.Columns[12].IsComputed.Should().BeFalse(); + differences.Tables[0].Source.Columns[12].IsIdentity.Should().BeFalse(); + differences.Tables[0].Source.Columns[12].IsNullable.Should().BeFalse(); + differences.Tables[0].Source.Columns[12].MaxLength.Should().Be(20); + differences.Tables[0].Source.Columns[12].Name.Should().Be("ColumnWithOtherDefaultConstraintName"); + differences.Tables[0].Source.Columns[12].Position.Should().Be(13); + differences.Tables[0].Source.Columns[12].Precision.Should().Be(0); + differences.Tables[0].Source.Columns[12].Scale.Should().Be(0); + differences.Tables[0].Source.Columns[12].TypeName.Should().Be("varchar"); + differences.Tables[0].Target.Name.Should().Be("TableDifference"); differences.Tables[0].Target.Schema.Should().Be("dbo"); - differences.Tables[0].Target.Columns.Should().HaveCount(10); + differences.Tables[0].Target.Columns.Should().HaveCount(13); differences.Tables[0].Target.Columns[0].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); differences.Tables[0].Target.Columns[0].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[0].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[0].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[0].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[0].IsNullable.Should().BeFalse(); @@ -219,6 +272,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[1].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); differences.Tables[0].Target.Columns[1].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[1].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[1].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[1].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[1].IsNullable.Should().BeFalse(); @@ -231,6 +285,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[2].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[2].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[2].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[2].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[2].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[2].IsNullable.Should().BeFalse(); @@ -243,6 +298,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[3].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); differences.Tables[0].Target.Columns[3].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[3].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[3].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[3].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[3].IsNullable.Should().BeFalse(); @@ -255,6 +311,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[4].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[4].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[4].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[4].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[4].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[4].IsNullable.Should().BeFalse(); @@ -267,6 +324,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[5].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[5].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[5].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[5].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[5].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[5].IsNullable.Should().BeFalse(); @@ -279,6 +337,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[6].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[6].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[6].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[6].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[6].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[6].IsNullable.Should().BeTrue(); @@ -291,6 +350,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[7].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[7].ComputedExpression.Should().Be("([Scale]-[Precision])"); + differences.Tables[0].Target.Columns[7].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[7].IsComputed.Should().BeTrue(); differences.Tables[0].Target.Columns[7].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[7].IsNullable.Should().BeTrue(); @@ -303,6 +363,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[8].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[8].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[8].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[8].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[8].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[8].IsNullable.Should().BeFalse(); @@ -315,6 +376,7 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[9].CollationName.Should().BeNull(); differences.Tables[0].Target.Columns[9].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[9].DefaultConstraint.Should().BeNull(); differences.Tables[0].Target.Columns[9].IsComputed.Should().BeFalse(); differences.Tables[0].Target.Columns[9].IsIdentity.Should().BeFalse(); differences.Tables[0].Target.Columns[9].IsNullable.Should().BeFalse(); @@ -325,43 +387,123 @@ public async Task CompareAsync() differences.Tables[0].Target.Columns[9].Scale.Should().Be(0); differences.Tables[0].Target.Columns[9].TypeName.Should().Be("int"); - differences.Tables[0].Columns.Should().HaveCount(9); - + differences.Tables[0].Target.Columns[10].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Target.Columns[10].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[10].DefaultConstraint.Expression.Should().Be("('Target expression')"); + differences.Tables[0].Target.Columns[10].DefaultConstraint.Name.Should().Be("DF_TableDifference_ColumnWithDefaultConstraint"); + differences.Tables[0].Target.Columns[10].IsComputed.Should().BeFalse(); + differences.Tables[0].Target.Columns[10].IsIdentity.Should().BeFalse(); + differences.Tables[0].Target.Columns[10].IsNullable.Should().BeFalse(); + differences.Tables[0].Target.Columns[10].MaxLength.Should().Be(20); + differences.Tables[0].Target.Columns[10].Name.Should().Be("ColumnWithDefaultConstraint"); + differences.Tables[0].Target.Columns[10].Position.Should().Be(11); + differences.Tables[0].Target.Columns[10].Precision.Should().Be(0); + differences.Tables[0].Target.Columns[10].Scale.Should().Be(0); + differences.Tables[0].Target.Columns[10].TypeName.Should().Be("varchar"); + + differences.Tables[0].Target.Columns[11].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Target.Columns[11].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[11].DefaultConstraint.Should().BeNull(); + differences.Tables[0].Target.Columns[11].IsComputed.Should().BeFalse(); + differences.Tables[0].Target.Columns[11].IsIdentity.Should().BeFalse(); + differences.Tables[0].Target.Columns[11].IsNullable.Should().BeFalse(); + differences.Tables[0].Target.Columns[11].MaxLength.Should().Be(20); + differences.Tables[0].Target.Columns[11].Name.Should().Be("ColumnWithMissingDefaultConstraint"); + differences.Tables[0].Target.Columns[11].Position.Should().Be(12); + differences.Tables[0].Target.Columns[11].Precision.Should().Be(0); + differences.Tables[0].Target.Columns[11].Scale.Should().Be(0); + differences.Tables[0].Target.Columns[11].TypeName.Should().Be("varchar"); + + differences.Tables[0].Target.Columns[12].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[0].Target.Columns[12].ComputedExpression.Should().BeNull(); + differences.Tables[0].Target.Columns[12].DefaultConstraint.Expression.Should().Be("('Same expression')"); + differences.Tables[0].Target.Columns[12].DefaultConstraint.Name.Should().Be("DF_TableDifference_WrongName"); + differences.Tables[0].Target.Columns[12].IsComputed.Should().BeFalse(); + differences.Tables[0].Target.Columns[12].IsIdentity.Should().BeFalse(); + differences.Tables[0].Target.Columns[12].IsNullable.Should().BeFalse(); + differences.Tables[0].Target.Columns[12].MaxLength.Should().Be(20); + differences.Tables[0].Target.Columns[12].Name.Should().Be("ColumnWithOtherDefaultConstraintName"); + differences.Tables[0].Target.Columns[12].Position.Should().Be(13); + differences.Tables[0].Target.Columns[12].Precision.Should().Be(0); + differences.Tables[0].Target.Columns[12].Scale.Should().Be(0); + differences.Tables[0].Target.Columns[12].TypeName.Should().Be("varchar"); + + differences.Tables[0].Columns.Should().HaveCount(12); + + differences.Tables[0].Columns[0].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[0].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[0]); differences.Tables[0].Columns[0].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[0]); differences.Tables[0].Columns[0].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[1].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[1].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[1]); differences.Tables[0].Columns[1].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[1]); differences.Tables[0].Columns[1].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[2].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[2].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[3]); differences.Tables[0].Columns[2].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[2]); differences.Tables[0].Columns[2].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[3].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[3].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[2]); differences.Tables[0].Columns[3].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[3]); differences.Tables[0].Columns[3].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[4].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[4].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[4]); differences.Tables[0].Columns[4].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[4]); differences.Tables[0].Columns[4].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[5].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[5].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[5]); differences.Tables[0].Columns[5].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[5]); differences.Tables[0].Columns[5].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[6].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[6].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[7]); differences.Tables[0].Columns[6].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[7]); differences.Tables[0].Columns[6].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[7].DefaultConstraint.Should().BeNull(); differences.Tables[0].Columns[7].Source.Should().BeNull(); differences.Tables[0].Columns[7].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[8]); differences.Tables[0].Columns[7].Type.Should().Be(SqlObjectDifferenceType.MissingInSource); - differences.Tables[0].Columns[8].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[8]); - differences.Tables[0].Columns[8].Target.Should().BeNull(); - differences.Tables[0].Columns[8].Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); + differences.Tables[0].Columns[8].DefaultConstraint.Source.Should().BeSameAs(differences.Tables[0].Source.Columns[10].DefaultConstraint); + differences.Tables[0].Columns[8].DefaultConstraint.Target.Should().BeSameAs(differences.Tables[0].Target.Columns[10].DefaultConstraint); + differences.Tables[0].Columns[8].DefaultConstraint.Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[8].DefaultConstraint.Properties.Should().HaveCount(1); + differences.Tables[0].Columns[8].DefaultConstraint.Properties[0].Name.Should().Be("Expression"); + differences.Tables[0].Columns[8].DefaultConstraint.Properties[0].Source.Should().Be("('Source expression')"); + differences.Tables[0].Columns[8].DefaultConstraint.Properties[0].Target.Should().Be("('Target expression')"); + differences.Tables[0].Columns[8].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[10]); + differences.Tables[0].Columns[8].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[10]); + differences.Tables[0].Columns[8].Type.Should().Be(SqlObjectDifferenceType.Different); + + differences.Tables[0].Columns[9].DefaultConstraint.Source.Should().BeSameAs(differences.Tables[0].Source.Columns[11].DefaultConstraint); + differences.Tables[0].Columns[9].DefaultConstraint.Target.Should().BeSameAs(differences.Tables[0].Target.Columns[11].DefaultConstraint); + differences.Tables[0].Columns[9].DefaultConstraint.Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); + differences.Tables[0].Columns[9].DefaultConstraint.Properties.Should().BeEmpty(); + differences.Tables[0].Columns[9].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[11]); + differences.Tables[0].Columns[9].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[11]); + differences.Tables[0].Columns[9].Type.Should().Be(SqlObjectDifferenceType.Different); + + differences.Tables[0].Columns[10].DefaultConstraint.Source.Should().BeSameAs(differences.Tables[0].Source.Columns[12].DefaultConstraint); + differences.Tables[0].Columns[10].DefaultConstraint.Target.Should().BeSameAs(differences.Tables[0].Target.Columns[12].DefaultConstraint); + differences.Tables[0].Columns[10].DefaultConstraint.Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].Columns[10].DefaultConstraint.Properties.Should().HaveCount(1); + differences.Tables[0].Columns[10].DefaultConstraint.Properties[0].Name.Should().Be("Name"); + differences.Tables[0].Columns[10].DefaultConstraint.Properties[0].Source.Should().Be("DF_TableDifference_ColumnWithOtherDefaultConstraintName"); + differences.Tables[0].Columns[10].DefaultConstraint.Properties[0].Target.Should().Be("DF_TableDifference_WrongName"); + differences.Tables[0].Columns[10].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[12]); + differences.Tables[0].Columns[10].Target.Should().BeSameAs(differences.Tables[0].Target.Columns[12]); + differences.Tables[0].Columns[10].Type.Should().Be(SqlObjectDifferenceType.Different); + + differences.Tables[0].Columns[11].DefaultConstraint.Should().BeNull(); + differences.Tables[0].Columns[11].Source.Should().BeSameAs(differences.Tables[0].Source.Columns[8]); + differences.Tables[0].Columns[11].Target.Should().BeNull(); + differences.Tables[0].Columns[11].Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); // Tables / Foreign keys differences.Tables[0].Source.ForeignKeys.Should().HaveCount(1); From cdd7713e25b12c2121c9d7abd7678619788384be Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 5 Sep 2025 15:46:20 +0200 Subject: [PATCH 9/9] The SqlTableDifferences use a primary key object instance of a list to represents the difference of primary key (fixes #8). --- ...lDatabaseComparisonResultsTextGenerator.cs | 2 +- .../Comparer/SqlObjectComparer.cs | 20 +- .../Comparer/SqlTableDifferences.cs | 6 +- .../Tables/TableWithDifferentPrimaryKey.sql | 6 + ...g.Databases.SqlServer.Tests.Source.sqlproj | 1 + .../Tables/TableWithDifferentPrimaryKey.sql | 6 + ...g.Databases.SqlServer.Tests.Target.sqlproj | 1 + ...erverDatabaseComparerTest.CompareAsync.txt | 11 +- .../SqlServerDatabaseComparerTest.cs | 217 +++++++++++++----- 9 files changed, 192 insertions(+), 78 deletions(-) create mode 100644 tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableWithDifferentPrimaryKey.sql create mode 100644 tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableWithDifferentPrimaryKey.sql diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs index 67656fe..f237194 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs @@ -105,7 +105,7 @@ public void Visit(SqlTableDifferences differences) this.Generate(differences.Indexes, "Indexes"); - this.Generate(differences.PrimaryKeys, "Primary keys"); + this.Generate(differences.PrimaryKey, "Primary key"); this.Generate(differences.Triggers, "Triggers"); diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs index b68a284..35abcf8 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs @@ -211,7 +211,7 @@ public static IList Compare(IReadOnlyList source, var indexesDifferences = Compare(sourceTable.Indexes, table.Indexes, i => i.Name, diff => new SqlIndexDifferences(diff)); // Compare the primary key - var primaryKeyDifferences = Compare(CreateArray(sourceTable.PrimaryKey), CreateArray(table.PrimaryKey), pk => pk.Name, diff => new SqlPrimaryKeyDifferences(diff)); + var primaryKeyDifferences = (SqlPrimaryKeyDifferences?)Compare(sourceTable.PrimaryKey, table.PrimaryKey); // Compare the triggers var triggersDifferences = Compare(sourceTable.Triggers, table.Triggers, tr => tr.Name); @@ -219,9 +219,12 @@ public static IList Compare(IReadOnlyList source, // Compare the unique constraints var uniqueConstraintsDifferences = Compare(sourceTable.UniqueConstraints, table.UniqueConstraints, uc => uc.Name, diff => new SqlUniqueConstraintDifferences(diff)); - if (columnsDifferences.Count + triggersDifferences.Count + checkConstraintDifferences.Count + indexesDifferences.Count + foreignKeysDifferences.Count + uniqueConstraintsDifferences.Count + primaryKeyDifferences.Count > 0) + if (columnsDifferences.Count + triggersDifferences.Count + checkConstraintDifferences.Count + indexesDifferences.Count + foreignKeysDifferences.Count + uniqueConstraintsDifferences.Count > 0 || primaryKeyDifferences is not null) { - return new SqlTableDifferences(sourceTable, table, SqlObjectDifferenceType.Different, [], primaryKeyDifferences, columnsDifferences, triggersDifferences, checkConstraintDifferences, indexesDifferences, foreignKeysDifferences, uniqueConstraintsDifferences); + return new SqlTableDifferences(sourceTable, table, SqlObjectDifferenceType.Different, [], columnsDifferences, triggersDifferences, checkConstraintDifferences, indexesDifferences, foreignKeysDifferences, uniqueConstraintsDifferences) + { + PrimaryKey = primaryKeyDifferences, + }; } return this.CreateDifferences(table); @@ -328,17 +331,6 @@ private static IReadOnlyList GetPropertyDifferences return objects.SingleOrDefault(o => Equals(keySelector(o), value)); } - private static T[] CreateArray(T? value) - where T : class - { - if (value is null) - { - return []; - } - - return [value]; - } - private SqlObjectPropertyDifference? CompareProperty(TSqlObject target, Func propertyValueForComparison, string name, Func? propertyValueToDisplay = null) where TSqlObject : SqlObject { diff --git a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs index 19d69f4..2b7a1ee 100644 --- a/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs +++ b/src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs @@ -18,7 +18,6 @@ internal SqlTableDifferences( SqlTable? target, SqlObjectDifferenceType type, IReadOnlyList? properties, - IList primaryKeys, IList columns, IList> triggers, IList> checkConstraints, @@ -27,7 +26,6 @@ internal SqlTableDifferences( IList uniqueConstraints) : base(source, target, type, properties) { - this.PrimaryKeys = new ReadOnlyCollection(primaryKeys); this.Columns = new ReadOnlyCollection(columns); this.Triggers = new ReadOnlyCollection>(triggers); this.CheckConstraints = new ReadOnlyCollection>(checkConstraints); @@ -38,7 +36,7 @@ internal SqlTableDifferences( internal SqlTableDifferences( SqlObjectDifferences differences) - : this(differences.Source, differences.Target, differences.Type, differences.Properties, [], [], [], [], [], [], []) + : this(differences.Source, differences.Target, differences.Type, differences.Properties, [], [], [], [], [], []) { } @@ -60,7 +58,7 @@ internal SqlTableDifferences( /// /// Gets the primary key differences between the two SQL tables. /// - public ReadOnlyCollection PrimaryKeys { get; } + public SqlPrimaryKeyDifferences? PrimaryKey { get; internal set; } /// /// Gets the foreign keys differences between the two SQL tables. diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableWithDifferentPrimaryKey.sql b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableWithDifferentPrimaryKey.sql new file mode 100644 index 0000000..1dbe63a --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableWithDifferentPrimaryKey.sql @@ -0,0 +1,6 @@ +CREATE TABLE [dbo].[TableWithDifferentPrimaryKey] +( + [Id] INT NOT NULL, + + CONSTRAINT [PK_TableWithDifferentPrimaryKey_Source] PRIMARY KEY CLUSTERED ([Id] ASC) +) diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj index 05104a8..b17888a 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj @@ -101,5 +101,6 @@ + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableWithDifferentPrimaryKey.sql b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableWithDifferentPrimaryKey.sql new file mode 100644 index 0000000..fdc64c8 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Tables/TableWithDifferentPrimaryKey.sql @@ -0,0 +1,6 @@ +CREATE TABLE [dbo].[TableWithDifferentPrimaryKey] +( + [Id] INT NOT NULL, + + CONSTRAINT [PK_TableWithDifferentPrimaryKey_Target] PRIMARY KEY CLUSTERED ([Id] ASC) +) diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj index 8728546..6c3b10a 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj @@ -115,5 +115,6 @@ + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt index 9883303..f4378c9 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt @@ -114,7 +114,7 @@ * Position: Source: 1 Target: 2 - ------ Primary keys ------ + ------ Primary key ------ - PrimaryKeyDifference * Type: Source: NONCLUSTERED @@ -165,6 +165,15 @@ Source: 1 Target: 2 - dbo.TableTarget (Missing in the source) +- dbo.TableWithDifferentPrimaryKey + ------ Indexes ------ + - PK_TableWithDifferentPrimaryKey_Target (Missing in the source) + - PK_TableWithDifferentPrimaryKey_Source (Missing in the target) + ------ Primary key ------ + - PK_TableWithDifferentPrimaryKey_Source + * Name: + Source: PK_TableWithDifferentPrimaryKey_Source + Target: PK_TableWithDifferentPrimaryKey_Target - dbo.TableSource (Missing in the target) ------ Stored procedures ------ - dbo.StoredProcedureDifference diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index 0578d8d..4924451 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -52,7 +52,7 @@ public async Task CompareAsync() differences.StoredProcedures[2].Target.Should().BeNull(); // Tables - differences.Tables.Should().HaveCount(3); + differences.Tables.Should().HaveCount(4); differences.Tables[0].Source.Name.Should().Be("TableDifference"); differences.Tables[0].Source.Schema.Should().Be("dbo"); @@ -677,22 +677,20 @@ public async Task CompareAsync() differences.Tables[0].Target.PrimaryKey.Columns[1].Name.Should().Be("Type"); differences.Tables[0].Target.PrimaryKey.Columns[1].Position.Should().Be(2); - differences.Tables[0].PrimaryKeys.Should().HaveCount(1); - differences.Tables[0].PrimaryKeys[0].Columns.Should().HaveCount(2); - differences.Tables[0].PrimaryKeys[0].Columns[0].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[1]); - differences.Tables[0].PrimaryKeys[0].Columns[0].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[0]); - differences.Tables[0].PrimaryKeys[0].Columns[0].Type.Should().Be(SqlObjectDifferenceType.Different); - differences.Tables[0].PrimaryKeys[0].Columns[1].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[0]); - differences.Tables[0].PrimaryKeys[0].Columns[1].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[1]); - differences.Tables[0].PrimaryKeys[0].Columns[1].Type.Should().Be(SqlObjectDifferenceType.Different); - - differences.Tables[0].PrimaryKeys[0].Properties.Should().HaveCount(1); - differences.Tables[0].PrimaryKeys[0].Properties[0].Name.Should().Be("Type"); - differences.Tables[0].PrimaryKeys[0].Properties[0].Source.Should().Be("NONCLUSTERED"); - differences.Tables[0].PrimaryKeys[0].Properties[0].Target.Should().Be("CLUSTERED"); - differences.Tables[0].PrimaryKeys[0].Source.Should().Be(differences.Tables[0].Source.PrimaryKey); - differences.Tables[0].PrimaryKeys[0].Target.Should().Be(differences.Tables[0].Target.PrimaryKey); - differences.Tables[0].PrimaryKeys[0].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].PrimaryKey.Columns.Should().HaveCount(2); + differences.Tables[0].PrimaryKey.Columns[0].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[1]); + differences.Tables[0].PrimaryKey.Columns[0].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[0]); + differences.Tables[0].PrimaryKey.Columns[0].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].PrimaryKey.Columns[1].Source.Should().BeSameAs(differences.Tables[0].Source.PrimaryKey.Columns[0]); + differences.Tables[0].PrimaryKey.Columns[1].Target.Should().BeSameAs(differences.Tables[0].Target.PrimaryKey.Columns[1]); + differences.Tables[0].PrimaryKey.Columns[1].Type.Should().Be(SqlObjectDifferenceType.Different); + differences.Tables[0].PrimaryKey.Properties.Should().HaveCount(1); + differences.Tables[0].PrimaryKey.Properties[0].Name.Should().Be("Type"); + differences.Tables[0].PrimaryKey.Properties[0].Source.Should().Be("NONCLUSTERED"); + differences.Tables[0].PrimaryKey.Properties[0].Target.Should().Be("CLUSTERED"); + differences.Tables[0].PrimaryKey.Source.Should().Be(differences.Tables[0].Source.PrimaryKey); + differences.Tables[0].PrimaryKey.Target.Should().Be(differences.Tables[0].Target.PrimaryKey); + differences.Tables[0].PrimaryKey.Type.Should().Be(SqlObjectDifferenceType.Different); // Tables / Triggers differences.Tables[0].Source.Triggers.Should().HaveCount(1); @@ -750,7 +748,7 @@ public async Task CompareAsync() // Missing tables differences.Tables[1].Columns.Should().BeEmpty(); differences.Tables[1].Indexes.Should().BeEmpty(); - differences.Tables[1].PrimaryKeys.Should().BeEmpty(); + differences.Tables[1].PrimaryKey.Should().BeNull(); differences.Tables[1].Source.Should().BeNull(); differences.Tables[1].UniqueConstraints.Should().BeEmpty(); differences.Tables[1].Target.CheckConstraints.Should().HaveCount(1); @@ -768,6 +766,28 @@ public async Task CompareAsync() differences.Tables[1].Target.Columns[0].Precision.Should().Be(10); differences.Tables[1].Target.Columns[0].Scale.Should().Be(0); differences.Tables[1].Target.Columns[0].TypeName.Should().Be("int"); + differences.Tables[1].Target.Columns[1].CollationName.Should().Be("SQL_Latin1_General_CP1_CI_AS"); + differences.Tables[1].Target.Columns[1].ComputedExpression.Should().BeNull(); + differences.Tables[1].Target.Columns[1].IsComputed.Should().BeFalse(); + differences.Tables[1].Target.Columns[1].IsIdentity.Should().BeFalse(); + differences.Tables[1].Target.Columns[1].IsNullable.Should().BeTrue(); + differences.Tables[1].Target.Columns[1].MaxLength.Should().Be(50); + differences.Tables[1].Target.Columns[1].Name.Should().Be("TargetName"); + differences.Tables[1].Target.Columns[1].Position.Should().Be(2); + differences.Tables[1].Target.Columns[1].Precision.Should().Be(0); + differences.Tables[1].Target.Columns[1].Scale.Should().Be(0); + differences.Tables[1].Target.Columns[1].TypeName.Should().Be("varchar"); + differences.Tables[1].Target.Columns[2].CollationName.Should().BeNull(); + differences.Tables[1].Target.Columns[2].ComputedExpression.Should().BeNull(); + differences.Tables[1].Target.Columns[2].IsComputed.Should().BeFalse(); + differences.Tables[1].Target.Columns[2].IsIdentity.Should().BeFalse(); + differences.Tables[1].Target.Columns[2].IsNullable.Should().BeFalse(); + differences.Tables[1].Target.Columns[2].MaxLength.Should().Be(4); + differences.Tables[1].Target.Columns[2].Name.Should().Be("TargetForeignKeyId"); + differences.Tables[1].Target.Columns[2].Position.Should().Be(3); + differences.Tables[1].Target.Columns[2].Precision.Should().Be(10); + differences.Tables[1].Target.Columns[2].Scale.Should().Be(0); + differences.Tables[1].Target.Columns[2].TypeName.Should().Be("int"); differences.Tables[1].Target.ForeignKeys.Should().HaveCount(1); differences.Tables[1].Target.ForeignKeys[0].Columns.Should().HaveCount(1); differences.Tables[1].Target.ForeignKeys[0].Columns[0].Name.Should().Be("TargetForeignKeyId"); @@ -809,13 +829,28 @@ public async Task CompareAsync() differences.Tables[1].Type.Should().Be(SqlObjectDifferenceType.MissingInSource); differences.Tables[2].Columns.Should().BeEmpty(); - differences.Tables[2].Indexes.Should().BeEmpty(); - differences.Tables[2].PrimaryKeys.Should().BeEmpty(); + differences.Tables[2].Indexes.Should().HaveCount(2); + differences.Tables[2].Indexes[0].Columns.Should().BeEmpty(); + differences.Tables[2].Indexes[0].IncludedColumns.Should().BeEmpty(); + differences.Tables[2].Indexes[0].Properties.Should().BeEmpty(); + differences.Tables[2].Indexes[0].Source.Should().BeNull(); + differences.Tables[2].Indexes[0].Target.Should().BeSameAs(differences.Tables[2].Target.Indexes[0]); + differences.Tables[2].Indexes[0].Type.Should().Be(SqlObjectDifferenceType.MissingInSource); + differences.Tables[2].Indexes[1].Columns.Should().BeEmpty(); + differences.Tables[2].Indexes[1].IncludedColumns.Should().BeEmpty(); + differences.Tables[2].Indexes[1].Properties.Should().BeEmpty(); + differences.Tables[2].Indexes[1].Source.Should().BeSameAs(differences.Tables[2].Source.Indexes[0]); + differences.Tables[2].Indexes[1].Target.Should().BeNull(); + differences.Tables[2].Indexes[1].Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); + differences.Tables[2].PrimaryKey.Source.Should().BeSameAs(differences.Tables[2].Source.PrimaryKey); + differences.Tables[2].PrimaryKey.Target.Should().BeSameAs(differences.Tables[2].Target.PrimaryKey); + differences.Tables[2].PrimaryKey.Properties.Should().HaveCount(1); + differences.Tables[2].PrimaryKey.Properties[0].Name.Should().Be("Name"); + differences.Tables[2].PrimaryKey.Properties[0].Source.Should().Be("PK_TableWithDifferentPrimaryKey_Source"); + differences.Tables[2].PrimaryKey.Properties[0].Target.Should().Be("PK_TableWithDifferentPrimaryKey_Target"); differences.Tables[2].UniqueConstraints.Should().BeEmpty(); - differences.Tables[2].Source.CheckConstraints.Should().HaveCount(1); - differences.Tables[2].Source.CheckConstraints[0].Name.Should().Be("CheckConstraintSource"); - differences.Tables[2].Source.CheckConstraints[0].Code.Should().Be("([Id]>(0))"); - differences.Tables[2].Source.Columns.Should().HaveCount(3); + differences.Tables[2].Source.CheckConstraints.Should().BeEmpty(); + differences.Tables[2].Source.Columns.Should().HaveCount(1); differences.Tables[2].Source.Columns[0].CollationName.Should().BeNull(); differences.Tables[2].Source.Columns[0].ComputedExpression.Should().BeNull(); differences.Tables[2].Source.Columns[0].IsComputed.Should().BeFalse(); @@ -827,45 +862,111 @@ public async Task CompareAsync() differences.Tables[2].Source.Columns[0].Precision.Should().Be(10); differences.Tables[2].Source.Columns[0].Scale.Should().Be(0); differences.Tables[2].Source.Columns[0].TypeName.Should().Be("int"); - differences.Tables[2].Source.Indexes.Should().HaveCount(2); + differences.Tables[2].Source.Indexes.Should().HaveCount(1); differences.Tables[2].Source.Indexes[0].Columns.Should().HaveCount(1); - differences.Tables[2].Source.Indexes[0].Columns[0].Name.Should().Be("SourceName"); + differences.Tables[2].Source.Indexes[0].Columns[0].Name.Should().Be("Id"); differences.Tables[2].Source.Indexes[0].Columns[0].Position.Should().Be(1); - differences.Tables[2].Source.Indexes[0].Filter.Should().Be("([SourceName]='')"); - differences.Tables[2].Source.Indexes[0].IncludedColumns.Should().HaveCount(1); - differences.Tables[2].Source.Indexes[0].IncludedColumns[0].Name.Should().Be("SourceForeignKeyId"); - differences.Tables[2].Source.Indexes[0].IncludedColumns[0].Position.Should().Be(1); - differences.Tables[2].Source.Indexes[0].IsUnique.Should().BeFalse(); - differences.Tables[2].Source.Indexes[0].Name.Should().Be("IndexSource"); - differences.Tables[2].Source.Indexes[1].Columns.Should().HaveCount(1); - differences.Tables[2].Source.Indexes[1].Columns[0].Name.Should().Be("Id"); - differences.Tables[2].Source.Indexes[1].Columns[0].Position.Should().Be(1); - differences.Tables[2].Source.Indexes[1].Filter.Should().BeNull(); - differences.Tables[2].Source.Indexes[1].IncludedColumns.Should().HaveCount(0); - differences.Tables[2].Source.ForeignKeys.Should().HaveCount(1); - differences.Tables[2].Source.ForeignKeys[0].Columns.Should().HaveCount(1); - differences.Tables[2].Source.ForeignKeys[0].Columns[0].Name.Should().Be("SourceForeignKeyId"); - differences.Tables[2].Source.ForeignKeys[0].Columns[0].Position.Should().Be(1); - differences.Tables[2].Source.ForeignKeys[0].DeleteAction.Should().Be("NO_ACTION"); - differences.Tables[2].Source.ForeignKeys[0].Name.Should().Be("ForeignKeySource"); - differences.Tables[2].Source.ForeignKeys[0].ReferencedTable.Should().Be("ReferencedTable"); - differences.Tables[2].Source.ForeignKeys[0].UpdateAction.Should().Be("NO_ACTION"); - differences.Tables[2].Source.Name.Should().Be("TableSource"); + differences.Tables[2].Source.Indexes[0].Filter.Should().BeNull(); + differences.Tables[2].Source.Indexes[0].IncludedColumns.Should().BeEmpty(); + differences.Tables[2].Source.Indexes[0].IsUnique.Should().BeTrue(); + differences.Tables[2].Source.Indexes[0].Name.Should().Be("PK_TableWithDifferentPrimaryKey_Source"); + differences.Tables[2].Source.ForeignKeys.Should().BeEmpty(); + differences.Tables[2].Source.Name.Should().Be("TableWithDifferentPrimaryKey"); differences.Tables[2].Source.Schema.Should().Be("dbo"); - differences.Tables[2].Source.PrimaryKey.Name.Should().Be("PrimaryKeySource"); + differences.Tables[2].Source.PrimaryKey.Name.Should().Be("PK_TableWithDifferentPrimaryKey_Source"); differences.Tables[2].Source.PrimaryKey.Type.Should().Be("CLUSTERED"); - differences.Tables[2].Source.Triggers.Should().HaveCount(1); - differences.Tables[2].Source.Triggers[0].Name.Should().Be("TriggerSource"); - differences.Tables[2].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerSource]\r\n\tON [dbo].[TableSource]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND"); - differences.Tables[2].Source.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); - differences.Tables[2].Source.UniqueConstraints.Should().HaveCount(1); - differences.Tables[2].Source.UniqueConstraints[0].Columns.Should().HaveCount(1); - differences.Tables[2].Source.UniqueConstraints[0].Columns[0].Name.Should().Be("Id"); - differences.Tables[2].Source.UniqueConstraints[0].Columns[0].Position.Should().Be(1); - differences.Tables[2].Source.UniqueConstraints[0].Name.Should().Be("UniqueConstraintSource"); - differences.Tables[2].Source.UniqueConstraints[0].Type.Should().Be("NONCLUSTERED"); + differences.Tables[2].Source.Triggers.Should().BeEmpty(); + differences.Tables[2].Source.UniqueConstraints.Should().BeEmpty(); + differences.Tables[2].Target.CheckConstraints.Should().BeEmpty(); + differences.Tables[2].Target.Columns.Should().HaveCount(1); + differences.Tables[2].Target.Columns[0].CollationName.Should().BeNull(); + differences.Tables[2].Target.Columns[0].ComputedExpression.Should().BeNull(); + differences.Tables[2].Target.Columns[0].IsComputed.Should().BeFalse(); + differences.Tables[2].Target.Columns[0].IsIdentity.Should().BeFalse(); + differences.Tables[2].Target.Columns[0].IsNullable.Should().BeFalse(); + differences.Tables[2].Target.Columns[0].MaxLength.Should().Be(4); + differences.Tables[2].Target.Columns[0].Name.Should().Be("Id"); + differences.Tables[2].Target.Columns[0].Position.Should().Be(1); + differences.Tables[2].Target.Columns[0].Precision.Should().Be(10); + differences.Tables[2].Target.Columns[0].Scale.Should().Be(0); + differences.Tables[2].Target.Columns[0].TypeName.Should().Be("int"); + differences.Tables[2].Target.Indexes.Should().HaveCount(1); + differences.Tables[2].Target.Indexes[0].Columns.Should().HaveCount(1); + differences.Tables[2].Target.Indexes[0].Columns[0].Name.Should().Be("Id"); + differences.Tables[2].Target.Indexes[0].Columns[0].Position.Should().Be(1); + differences.Tables[2].Target.Indexes[0].Filter.Should().BeNull(); + differences.Tables[2].Target.Indexes[0].IncludedColumns.Should().BeEmpty(); + differences.Tables[2].Target.Indexes[0].IsUnique.Should().BeTrue(); + differences.Tables[2].Target.Indexes[0].Name.Should().Be("PK_TableWithDifferentPrimaryKey_Target"); + differences.Tables[2].Target.ForeignKeys.Should().BeEmpty(); + differences.Tables[2].Target.Name.Should().Be("TableWithDifferentPrimaryKey"); + differences.Tables[2].Target.Schema.Should().Be("dbo"); + differences.Tables[2].Target.PrimaryKey.Name.Should().Be("PK_TableWithDifferentPrimaryKey_Target"); + differences.Tables[2].Target.PrimaryKey.Type.Should().Be("CLUSTERED"); + differences.Tables[2].Target.Triggers.Should().BeEmpty(); + differences.Tables[2].Target.UniqueConstraints.Should().BeEmpty(); differences.Tables[2].Triggers.Should().BeEmpty(); - differences.Tables[2].Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); + differences.Tables[2].Type.Should().Be(SqlObjectDifferenceType.Different); + + differences.Tables[3].Columns.Should().BeEmpty(); + differences.Tables[3].Indexes.Should().BeEmpty(); + differences.Tables[3].PrimaryKey.Should().BeNull(); + differences.Tables[3].UniqueConstraints.Should().BeEmpty(); + differences.Tables[3].Source.CheckConstraints.Should().HaveCount(1); + differences.Tables[3].Source.CheckConstraints[0].Name.Should().Be("CheckConstraintSource"); + differences.Tables[3].Source.CheckConstraints[0].Code.Should().Be("([Id]>(0))"); + differences.Tables[3].Source.Columns.Should().HaveCount(3); + differences.Tables[3].Source.Columns[0].CollationName.Should().BeNull(); + differences.Tables[3].Source.Columns[0].ComputedExpression.Should().BeNull(); + differences.Tables[3].Source.Columns[0].IsComputed.Should().BeFalse(); + differences.Tables[3].Source.Columns[0].IsIdentity.Should().BeFalse(); + differences.Tables[3].Source.Columns[0].IsNullable.Should().BeFalse(); + differences.Tables[3].Source.Columns[0].MaxLength.Should().Be(4); + differences.Tables[3].Source.Columns[0].Name.Should().Be("Id"); + differences.Tables[3].Source.Columns[0].Position.Should().Be(1); + differences.Tables[3].Source.Columns[0].Precision.Should().Be(10); + differences.Tables[3].Source.Columns[0].Scale.Should().Be(0); + differences.Tables[3].Source.Columns[0].TypeName.Should().Be("int"); + differences.Tables[3].Source.Indexes.Should().HaveCount(2); + differences.Tables[3].Source.Indexes[0].Columns.Should().HaveCount(1); + differences.Tables[3].Source.Indexes[0].Columns[0].Name.Should().Be("SourceName"); + differences.Tables[3].Source.Indexes[0].Columns[0].Position.Should().Be(1); + differences.Tables[3].Source.Indexes[0].Filter.Should().Be("([SourceName]='')"); + differences.Tables[3].Source.Indexes[0].IncludedColumns.Should().HaveCount(1); + differences.Tables[3].Source.Indexes[0].IncludedColumns[0].Name.Should().Be("SourceForeignKeyId"); + differences.Tables[3].Source.Indexes[0].IncludedColumns[0].Position.Should().Be(1); + differences.Tables[3].Source.Indexes[0].IsUnique.Should().BeFalse(); + differences.Tables[3].Source.Indexes[0].Name.Should().Be("IndexSource"); + differences.Tables[3].Source.Indexes[1].Columns.Should().HaveCount(1); + differences.Tables[3].Source.Indexes[1].Columns[0].Name.Should().Be("Id"); + differences.Tables[3].Source.Indexes[1].Columns[0].Position.Should().Be(1); + differences.Tables[3].Source.Indexes[1].Filter.Should().BeNull(); + differences.Tables[3].Source.Indexes[1].IncludedColumns.Should().HaveCount(0); + differences.Tables[3].Source.ForeignKeys.Should().HaveCount(1); + differences.Tables[3].Source.ForeignKeys[0].Columns.Should().HaveCount(1); + differences.Tables[3].Source.ForeignKeys[0].Columns[0].Name.Should().Be("SourceForeignKeyId"); + differences.Tables[3].Source.ForeignKeys[0].Columns[0].Position.Should().Be(1); + differences.Tables[3].Source.ForeignKeys[0].DeleteAction.Should().Be("NO_ACTION"); + differences.Tables[3].Source.ForeignKeys[0].Name.Should().Be("ForeignKeySource"); + differences.Tables[3].Source.ForeignKeys[0].ReferencedTable.Should().Be("ReferencedTable"); + differences.Tables[3].Source.ForeignKeys[0].UpdateAction.Should().Be("NO_ACTION"); + differences.Tables[3].Source.Name.Should().Be("TableSource"); + differences.Tables[3].Source.Schema.Should().Be("dbo"); + differences.Tables[3].Source.PrimaryKey.Name.Should().Be("PrimaryKeySource"); + differences.Tables[3].Source.PrimaryKey.Type.Should().Be("CLUSTERED"); + differences.Tables[3].Source.Triggers.Should().HaveCount(1); + differences.Tables[3].Source.Triggers[0].Name.Should().Be("TriggerSource"); + differences.Tables[3].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerSource]\r\n\tON [dbo].[TableSource]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND"); + differences.Tables[3].Source.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); + differences.Tables[3].Source.UniqueConstraints.Should().HaveCount(1); + differences.Tables[3].Source.UniqueConstraints[0].Columns.Should().HaveCount(1); + differences.Tables[3].Source.UniqueConstraints[0].Columns[0].Name.Should().Be("Id"); + differences.Tables[3].Source.UniqueConstraints[0].Columns[0].Position.Should().Be(1); + differences.Tables[3].Source.UniqueConstraints[0].Name.Should().Be("UniqueConstraintSource"); + differences.Tables[3].Source.UniqueConstraints[0].Type.Should().Be("NONCLUSTERED"); + differences.Tables[3].Target.Should().BeNull(); + differences.Tables[3].Triggers.Should().BeEmpty(); + differences.Tables[3].Type.Should().Be(SqlObjectDifferenceType.MissingInTarget); // UserTypes differences.UserTypes.Should().HaveCount(3);