From caa7303140839015a23753f5e52d507ff70d9b1d Mon Sep 17 00:00:00 2001 From: MD-V Date: Fri, 6 Sep 2024 13:17:47 +0200 Subject: [PATCH] Add IBM DB2 DbAdapter, add IBM DB2 DbAdapter unit tests, add IBM DB2 to Docker compose file --- .gitignore | 3 + Respawn.DatabaseTests/DB2Tests.cs | 521 ++++++++++++++++++++++++++++++ Respawn/DB2DbAdapter.cs | 224 +++++++++++++ Respawn/DbAdapter.cs | 1 + docker-compose.yml | 20 ++ 5 files changed, 769 insertions(+) create mode 100644 Respawn.DatabaseTests/DB2Tests.cs create mode 100644 Respawn/DB2DbAdapter.cs diff --git a/.gitignore b/.gitignore index 55cd97a..aeffcbc 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,6 @@ tools/roundhouse/output/ # Informix files informix-server/wl* + +# DB2 files +db2-server/* \ No newline at end of file diff --git a/Respawn.DatabaseTests/DB2Tests.cs b/Respawn.DatabaseTests/DB2Tests.cs new file mode 100644 index 0000000..c1896ac --- /dev/null +++ b/Respawn.DatabaseTests/DB2Tests.cs @@ -0,0 +1,521 @@ +using IBM.Data.DB2.Core; +using Shouldly; +using System; +using System.Threading.Tasks; +using NPoco; +using Respawn.Graph; +using Xunit; +using Xunit.Abstractions; + +namespace Respawn.DatabaseTests +{ + public class DB2Tests : IAsyncLifetime + { + private DB2Connection _connection; + private readonly ITestOutputHelper _output; + + private const string _connectionString = "Server=127.0.0.1:50000;Database=SAMPLEDB;UID=db2inst1;PWD=password;Persist Security Info=True;Authentication=Server;"; + + public DB2Tests(ITestOutputHelper output) + { + _output = output; + } + + public Task DisposeAsync() + { + _connection?.Close(); + _connection?.Dispose(); + _connection = null; + return Task.FromResult(0); + } + + public async Task InitializeAsync() + { + _connection = new DB2Connection(_connectionString); + + await _connection.OpenAsync(); + } + + [SkipOnCI] + public async Task ShouldDeleteData() + { + await using var command = new DB2Command("DROP TABLE IF EXISTS Foo; CREATE TABLE Foo (Value INT);", _connection); + + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO Foo VALUES (?)"; + + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value", i)); + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(100); + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" } + }); + await checkPoint.ResetAsync(_connection); + + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS Foo;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldIgnoreTables() + { + await using var command = new DB2Command("DROP TABLE IF EXISTS Foo; CREATE TABLE Foo (Value INT);", _connection); + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS Bar; CREATE TABLE Bar (Value INT);"; + command.ExecuteNonQuery(); + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value", i)); + command.CommandText = "INSERT INTO Foo VALUES (?);"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO Bar VALUES (?);"; + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions() + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" }, + TablesToIgnore = new Table[] { "Foo" } + }); + await checkPoint.ResetAsync(_connection); + + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(100); + command.CommandText = "SELECT COUNT(1) FROM Bar"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS Foo;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS Bar;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldHandleRelationships() + { + await using var command = new DB2Command("DROP TABLE IF EXISTS Foo; CREATE TABLE Foo (Value INT NOT NULL PRIMARY KEY);", _connection); + command.ExecuteNonQuery(); + command.CommandText = @"DROP TABLE IF EXISTS Bar; + CREATE TABLE Bar ( + Value INT, + FooValue INT, + FOREIGN KEY (FooValue) REFERENCES Foo(Value) + );"; + command.ExecuteNonQuery(); + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value1", i)); + command.Parameters.Add(new DB2Parameter("Value2", i)); + command.CommandText = "INSERT INTO Foo VALUES (?);"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO Bar VALUES (?, ?);"; + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(100); + command.CommandText = "SELECT COUNT(1) FROM Bar"; + command.ExecuteScalar().ShouldBe(100); + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM Bar"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS Foo;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS Bar;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldHandleCircularRelationships() + { + await using var command = new DB2Command(@"DROP TABLE IF EXISTS Parent; + CREATE TABLE Parent ( + Id INT NOT NULL PRIMARY KEY, + ChildId INT NULL + );", _connection); + command.ExecuteNonQuery(); + command.CommandText = @"DROP TABLE IF EXISTS Child; + CREATE TABLE Child ( + Id INT NOT NULL PRIMARY KEY, + ParentId INT NULL + );"; + command.ExecuteNonQuery(); + command.CommandText = @"ALTER TABLE Parent ADD CONSTRAINT FK_Child FOREIGN KEY (ChildId) REFERENCES Child (Id)"; + command.ExecuteNonQuery(); + command.CommandText = @"ALTER TABLE Child ADD CONSTRAINT FK_Parent FOREIGN KEY (ParentId) REFERENCES Parent (Id)"; + command.ExecuteNonQuery(); + + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value1", i)); + command.Parameters.Add(new DB2Parameter("Value2", DBNull.Value)); + + command.CommandText = "INSERT INTO Parent VALUES (?, ?);"; + command.ExecuteNonQuery(); + + command.CommandText = "INSERT INTO Child VALUES (?, ?);"; + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + command.CommandText = @"UPDATE Parent SET ChildId = 0"; + command.ExecuteNonQuery(); + command.CommandText = @"UPDATE Child SET ParentId = 1"; + command.ExecuteNonQuery(); + + command.CommandText = "SELECT COUNT(1) FROM Parent"; + command.ExecuteScalar().ShouldBe(100); + command.CommandText = "SELECT COUNT(1) FROM Child"; + command.ExecuteScalar().ShouldBe(100); + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + command.CommandText = "SELECT COUNT(1) FROM Parent"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM Child"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS Parent;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS Child;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldHandleSelfRelationships() + { + await using var command = new DB2Command(@"DROP TABLE IF EXISTS Foo; + CREATE TABLE Foo ( + Id INT NOT NULL PRIMARY KEY, + ParentId INT NULL + );", _connection); + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE Foo ADD CONSTRAINT FK_Parent1 FOREIGN KEY (ParentId) REFERENCES Foo (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO Foo (Id) VALUES (?)"; + command.Parameters.Add(new DB2Parameter("Value", 1)); + command.ExecuteNonQuery(); + command.Parameters.Clear(); + + for (int i = 1; i < 100; i++) + { + command.CommandText = "INSERT INTO Foo VALUES (?, ?)"; + command.Parameters.Add(new DB2Parameter("Value1", i + 1)); + command.Parameters.Add(new DB2Parameter("Value2", i)); + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(100); + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + command.CommandText = "SELECT COUNT(1) FROM Foo"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS Foo;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldHandleComplexCycles() + { + await using var command = new DB2Command("DROP TABLE IF EXISTS A; CREATE TABLE A (Id INT NOT NULL PRIMARY KEY, B_Id INT NULL)", _connection); + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS B; CREATE TABLE B (Id INT NOT NULL PRIMARY KEY, A_Id INT NULL, C_Id INT NULL, D_Id INT NULL)"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS C; CREATE TABLE C (Id INT NOT NULL PRIMARY KEY, D_Id INT NULL)"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS D; CREATE TABLE D (Id INT NOT NULL PRIMARY KEY)"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS E; CREATE TABLE E (Id INT NOT NULL PRIMARY KEY, A_Id INT NULL)"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS F; CREATE TABLE F (Id INT NOT NULL PRIMARY KEY, B_Id INT NULL)"; + command.ExecuteNonQuery(); + + command.CommandText = "ALTER TABLE A ADD CONSTRAINT FK_A_B FOREIGN KEY (B_Id) REFERENCES B (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE B ADD CONSTRAINT FK_B_A FOREIGN KEY (A_Id) REFERENCES A (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE B ADD CONSTRAINT FK_B_C FOREIGN KEY (C_Id) REFERENCES C (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE B ADD CONSTRAINT FK_B_D FOREIGN KEY (D_Id) REFERENCES D (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE C ADD CONSTRAINT FK_C_D FOREIGN KEY (D_Id) REFERENCES D (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE E ADD CONSTRAINT FK_E_A FOREIGN KEY (A_Id) REFERENCES A (Id)"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER TABLE F ADD CONSTRAINT FK_F_B FOREIGN KEY (B_Id) REFERENCES B (Id)"; + command.ExecuteNonQuery(); + + command.CommandText = "INSERT INTO D (Id) VALUES (1)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO C (Id, D_Id) VALUES (1, 1)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO A (Id) VALUES (1)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO B (Id, C_Id, D_Id) VALUES (1, 1, 1)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO E (Id, A_Id) VALUES (1, 1)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO F (Id, B_Id) VALUES (1, 1)"; + command.ExecuteNonQuery(); + command.CommandText = "UPDATE A SET B_Id = 1"; + command.ExecuteNonQuery(); + command.CommandText = "UPDATE B SET A_Id = 1"; + command.ExecuteNonQuery(); + + command.CommandText = "SELECT COUNT(1) FROM A"; + command.ExecuteScalar().ShouldBe(1); + command.CommandText = "SELECT COUNT(1) FROM B"; + command.ExecuteScalar().ShouldBe(1); + command.CommandText = "SELECT COUNT(1) FROM C"; + command.ExecuteScalar().ShouldBe(1); + command.CommandText = "SELECT COUNT(1) FROM D"; + command.ExecuteScalar().ShouldBe(1); + command.CommandText = "SELECT COUNT(1) FROM E"; + command.ExecuteScalar().ShouldBe(1); + command.CommandText = "SELECT COUNT(1) FROM F"; + command.ExecuteScalar().ShouldBe(1); + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { "DB2INST1" } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + command.CommandText = "SELECT COUNT(1) FROM A"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM B"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM C"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM D"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM E"; + command.ExecuteScalar().ShouldBe(0); + command.CommandText = "SELECT COUNT(1) FROM F"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS A;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS B;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS C;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS D;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS E;"; + command.ExecuteNonQuery(); + command.CommandText = "DROP TABLE IF EXISTS F;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldExcludeSchemas() + { + const string schema_1 = "schema1"; + const string schema_2 = "schema2"; + + var database = new Database(_connection); + await database.ExecuteAsync($"DROP TABLE IF EXISTS {schema_1}.Foo;"); + await database.ExecuteAsync($"DROP TABLE IF EXISTS {schema_2}.Bar;"); + + await CreateSchema(schema_1); + await CreateSchema(schema_2); + await using var command = new DB2Command($"DROP TABLE IF EXISTS {schema_1}.Foo; CREATE TABLE {schema_1}.Foo (Value INT)", _connection); + command.ExecuteNonQuery(); + command.CommandText = $"DROP TABLE IF EXISTS {schema_2}.Bar; CREATE TABLE {schema_2}.Bar (Value INT)"; + command.ExecuteNonQuery(); + + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value", i)); + command.CommandText = $"INSERT INTO {schema_1}.Foo VALUES (?)"; + command.ExecuteNonQuery(); + command.CommandText = $"INSERT INTO {schema_2}.Bar VALUES (?)"; + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToExclude = new[] { schema_1 } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + command.CommandText = $"SELECT COUNT(1) FROM {schema_1}.Foo"; + command.ExecuteScalar().ShouldBe(100); + command.CommandText = $"SELECT COUNT(1) FROM {schema_2}.Bar"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = $"DROP TABLE IF EXISTS {schema_1}.Foo;"; + command.ExecuteNonQuery(); + command.CommandText = $"DROP TABLE IF EXISTS {schema_2}.Bar;"; + command.ExecuteNonQuery(); + + command.CommandText = $"DROP SCHEMA {schema_1} RESTRICT;"; + command.ExecuteNonQuery(); + command.CommandText = $"DROP SCHEMA {schema_2} RESTRICT;"; + command.ExecuteNonQuery(); + } + + [SkipOnCI] + public async Task ShouldIncludeSchemas() + { + const string schema_1 = "schema1"; + const string schema_2 = "schema2"; + + await using var command = new DB2Command($"DROP TABLE IF EXISTS {schema_1}.Foo;", _connection); + command.ExecuteNonQuery(); + command.CommandText = $"DROP TABLE IF EXISTS {schema_2}.Bar;"; + command.ExecuteNonQuery(); + + await CreateSchema(schema_1); + await CreateSchema(schema_2); + command.CommandText = $"CREATE TABLE {schema_1}.Foo (Value INT)"; + command.ExecuteNonQuery(); + command.CommandText = $"CREATE TABLE {schema_2}.Bar (Value INT)"; + command.ExecuteNonQuery(); + + for (int i = 0; i < 100; i++) + { + command.Parameters.Add(new DB2Parameter("Value", i)); + command.CommandText = $"INSERT INTO {schema_1}.Foo VALUES (?)"; + command.ExecuteNonQuery(); + command.CommandText = $"INSERT INTO {schema_2}.Bar VALUES (?)"; + command.ExecuteNonQuery(); + command.Parameters.Clear(); + } + + var checkPoint = await Respawner.CreateAsync(_connection, new RespawnerOptions + { + DbAdapter = DbAdapter.DB2, + SchemasToInclude = new[] { schema_2 } + }); + try + { + await checkPoint.ResetAsync(_connection); + } + catch + { + _output.WriteLine(checkPoint.DeleteSql ?? string.Empty); + throw; + } + + command.CommandText = $"SELECT COUNT(1) FROM {schema_1}.Foo"; + command.ExecuteScalar().ShouldBe(100); + command.CommandText = $"SELECT COUNT(1) FROM {schema_2}.Bar"; + command.ExecuteScalar().ShouldBe(0); + + // Cleanup + command.CommandText = $"DROP TABLE IF EXISTS {schema_1}.Foo;"; + command.ExecuteNonQuery(); + command.CommandText = $"DROP TABLE IF EXISTS {schema_2}.Bar;"; + command.ExecuteNonQuery(); + + command.CommandText = $"DROP SCHEMA {schema_1} RESTRICT;"; + command.ExecuteNonQuery(); + command.CommandText = $"DROP SCHEMA {schema_2} RESTRICT;"; + command.ExecuteNonQuery(); + } + + private async Task CreateSchema(string schemaName) + { + var database = new Database(_connection); + + try + { + await database.ExecuteAsync($"DROP SCHEMA {schemaName} RESTRICT;"); + } + catch (DB2Exception) + { + // Ignore + } + + await database.ExecuteAsync($"CREATE SCHEMA {schemaName} AUTHORIZATION db2inst1;"); + } + } +} + +//#endif diff --git a/Respawn/DB2DbAdapter.cs b/Respawn/DB2DbAdapter.cs new file mode 100644 index 0000000..f5494ec --- /dev/null +++ b/Respawn/DB2DbAdapter.cs @@ -0,0 +1,224 @@ +using Respawn.Graph; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Respawn +{ + internal class DB2DbAdapter : IDbAdapter + { + private const char QuoteCharacter = '"'; + private const string SysToolsSchema = "SYSTOOLS"; + + public string BuildTableCommandText(RespawnerOptions options) + { + var sb = new StringBuilder(@"SELECT TABSCHEMA, TABNAME + FROM SYSCAT.TABLES + WHERE TYPE = 'T' + AND OWNERTYPE = 'U'"); + + if (options.TablesToIgnore.Any()) + { + var tablesToIgnoreGroups = options.TablesToIgnore + .GroupBy( + t => t.Schema != null, + t => t, + (hasSchema, tables) => new + { + HasSchema = hasSchema, + Tables = tables + }) + .ToList(); + foreach (var tableGroup in tablesToIgnoreGroups) + { + if (tableGroup.HasSchema) + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Schema!.ToUpperInvariant()}.{table.Name.ToUpperInvariant()}'")); + + sb.Append($" AND TABSCHEMA || '.' || TABNAME NOT IN ({args})"); + } + else + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Name.ToUpperInvariant()}'")); + + sb.Append($" AND TABNAME NOT IN ({args})"); + } + } + } + if (options.TablesToInclude.Any()) + { + var tablesToIncludeGroups = options.TablesToInclude + .GroupBy( + t => t.Schema != null, + t => t, + (hasSchema, tables) => new + { + HasSchema = hasSchema, + Tables = tables + }) + .ToList(); + foreach (var tableGroup in tablesToIncludeGroups) + { + if (tableGroup.HasSchema) + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Schema!.ToUpperInvariant()}.{table.Name.ToUpperInvariant()}'")); + + sb.Append($" AND TABSCHEMA || '.' || TABNAME IN ({args})"); + } + else + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Name.ToUpperInvariant()}'")); + + sb.Append($" AND TABNAME IN ({args})"); + } + } + } + + if (options.SchemasToExclude.Any()) + { + var args = string.Join(",", options.SchemasToExclude.Select(t => $"'{t.ToUpperInvariant()}'")); + args += $", '{SysToolsSchema}'"; + sb.Append($" AND TABSCHEMA NOT IN ({args})"); + } + else if (options.SchemasToInclude.Any()) + { + var args = string.Join(",", options.SchemasToInclude.Select(t => $"'{t.ToUpperInvariant()}'")); + + sb.Append($" AND TABSCHEMA IN ({args})"); + } + else + { + sb.Append($" AND TABSCHEMA != '{SysToolsSchema}'"); + } + + return sb.ToString(); + } + + + public string BuildRelationshipCommandText(RespawnerOptions options) + { + var sb = new StringBuilder(); + + sb.Append(@"SELECT TABSCHEMA, TABNAME, REFTABSCHEMA, REFTABNAME, CONSTNAME FROM SYSCAT.REFERENCES WHERE 1=1"); + + if (options.TablesToIgnore.Any()) + { + var tablesToIgnoreGroups = options.TablesToIgnore + .GroupBy( + t => t.Schema != null, + t => t, + (hasSchema, tables) => new + { + HasSchema = hasSchema, + Tables = tables + }) + .ToList(); + foreach (var tableGroup in tablesToIgnoreGroups) + { + if (tableGroup.HasSchema) + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Schema}.{table.Name}'")); + + sb.Append(" AND TABSCHEMA + '.' + TABSCHEMA NOT IN (" + args + ")"); + } + else + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Name}'")); + + sb.Append(" AND TABNAME NOT IN (" + args + ")"); + } + } + } + if (options.TablesToInclude.Any()) + { + var tablesToIncludeGroups = options.TablesToInclude + .GroupBy( + t => t.Schema != null, + t => t, + (hasSchema, tables) => new + { + HasSchema = hasSchema, + Tables = tables + }) + .ToList(); + foreach (var tableGroup in tablesToIncludeGroups) + { + if (tableGroup.HasSchema) + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Schema!.ToUpperInvariant()}.{table.Name.ToUpperInvariant()}'")); + + sb.Append(" AND TABSCHEMA + '.' + TABNAME IN (" + args + ")"); + } + else + { + var args = string.Join(",", tableGroup.Tables.Select(table => $"'{table.Name.ToUpperInvariant()}'")); + + sb.Append(" AND TABNAME IN (" + args + ")"); + } + } + } + if (options.SchemasToExclude.Any()) + { + var args = string.Join(",", options.SchemasToExclude.Select(t => $"'{t.ToUpperInvariant()}'")); + + sb.Append(" AND TABSCHEMA NOT IN (" + args + ")"); + } + else if (options.SchemasToInclude.Any()) + { + var args = string.Join(",", options.SchemasToInclude.Select(t => $"'{t.ToUpperInvariant()}'")); + + sb.Append(" AND TABSCHEMA IN (" + args + ")"); + } + + + return sb.ToString(); + } + + public string BuildDeleteCommandText(GraphBuilder builder) + { + var sb = new StringBuilder(); + + foreach (var table in builder.CyclicalTableRelationships) + { + sb.AppendLine($"ALTER TABLE {table.ParentTable} ALTER FOREIGN KEY {table.Name} NOT ENFORCED;"); + } + foreach (var table in builder.ToDelete) + { + sb.AppendLine($"DELETE FROM {table.GetFullName(QuoteCharacter)};"); + } + foreach (var table in builder.CyclicalTableRelationships) + { + sb.AppendLine($"ALTER TABLE {table.ParentTable} ALTER FOREIGN KEY {table.Name} ENFORCED;"); + } + + return sb.ToString(); + } + + public string BuildReseedSql(IEnumerable tablesToDelete) + { + throw new System.NotImplementedException(); + } + + public string BuildTemporalTableCommandText(RespawnerOptions options) + { + throw new System.NotImplementedException(); + } + + public string BuildTurnOffSystemVersioningCommandText(IEnumerable tablesToTurnOffSystemVersioning) + { + throw new System.NotImplementedException(); + } + + public string BuildTurnOnSystemVersioningCommandText(IEnumerable tablesToTurnOnSystemVersioning) + { + throw new System.NotImplementedException(); + } + + public Task CheckSupportsTemporalTables(DbConnection connection) + { + return Task.FromResult(false); + } + } +} diff --git a/Respawn/DbAdapter.cs b/Respawn/DbAdapter.cs index a001b0a..4c95f47 100644 --- a/Respawn/DbAdapter.cs +++ b/Respawn/DbAdapter.cs @@ -7,5 +7,6 @@ public static class DbAdapter public static readonly IDbAdapter MySql = new MySqlAdapter(); public static readonly IDbAdapter Oracle = new OracleDbAdapter(); public static readonly IDbAdapter Informix = new InformixDbAdapter(); + public static readonly IDbAdapter DB2 = new DB2DbAdapter(); } } diff --git a/docker-compose.yml b/docker-compose.yml index 40a7a25..a30093f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,26 @@ services: privileged: true volumes: - ./informix-server:/opt/ibm/config + db2: + image: icr.io/db2_community/db2:latest + restart: always + tty: true + environment: + TO_CREATE_SAMPLEDB: false + UPDATEAVAIL: NO + ENABLE_ORACLE_COMPATIBILITY: false + BLU: false + DBNAME: SAMPLEDB + DB2INST1_PASSWORD: password + DB2INSTANCE: db2inst1 + LICENSE: accept + DBPORT: 50000 + STORAGE_DIR: /database + ports: + - 50000:50000 + privileged: true + volumes: + - ./db2-server:/database adminer: image: adminer restart: always