diff --git a/README.md b/README.md index 879b62d..be1bac2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,24 @@ using (var conn = new NpgsqlConnection("ConnectionString")) } ``` +## Custom Database Adapter + +If you need to support a database that is not included, or want to provide your own implementation of `IDbAdapter`, you can do so: + +```csharp +var options = new RespawnerOptions +{ + DbAdapter = new MyCustomDbAdapter() +}; +var respawner = await Respawner.CreateAsync(connection, options); +``` + + `myCustomDbAdapter` should implement the `IDbAdapter` interface. + +This allows you to extend Respawn for any database or custom logic you require. + +For a very simple example of a custom IDbAdapter being used see the [SqliteCustomAdapterTests](Respawn.DatabaseTests\SqliteCustomAdapterTests.cs) + ## How does it work? Respawn examines the SQL metadata intelligently to build a deterministic order of tables to delete based on foreign key relationships between tables. It navigates these relationships to build a DELETE script starting with the tables with no relationships and moving inwards until all tables are accounted for. diff --git a/Respawn.DatabaseTests/Respawn.DatabaseTests.csproj b/Respawn.DatabaseTests/Respawn.DatabaseTests.csproj index 8238d16..e8c3915 100644 --- a/Respawn.DatabaseTests/Respawn.DatabaseTests.csproj +++ b/Respawn.DatabaseTests/Respawn.DatabaseTests.csproj @@ -6,6 +6,7 @@ + diff --git a/Respawn.DatabaseTests/SqliteCustomAdapterTests.cs b/Respawn.DatabaseTests/SqliteCustomAdapterTests.cs new file mode 100644 index 0000000..c94e07b --- /dev/null +++ b/Respawn.DatabaseTests/SqliteCustomAdapterTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Data.Common; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Respawn; +using Respawn.Graph; +using Shouldly; +using Xunit; + +namespace Respawn.DatabaseTests +{ + public class CustomSqliteAdapter : IDbAdapter + { + public string BuildDeleteCommandText(GraphBuilder graph, RespawnerOptions options) + => "DELETE FROM TestTable;"; + public string BuildReseedSql(System.Collections.Generic.IEnumerable tables) => null; + public string BuildTableCommandText(RespawnerOptions options) => "SELECT NULL, name FROM sqlite_master WHERE type='table' AND name='TestTable';"; + public string BuildRelationshipCommandText(RespawnerOptions options) => "SELECT NULL, 'TestTable', NULL, 'TestTable', 'PK_TestTable';"; + public string BuildTemporalTableCommandText(RespawnerOptions options) => "SELECT NULL, NULL, NULL, NULL;"; + public string BuildTurnOffSystemVersioningCommandText(System.Collections.Generic.IEnumerable tables) => string.Empty; + public string BuildTurnOnSystemVersioningCommandText(System.Collections.Generic.IEnumerable tables) => string.Empty; + public Task CheckSupportsTemporalTables(DbConnection connection) => Task.FromResult(false); + } + + public class SqliteCustomAdapterTests + { + [Fact] + public async Task ShouldResetTableWithCustomAdapter() + { + var connectionString = "Data Source=:memory:"; + await using var conn = new SqliteConnection(connectionString); + await conn.OpenAsync(); + + var createTable = conn.CreateCommand(); + createTable.CommandText = "CREATE TABLE TestTable (Id INTEGER PRIMARY KEY, Name TEXT);"; + await createTable.ExecuteNonQueryAsync(); + + var insert = conn.CreateCommand(); + insert.CommandText = "INSERT INTO TestTable (Name) VALUES ('foo');"; + await insert.ExecuteNonQueryAsync(); + + // Confirm row exists + var countCmd = conn.CreateCommand(); + countCmd.CommandText = "SELECT COUNT(*) FROM TestTable;"; + var countBefore = (long)await countCmd.ExecuteScalarAsync(); + countBefore.ShouldBe(1); + + // Use custom adapter + var options = new RespawnerOptions + { + DbAdapter = new CustomSqliteAdapter(), + }; + var respawner = await Respawner.CreateAsync(conn, options); + await respawner.ResetAsync(conn); + + // Confirm table is empty + var countAfter = (long)await countCmd.ExecuteScalarAsync(); + countAfter.ShouldBe(0); + } + } +}