Skip to content

Commit

Permalink
Implemented exposing DependencyTraversalStrategy configurability
Browse files Browse the repository at this point in the history
  • Loading branch information
carl-berg committed Dec 21, 2020
1 parent eab1ee0 commit 62245ca
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 23 deletions.
14 changes: 9 additions & 5 deletions DataDude.Tests/Core/TestTable.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using DataDude.Schema;
using System.Linq;
using DataDude.Schema;

namespace DataDude.Tests.Core
{
public class TestTable : TableInformation
{
public TestTable(string name)
: base("dbo", name, table => new[]
: this(name, "Id")
{
new ColumnInformation(table, "Id", "int", true, true, false, false, null, 4, 0, 0),
})
}

public TestTable(string name, params string[] columns)
: base("dbo", name, table => columns.Select(c => new ColumnInformation(table, c, "int", true, true, false, false, null, 4, 0, 0)))
{
}

Expand All @@ -18,9 +21,10 @@ public TestTable AddFk(params TableInformation[] to)
{
var fk = new ForeignKeyInformation(
$"FK_{Name}_{referenceTable.Name}",
this,
referenceTable,
new[] { (this["Id"], referenceTable["Id"]) });
AddForeignKey(fk);
AddForeignKey(t => fk);
}

return this;
Expand Down
10 changes: 7 additions & 3 deletions DataDude.Tests/Inserts/AutoInsertFKTableTestscs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using DataDude.Instructions.Insert;
using DataDude.Instructions.Insert.AutomaticForeignKeys;
using DataDude.Schema;
using DataDude.Tests.Core;
using Shouldly;
using Xunit;
Expand All @@ -21,7 +22,8 @@ public async Task Chain()
var context = new DataDudeContext() { Schema = schema };

context.Instructions.Add(new InsertInstruction("C"));
await new AddMissingInsertInstructionsPreProcessor().PreProcess(context);
var dependencyService = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
await new AddMissingInsertInstructionsPreProcessor(dependencyService).PreProcess(context);
context.Instructions
.OfType<InsertInstruction>()
.Select(x => x.TableName)
Expand All @@ -41,7 +43,8 @@ public async Task Chain_Sparsely_Specified_Scenario_1()

context.Instructions.Add(new InsertInstruction("B"));
context.Instructions.Add(new InsertInstruction("D"));
await new AddMissingInsertInstructionsPreProcessor().PreProcess(context);
var dependencyService = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
await new AddMissingInsertInstructionsPreProcessor(dependencyService).PreProcess(context);
context.Instructions
.OfType<InsertInstruction>()
.Select(x => x.TableName)
Expand All @@ -61,7 +64,8 @@ public async Task Chain_Sparsely_Specified_Scenario_2()

context.Instructions.Add(new InsertInstruction("A"));
context.Instructions.Add(new InsertInstruction("D"));
await new AddMissingInsertInstructionsPreProcessor().PreProcess(context);
var dependencyService = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
await new AddMissingInsertInstructionsPreProcessor(dependencyService).PreProcess(context);
context.Instructions
.OfType<InsertInstruction>()
.Select(x => x.TableName)
Expand Down
61 changes: 57 additions & 4 deletions DataDude.Tests/Schema/DependencyServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void Resolve_Chain()
var b = new TestTable("B").AddFk(a);
var c = new TestTable("C").AddFk(b);

var service = new DependencyService();
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
service.GetOrderedDependenciesFor(c).ShouldBe(new[] { a, b }, true);
service.GetOrderedDependenciesFor(b).ShouldBe(new[] { a });
service.GetOrderedDependenciesFor(a).ShouldBeEmpty();
Expand All @@ -25,7 +25,7 @@ public void Resolve_Chain()
[Fact]
public void Resolve_Fork()
{
var service = new DependencyService();
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
var a = new TestTable("A");
var b = new TestTable("B");
var c = new TestTable("C").AddFk(a, b);
Expand All @@ -38,7 +38,7 @@ public void Resolve_Fork()
[Fact]
public void Resolve_Forked_Branches()
{
var service = new DependencyService();
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
var a = new TestTable("A");
var b = new TestTable("B");
var c = new TestTable("C").AddFk(a, b);
Expand All @@ -53,7 +53,7 @@ public void Resolve_Forked_Branches()
[Fact]
public void Resolve_Diamond()
{
var service = new DependencyService();
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
var a = new TestTable("A");
var b = new TestTable("B").AddFk(a);
var c = new TestTable("C").AddFk(a);
Expand All @@ -64,6 +64,59 @@ public void Resolve_Diamond()
AssertDepencyOrderFor(d, dependencies);
}

[Fact]
public void Resolve_Recursive_using_Follow_All_ForeignKeys()
{
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);
var a = new TestTable("A");
a.AddForeignKey(t => new ForeignKeyInformation("FK", t, t, new[] { (t["Id"], t["Id"]) }));

Should.Throw<DependencyTraversalFailedException>(() => service.GetOrderedDependenciesFor(a));
}

[Fact]
public void Resolve_Recursive_using_Skip_Recursive_ForeignKeys()
{
var service = new DependencyService(DependencyTraversalStrategy.SkipRecursiveForeignKeys);
var a = new TestTable("A");
a.AddForeignKey(t => new ForeignKeyInformation("FK", t, t, new[] { (t["Id"], t["Id"]) }));

var dependencies = Should.NotThrow(() => service.GetOrderedDependenciesFor(a));
dependencies.ShouldBeEmpty();
}

[Fact]
public void Resolve_Nullable_using_Follow_All_ForeignKeys()
{
var service = new DependencyService(DependencyTraversalStrategy.FollowAllForeignKeys);

var a = new TestTable("A");
var b = new TableInformation("dbo", "B", table => new[]
{
new ColumnInformation(table, "a_Id", "int", false, false, isNullable: true, false, null, 0, 0, 0),
});
b.AddForeignKey(table => new ForeignKeyInformation("FK", table, a, new[] { (table["a_Id"], a["Id"]) }));

var dependencies = Should.NotThrow(() => service.GetOrderedDependenciesFor(b));
dependencies.ShouldContain(a);
}

[Fact]
public void Resolve_Nullable_using_Skip_Nullable_ForeignKeys()
{
var service = new DependencyService(DependencyTraversalStrategy.SkipNullableForeignKeys);

var a = new TestTable("A");
var b = new TableInformation("dbo", "B", table => new[]
{
new ColumnInformation(table, "a_Id", "int", false, false, isNullable: true, false, null, 0, 0, 0),
});
b.AddForeignKey(table => new ForeignKeyInformation("FK", table, a, new[] { (table["a_Id"], a["Id"]) }));

var dependencies = Should.NotThrow(() => service.GetOrderedDependenciesFor(b));
dependencies.ShouldBeEmpty();
}

private void AssertDepencyOrderFor(TableInformation table, IEnumerable<TableInformation> dependencies)
{
var previousDependencies = new HashSet<TableInformation>();
Expand Down
2 changes: 1 addition & 1 deletion DataDude/DataDude.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<Version>0.4.0-beta.6</Version>
<Version>0.4.0-beta.7</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ namespace DataDude.Instructions.Insert.AutomaticForeignKeys
/// </summary>
public class AddMissingInsertInstructionsPreProcessor : IInstructionPreProcessor
{
private readonly DependencyService _dependencyService;
public AddMissingInsertInstructionsPreProcessor(DependencyService dependencyService) => _dependencyService = dependencyService;

public Task PreProcess(DataDudeContext context)
{
var service = new DependencyService();
var toInsert = new Dictionary<InsertInstruction, InsertInformation>();
foreach (var instruction in context.Instructions.OfType<InsertInstruction>())
{
if (context.Schema?[instruction.TableName] is { } table)
{
var dependencies = service.GetOrderedDependenciesFor(table)
var dependencies = _dependencyService.GetOrderedDependenciesFor(table)
.Where(t => !toInsert.Values.Any(x => x.Contains(t)))
.ToList();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
namespace DataDude.Instructions.Insert.AutomaticForeignKeys
using DataDude.Schema;

namespace DataDude.Instructions.Insert.AutomaticForeignKeys
{
public class AutoFKConfiguration
{
/// <summary>
/// Gets or sets a value indicating whether "missing" foreign key dependencies will automatically be added as insert instructions.
/// </summary>
public bool AddMissingForeignKeys { get; set; }

/// <summary>
/// Gets or sets a traversal strategy for dependencies. Default value is to follow all foreign keys.
/// </summary>
public IDependencyTraversalStrategy DependencyTraversalStrategy { get; set; } = Schema.DependencyTraversalStrategy.FollowAllForeignKeys;
}
}
3 changes: 2 additions & 1 deletion DataDude/Instructions/Insert/InsertExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public static Dude EnableAutomaticForeignKeys(this Dude dude, Action<AutoFKConfi

if (config.AddMissingForeignKeys)
{
dude.Configure(x => x.InstructionPreProcessors.Add(new AddMissingInsertInstructionsPreProcessor()));
var dependencyService = new DependencyService(config.DependencyTraversalStrategy);
dude.Configure(x => x.InstructionPreProcessors.Add(new AddMissingInsertInstructionsPreProcessor(dependencyService)));
}

return dude;
Expand Down
9 changes: 6 additions & 3 deletions DataDude/Schema/DependencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ namespace DataDude.Schema
{
public class DependencyService
{
private readonly IDependencyTraversalStrategy _strategy;
public DependencyService(IDependencyTraversalStrategy strategy) => _strategy = strategy;

public IEnumerable<TableInformation> GetOrderedDependenciesFor(TableInformation table)
{
var dependencies = new HashSet<TableInformation>();
foreach (var dep in table.ForeignKeys.Select(x => x.ReferencedTable))
foreach (var dep in table.ForeignKeys.Where(_strategy.Process).Select(x => x.ReferencedTable))
{
ProcessDependencies(dep, ref dependencies);
}
Expand All @@ -26,7 +29,7 @@ public IEnumerable<TableInformation> GetOrderedDependenciesFor(TableInformation

if (sortedDependencies.Count() != dependencies.Count())
{
throw new Exception($"Failed building a dependency chain for table {table.FullName}");
throw new DependencyTraversalFailedException($"Failed building a dependency chain for table {table.FullName}");
}

return sortedDependencies;
Expand All @@ -35,7 +38,7 @@ public IEnumerable<TableInformation> GetOrderedDependenciesFor(TableInformation
private void ProcessDependencies(TableInformation table, ref HashSet<TableInformation> dependencies)
{
dependencies.Add(table);
foreach (var dep in table.ForeignKeys.Select(x => x.ReferencedTable))
foreach (var dep in table.ForeignKeys.Where(_strategy.Process).Select(x => x.ReferencedTable))
{
if (!dependencies.Contains(dep))
{
Expand Down
17 changes: 17 additions & 0 deletions DataDude/Schema/DependencyTraversalFailedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace DataDude.Schema
{
public class DependencyTraversalFailedException : Exception
{
public DependencyTraversalFailedException(string message)
: base(message)
{
}

public DependencyTraversalFailedException(TableInformation sourceTable)
: this($"Failed building a dependency chain for table {sourceTable.FullName}")
{
}
}
}
43 changes: 43 additions & 0 deletions DataDude/Schema/DependencyTraversalStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Linq;

namespace DataDude.Schema
{
public static class DependencyTraversalStrategy
{
/// <summary>
/// Gets a strategy that does not follow nullable dependencies.
/// </summary>
public static IDependencyTraversalStrategy SkipNullableForeignKeys => new SkipNullableFKTraversalStrategy();

/// <summary>
/// Gets a strategy that does not follow recursivce dependencies.
/// </summary>
public static IDependencyTraversalStrategy SkipRecursiveForeignKeys => new SkipRecursiveFKTraversalStrategy();

/// <summary>
/// Gets a strategy that follows all dependencies.
/// </summary>
public static IDependencyTraversalStrategy FollowAllForeignKeys => new FollowAllForeignKeysTraversalStrategy();

private class SkipNullableFKTraversalStrategy : IDependencyTraversalStrategy
{
public bool Process(ForeignKeyInformation foreignKey)
{
return foreignKey.Columns.All(c => c.Column.IsNullable == false);
}
}

private class SkipRecursiveFKTraversalStrategy : IDependencyTraversalStrategy
{
public bool Process(ForeignKeyInformation foreignKey)
{
return foreignKey.Table != foreignKey.ReferencedTable;
}
}

private class FollowAllForeignKeysTraversalStrategy : IDependencyTraversalStrategy
{
public bool Process(ForeignKeyInformation foreignKey) => true;
}
}
}
4 changes: 3 additions & 1 deletion DataDude/Schema/ForeignKeyInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ namespace DataDude.Schema
{
public class ForeignKeyInformation
{
public ForeignKeyInformation(string name, TableInformation referencedTable, IEnumerable<(ColumnInformation Column, ColumnInformation ReferencedColumn)> columns)
public ForeignKeyInformation(string name, TableInformation table, TableInformation referencedTable, IEnumerable<(ColumnInformation Column, ColumnInformation ReferencedColumn)> columns)
{
Name = name;
Table = table;
ReferencedTable = referencedTable;
Columns = columns;
}

public string Name { get; }
public TableInformation Table { get; }
public TableInformation ReferencedTable { get; }
public IEnumerable<(ColumnInformation Column, ColumnInformation ReferencedColumn)> Columns { get; }
}
Expand Down
7 changes: 7 additions & 0 deletions DataDude/Schema/IDependencyTraversalStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DataDude.Schema
{
public interface IDependencyTraversalStrategy
{
bool Process(ForeignKeyInformation foreignKey);
}
}
2 changes: 1 addition & 1 deletion DataDude/Schema/SqlServerSchemaLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public async Task<SchemaInformation> Load(IDbConnection connection, IDbTransacti
}

var fkColumns = GetForeignKeyColumns(constraintName, table, referenceTable, group);
table.AddForeignKey(new ForeignKeyInformation(group.Key.ConstraintName, referenceTable, fkColumns));
table.AddForeignKey(table => new ForeignKeyInformation(group.Key.ConstraintName, table, referenceTable, fkColumns));
}

foreach (var group in triggers.GroupBy(x => (x.SchemaName, x.TableName)))
Expand Down
2 changes: 1 addition & 1 deletion DataDude/Schema/TableInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public TableInformation(string schema, string name, Func<TableInformation, IEnum
public ColumnInformation? this[string name] => _columns.TryGetValue(name, out var column) ? column : null;
public IEnumerator<ColumnInformation> GetEnumerator() => _columns.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void AddForeignKey(ForeignKeyInformation fk) => _foreignKeys.Add(fk);
public void AddForeignKey(Func<TableInformation, ForeignKeyInformation> getFk) => _foreignKeys.Add(getFk(this));
public void AddTrigger(TriggerInformation trigger) => _triggers.Add(trigger);

public override string ToString() => Name;
Expand Down

0 comments on commit 62245ca

Please sign in to comment.