Skip to content

Commit ef44a45

Browse files
committed
+ base implementation
+ some unit tests via NSubstitute + gha: run tests on pr
1 parent ec95243 commit ef44a45

File tree

10 files changed

+373
-7
lines changed

10 files changed

+373
-7
lines changed

.github/workflows/pr.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
on:
2+
push:
3+
branches:
4+
- "**"
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/setup-dotnet@v1
11+
- uses: actions/checkout@v2
12+
- run: dotnet build --configuration Release
13+
- run: |
14+
cd ${{ github.workspace }}/mongo-declarative-indexes.Tests
15+
dotnet test --configuration Release --no-build
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using MongoDB.DeclarativeIndexes;
5+
using NSubstitute;
6+
using Xunit;
7+
using Index = MongoDB.DeclarativeIndexes.Index;
8+
9+
namespace mongo_declarative_indexes.Tests
10+
{
11+
public class IndexEnsurerShould
12+
{
13+
[Fact]
14+
public void Ensure_CreatesMissingIndexes()
15+
{
16+
var database = Substitute.For<IDatabase>();
17+
var ensurer = new IndexEnsurer(database);
18+
var expectedIndexes = new[] {new Index(keys: new Key("field", IndexType.Ascending))};
19+
ensurer.Ensure(new CollectionIndexes("testCollection", expectedIndexes));
20+
21+
database.Received().CreateManyIndexes("testCollection",
22+
Arg.Is<Index[]>(actualIndexes =>
23+
actualIndexes.SequenceEqual(expectedIndexes)));
24+
}
25+
26+
[Fact]
27+
public void Ensure_DropsExtraIndexes()
28+
{
29+
var database = Substitute.For<IDatabase>();
30+
var extraIndex = new Dictionary<string, object>
31+
{
32+
{"v", 2},
33+
{"key", new Dictionary<string, object> {{"field", 1}}},
34+
{"name", "field_1"},
35+
{"ns", "test.collections"}
36+
};
37+
database.ListCollectionNames().Returns(new[] {"collectionName"});
38+
database.ListIndexes("collectionName").Returns(new[] {extraIndex});
39+
40+
var ensurer = new IndexEnsurer(database);
41+
ensurer.Ensure(Array.Empty<CollectionIndexes>());
42+
43+
database.DidNotReceiveWithAnyArgs().CreateManyIndexes(default, default);
44+
database.Received().DropOneIndex("collectionName", "field_1");
45+
}
46+
47+
[Fact]
48+
public void Ensure_DoesNothingWithIdIndex()
49+
{
50+
var database = Substitute.For<IDatabase>();
51+
var idIndex = new Dictionary<string, object>
52+
{
53+
{"v", 2},
54+
{"key", new Dictionary<string, object> {{"_id", 1}}},
55+
{"name", "_id_"},
56+
{"ns", "test.collections"}
57+
};
58+
database.ListCollectionNames().Returns(new[] {"collectionName"});
59+
database.ListIndexes("collectionName").Returns(new[] {idIndex});
60+
61+
var ensurer = new IndexEnsurer(database);
62+
ensurer.Ensure(Array.Empty<CollectionIndexes>());
63+
64+
database.DidNotReceiveWithAnyArgs().CreateManyIndexes(default, default);
65+
database.DidNotReceiveWithAnyArgs().DropOneIndex(default, default);
66+
}
67+
68+
[Fact]
69+
public void Ensure_DropsExtraAndCreatesMissingIndexes()
70+
{
71+
var database = Substitute.For<IDatabase>();
72+
var extraIndex = new Dictionary<string, object>
73+
{
74+
{"v", 2},
75+
{"key", new Dictionary<string, object> {{"field", 1}}},
76+
{"name", "field_1"},
77+
{"ns", "test.collections"}
78+
};
79+
var remainingDbIndex = new Dictionary<string, object>
80+
{
81+
{"v", 2},
82+
{"key", new Dictionary<string, object> {{"another_field", 1}}},
83+
{"name", "another_field_1"},
84+
{"ns", "test.collections"}
85+
};
86+
database.ListCollectionNames().Returns(new[] {"collectionName"});
87+
database.ListIndexes("collectionName").Returns(new[] {extraIndex, remainingDbIndex});
88+
89+
90+
var expectedCreatedIndexes = new[] {new Index(keys: new Key("yet_another_field", IndexType.Descending))};
91+
var ensurer = new IndexEnsurer(database);
92+
var remainingIndex = new Index(keys: new Key("another_field", IndexType.Ascending));
93+
ensurer.Ensure(new CollectionIndexes("testCollection",
94+
expectedCreatedIndexes.Append(remainingIndex).ToArray()));
95+
database.Received().DropOneIndex("collectionName", "field_1");
96+
database.Received().CreateManyIndexes("testCollection",
97+
Arg.Is<Index[]>(actualIndexes =>
98+
actualIndexes
99+
.SequenceEqual(expectedCreatedIndexes)));
100+
}
101+
}
102+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<RootNamespace>mongo_declarative_indexes.Tests</RootNamespace>
6+
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
12+
<PackageReference Include="NSubstitute" Version="4.2.2" />
13+
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.14" />
14+
<PackageReference Include="xunit" Version="2.4.1" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="1.3.0">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\mongo-declarative-indexes\mongo-declarative-indexes.csproj" />
27+
</ItemGroup>
28+
29+
</Project>

mongo-declarative-indexes.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3+
#
34
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mongo-declarative-indexes", "mongo-declarative-indexes\mongo-declarative-indexes.csproj", "{4BAF5452-C015-4896-B132-F71398203255}"
45
EndProject
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mongo-declarative-indexes.Tests", "mongo-declarative-indexes.Tests\mongo-declarative-indexes.Tests.csproj", "{B4733E13-E55A-45B1-B592-032206E3BBF3}"
7+
EndProject
58
Global
69
GlobalSection(SolutionConfigurationPlatforms) = preSolution
710
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +15,9 @@ Global
1215
{4BAF5452-C015-4896-B132-F71398203255}.Debug|Any CPU.Build.0 = Debug|Any CPU
1316
{4BAF5452-C015-4896-B132-F71398203255}.Release|Any CPU.ActiveCfg = Release|Any CPU
1417
{4BAF5452-C015-4896-B132-F71398203255}.Release|Any CPU.Build.0 = Release|Any CPU
18+
{B4733E13-E55A-45B1-B592-032206E3BBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19+
{B4733E13-E55A-45B1-B592-032206E3BBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
20+
{B4733E13-E55A-45B1-B592-032206E3BBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
21+
{B4733E13-E55A-45B1-B592-032206E3BBF3}.Release|Any CPU.Build.0 = Release|Any CPU
1522
EndGlobalSection
1623
EndGlobal
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace MongoDB.DeclarativeIndexes
2+
{
3+
public class CollectionIndexes
4+
{
5+
public CollectionIndexes(string collectionName, params Index[] indexes)
6+
{
7+
CollectionName = collectionName;
8+
Indexes = indexes;
9+
}
10+
11+
public string CollectionName { get; }
12+
13+
public Index[] Indexes { get; }
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Generic;
2+
3+
namespace MongoDB.DeclarativeIndexes
4+
{
5+
/*
6+
* Abstraction for eliminate dependency on MongoDB.Driver
7+
*/
8+
public interface IDatabase
9+
{
10+
void CreateManyIndexes(string collectionName, IEnumerable<Index> indexes);
11+
12+
IEnumerable<Dictionary<string, object>> ListIndexes(string collectionName);
13+
14+
IEnumerable<string> ListCollectionNames();
15+
16+
void DropOneIndex(string collectionName, string indexName);
17+
}
18+
}

mongo-declarative-indexes/Index.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using MongoDB.DeclarativeIndexes.helpers;
5+
6+
namespace MongoDB.DeclarativeIndexes
7+
{
8+
public class Index
9+
{
10+
public Index(bool unique = false, string name = null, params Key[] keys)
11+
{
12+
Keys = keys;
13+
Unique = unique;
14+
Name = name;
15+
}
16+
17+
public Key[] Keys { get; }
18+
public bool Unique { get; }
19+
public string Name { get; }
20+
21+
public static Index FromDb(Dictionary<string, object> dbDocument)
22+
{
23+
/*
24+
* {
25+
* "v": 2,
26+
* "key": {"field_name": "type"},
27+
* "name": "index_name",
28+
* "ns": "{namespace}
29+
* }
30+
*/
31+
var dbKeys = (IEnumerable<KeyValuePair<string, object>>) dbDocument["key"];
32+
return new Index(keys: dbKeys.Select(x => new Key(x.Key, IndexType.Ascending)).ToArray(),
33+
name: (string) dbDocument.GetValueOrDefault("name"));
34+
}
35+
36+
public Dictionary<string, object> ToDb()
37+
{
38+
return Keys.ToDictionary(k => k.Field, k => ConvertIndexTypeToDb(k.IndexType));
39+
}
40+
41+
private static object ConvertIndexTypeToDb(IndexType indexType)
42+
{
43+
// ReSharper disable once HeapView.BoxingAllocation
44+
return indexType switch
45+
{
46+
IndexType.Ascending => 1,
47+
IndexType.Descending => -1,
48+
// IndexType.Text => "text",
49+
_ => throw new ArgumentOutOfRangeException(nameof(indexType), indexType, null)
50+
};
51+
}
52+
}
53+
54+
public class Key
55+
{
56+
public Key(string field, IndexType indexType)
57+
{
58+
Field = field;
59+
IndexType = indexType;
60+
}
61+
62+
public string Field { get; }
63+
64+
public IndexType IndexType { get; }
65+
}
66+
67+
public enum IndexType
68+
{
69+
Ascending,
70+
Descending,
71+
// Text
72+
}
73+
}
Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,103 @@
1-
namespace MongoDB.DeclarativeIndexes
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace MongoDB.DeclarativeIndexes
25
{
36
public class IndexEnsurer
47
{
5-
public void Ensure()
8+
private readonly IDatabase _database;
9+
10+
public IndexEnsurer(IDatabase database)
11+
{
12+
_database = database;
13+
}
14+
15+
public EnsurerContinuation Begin(IEnumerable<CollectionIndexes> indexes)
16+
{
17+
var targetIndexesByCollectionName = indexes.ToDictionary(i => i.CollectionName, i => i.Indexes);
18+
19+
var existingIndexes = _database.ListCollectionNames()
20+
.Select(name => new CollectionIndexes(name, GetExistingIndexes(name))).ToList();
21+
22+
var extraIndexes = existingIndexes
23+
.Select(i => new CollectionIndexes(i.CollectionName,
24+
i.Indexes
25+
.Where(x =>
26+
!targetIndexesByCollectionName
27+
.ContainsKey(i.CollectionName) ||
28+
!targetIndexesByCollectionName[i.CollectionName]
29+
.Contains(x)).ToArray())).ToList();
30+
31+
foreach (var collectionIndex in extraIndexes)
32+
foreach (var index in collectionIndex.Indexes)
33+
_database.DropOneIndex(collectionIndex.CollectionName, index.Name);
34+
35+
var existingIndexesByCollectionName = existingIndexes.ToDictionary(i => i.CollectionName, i => i.Indexes);
36+
37+
var missingIndexes = indexes.Select(collectionIndex =>
38+
new CollectionIndexes(collectionIndex.CollectionName,
39+
GetMissingIndexes(existingIndexesByCollectionName,
40+
collectionIndex))).ToList();
41+
42+
43+
return new EnsurerContinuation(_database, missingIndexes, extraIndexes);
44+
}
45+
46+
public void Ensure(params CollectionIndexes[] indexes)
47+
{
48+
var continuation = Begin(indexes);
49+
continuation.Continue();
50+
}
51+
52+
private Index[] GetExistingIndexes(string collectionName)
53+
{
54+
return _database.ListIndexes(collectionName).Select(Index.FromDb).Where(IsNotIdIndex).ToArray();
55+
}
56+
57+
private static bool IsNotIdIndex(Index index)
58+
{
59+
return index.Keys.Select(k => k.Field).Any(k => k != "_id");
60+
}
61+
62+
private static Index[] GetMissingIndexes(IReadOnlyDictionary<string, Index[]> existingIndexesByCollectionName,
63+
CollectionIndexes collectionIndex)
64+
{
65+
return collectionIndex.Indexes
66+
.Where(i => !existingIndexesByCollectionName.ContainsKey(collectionIndex.CollectionName) ||
67+
!existingIndexesByCollectionName[collectionIndex.CollectionName].Contains(i)).ToArray();
68+
}
69+
}
70+
71+
public sealed class EnsurerContinuation
72+
{
73+
private readonly IDatabase _database;
74+
75+
private readonly List<CollectionIndexes> _missingIndexes;
76+
private readonly List<CollectionIndexes> _extraIndexes;
77+
78+
internal EnsurerContinuation(IDatabase database, List<CollectionIndexes> missingIndexes,
79+
List<CollectionIndexes> extraIndexes)
680
{
7-
Begin();
8-
End();
81+
_missingIndexes = missingIndexes;
82+
_extraIndexes = extraIndexes;
83+
_database = database;
984
}
10-
11-
public void Begin()
85+
86+
87+
public void Rollback()
88+
{
89+
CreateIndexes(_extraIndexes);
90+
}
91+
92+
public void Continue()
1293
{
94+
CreateIndexes(_missingIndexes);
1395
}
1496

15-
public void End()
97+
private void CreateIndexes(IEnumerable<CollectionIndexes> indexes)
1698
{
99+
foreach (var collectionIndex in indexes)
100+
_database.CreateManyIndexes(collectionIndex.CollectionName, collectionIndex.Indexes);
17101
}
18102
}
19103
}

0 commit comments

Comments
 (0)