Skip to content

Commit e0e4763

Browse files
author
Dave Thompson
committed
Add stream deletion feature
1 parent 9c9049f commit e0e4763

10 files changed

Lines changed: 217 additions & 20 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using SimpleEventStore.Tests;
4+
5+
namespace SimpleEventStore.AzureDocumentDb.Tests
6+
{
7+
[TestFixture]
8+
public class AzureDocumentDbEventStoreDeletingStream : EventStoreDeletingStream
9+
{
10+
protected override Task<IStorageEngine> CreateStorageEngine()
11+
{
12+
return StorageEngineFactory.Create("DeletingStreamTests");
13+
}
14+
}
15+
}

SimpleEventStore.AzureDocumentDb/AzureDocumentDbStorageEngine.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ namespace SimpleEventStore.AzureDocumentDb
1111
internal class AzureDocumentDbStorageEngine : IStorageEngine
1212
{
1313
private const string AppendStoredProcedureName = "appendToStream";
14+
private const string DeleteStoredProcedureName = "deleteStream";
1415
private const string ConcurrencyConflictErrorKey = "Concurrency conflict.";
1516

1617
private readonly DocumentClient client;
1718
private readonly string databaseName;
1819
private readonly CollectionOptions collectionOptions;
1920
private readonly Uri commitsLink;
20-
private readonly Uri storedProcLink;
21+
private readonly Uri appendStoredProcedureLink;
22+
private readonly Uri deleteStoredProcedureLink;
2123
private readonly LoggingOptions loggingOptions;
2224
private readonly ISerializationTypeMap typeMap;
2325

@@ -27,7 +29,8 @@ internal AzureDocumentDbStorageEngine(DocumentClient client, string databaseName
2729
this.databaseName = databaseName;
2830
this.collectionOptions = collectionOptions;
2931
commitsLink = UriFactory.CreateDocumentCollectionUri(databaseName, collectionOptions.CollectionName);
30-
storedProcLink = UriFactory.CreateStoredProcedureUri(databaseName, collectionOptions.CollectionName, AppendStoredProcedureName);
32+
appendStoredProcedureLink = UriFactory.CreateStoredProcedureUri(databaseName, collectionOptions.CollectionName, AppendStoredProcedureName);
33+
deleteStoredProcedureLink = UriFactory.CreateStoredProcedureUri(databaseName, collectionOptions.CollectionName, DeleteStoredProcedureName);
3134
this.loggingOptions = loggingOptions;
3235
this.typeMap = typeMap;
3336
}
@@ -36,7 +39,8 @@ public async Task<IStorageEngine> Initialise()
3639
{
3740
await CreateDatabaseIfItDoesNotExist();
3841
await CreateCollectionIfItDoesNotExist();
39-
await CreateAppendStoredProcedureIfItDoesNotExist();
42+
await CreateStoredProcedureIfItDoesNotExist(AppendStoredProcedureName, "appendToStream.js");
43+
await CreateStoredProcedureIfItDoesNotExist(DeleteStoredProcedureName, "deleteStream.js");
4044

4145
return this;
4246
}
@@ -48,7 +52,7 @@ public async Task AppendToStream(string streamId, IEnumerable<StorageEvent> even
4852
try
4953
{
5054
var result = await client.ExecuteStoredProcedureAsync<dynamic>(
51-
storedProcLink,
55+
appendStoredProcedureLink,
5256
new RequestOptions { PartitionKey = new PartitionKey(streamId), ConsistencyLevel = collectionOptions.ConsistencyLevel },
5357
docs);
5458

@@ -90,6 +94,26 @@ public async Task<IReadOnlyCollection<StorageEvent>> ReadStreamForwards(string s
9094
return events.AsReadOnly();
9195
}
9296

97+
public async Task DeleteStream(string streamId)
98+
{
99+
while (true)
100+
{
101+
var result = await client.ExecuteStoredProcedureAsync<dynamic>(
102+
deleteStoredProcedureLink,
103+
new RequestOptions { PartitionKey = new PartitionKey(streamId), ConsistencyLevel = collectionOptions.ConsistencyLevel },
104+
streamId);
105+
106+
if ((bool)result.Response.continuation)
107+
{
108+
continue;
109+
}
110+
111+
loggingOptions.OnSuccess(ResponseInformation.FromWriteResponse(nameof(DeleteStream), result));
112+
113+
break;
114+
}
115+
}
116+
93117
private async Task CreateDatabaseIfItDoesNotExist()
94118
{
95119
await client.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseName });
@@ -117,18 +141,18 @@ private async Task CreateCollectionIfItDoesNotExist()
117141
await client.CreateDocumentCollectionIfNotExistsAsync(databaseUri, collection, requestOptions);
118142
}
119143

120-
private async Task CreateAppendStoredProcedureIfItDoesNotExist()
144+
private async Task CreateStoredProcedureIfItDoesNotExist(string procedureName, string resourceName)
121145
{
122146
var query = client.CreateStoredProcedureQuery(commitsLink)
123-
.Where(x => x.Id == AppendStoredProcedureName)
147+
.Where(x => x.Id == procedureName)
124148
.AsDocumentQuery();
125149

126150
if (!(await query.ExecuteNextAsync<StoredProcedure>()).Any())
127151
{
128152
await client.CreateStoredProcedureAsync(commitsLink, new StoredProcedure
129153
{
130-
Id = AppendStoredProcedureName,
131-
Body = Resources.GetString("appendToStream.js")
154+
Id = procedureName,
155+
Body = Resources.GetString(resourceName)
132156
});
133157
}
134158
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
function deleteStream(streamId) {
2+
var context = getContext();
3+
var collection = context.getCollection();
4+
var collectionLink = collection.getSelfLink();
5+
var response = getContext().getResponse();
6+
7+
// Based on https://raw.githubusercontent.com/Azure/azure-cosmosdb-js-server/master/samples/stored-procedures/bulkDelete.js
8+
var query = {
9+
query: "SELECT c._self FROM Commits c WHERE c.streamId = @streamId ORDER BY c.eventNumber ASC",
10+
parameters: [{ name: "@streamId", value: streamId }]
11+
};
12+
13+
var responseBody = {
14+
deleted: 0,
15+
continuation: true
16+
};
17+
18+
tryQueryAndDelete();
19+
20+
// Recursively runs the query w/ support for continuation tokens.
21+
// Calls tryDelete(documents) as soon as the query returns documents.
22+
function tryQueryAndDelete(continuation) {
23+
var requestOptions = { continuation: continuation };
24+
25+
var isAccepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, retrievedDocs, responseOptions) {
26+
if (err) throw err;
27+
28+
if (retrievedDocs.length > 0) {
29+
// Begin deleting documents as soon as documents are returned form the query results.
30+
// tryDelete() resumes querying after deleting; no need to page through continuation tokens.
31+
// - this is to prioritize writes over reads given timeout constraints.
32+
tryDelete(retrievedDocs);
33+
} else if (responseOptions.continuation) {
34+
// Else if the query came back empty, but with a continuation token; repeat the query w/ the token.
35+
tryQueryAndDelete(responseOptions.continuation);
36+
} else {
37+
// Else if there are no more documents and no continuation token - we are finished deleting documents.
38+
responseBody.continuation = false;
39+
response.setBody(responseBody);
40+
}
41+
});
42+
43+
// If we hit execution bounds - return continuation: true.
44+
if (!isAccepted) {
45+
response.setBody(responseBody);
46+
}
47+
}
48+
49+
// Recursively deletes documents passed in as an array argument.
50+
// Attempts to query for more on empty array.
51+
function tryDelete(documents) {
52+
if (documents.length > 0) {
53+
// Delete the first document in the array.
54+
var isAccepted = collection.deleteDocument(documents[0]._self, {}, function (err, responseOptions) {
55+
if (err) throw err;
56+
57+
responseBody.deleted++;
58+
documents.shift();
59+
60+
// Delete the next document in the array.
61+
tryDelete(documents);
62+
});
63+
64+
// If we hit execution bounds - return continuation: true.
65+
if (!isAccepted) {
66+
response.setBody(responseBody);
67+
}
68+
} else {
69+
// If the document array is empty, query for more documents.
70+
tryQueryAndDelete();
71+
}
72+
}
73+
}

SimpleEventStore.AzureDocumentDb/SimpleEventStore.AzureDocumentDb.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@
1313
<PackageProjectUrl>https://github.com/GivePenny/SimpleEventStore</PackageProjectUrl>
1414
<OutputTypeEx>library</OutputTypeEx>
1515
</PropertyGroup>
16+
<ItemGroup>
17+
<None Remove="Resources\deleteStream.js" />
18+
</ItemGroup>
1619
<ItemGroup>
1720
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.1.2" />
1821
</ItemGroup>
1922
<ItemGroup>
2023
<ProjectReference Include="..\SimpleEventStore\SimpleEventStore.csproj" />
2124
</ItemGroup>
2225
<ItemGroup>
26+
<EmbeddedResource Include="Resources\deleteStream.js" />
2327
<EmbeddedResource Include="Resources\appendToStream.js" />
2428
</ItemGroup>
2529
</Project>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using NUnit.Framework;
5+
using SimpleEventStore.Tests.Events;
6+
7+
namespace SimpleEventStore.Tests
8+
{
9+
[TestFixture]
10+
public abstract class EventStoreDeletingStream : EventStoreTestBase
11+
{
12+
[Test]
13+
public async Task when_deleting_stream_all_events_in_stream_are_deleted()
14+
{
15+
var streamId = Guid.NewGuid().ToString();
16+
var subject = await GetEventStore();
17+
var @event = new EventData(Guid.NewGuid(), new OrderCreated(streamId));
18+
19+
await subject.AppendToStream(streamId, 0, @event);
20+
21+
await subject.DeleteStream(streamId);
22+
23+
var stream = await subject.ReadStreamForwards(streamId);
24+
Assert.That(stream.Count, Is.EqualTo(0));
25+
}
26+
27+
[Test]
28+
public async Task when_deleting_stream_events_in_other_streams_are_preserved()
29+
{
30+
var deleteStreamId = Guid.NewGuid().ToString();
31+
var keepStreamId = Guid.NewGuid().ToString();
32+
var subject = await GetEventStore();
33+
34+
await subject.AppendToStream(keepStreamId, 0, new EventData(Guid.NewGuid(), new OrderCreated(keepStreamId)));
35+
await subject.AppendToStream(deleteStreamId, 0, new EventData(Guid.NewGuid(), new OrderCreated(deleteStreamId)));
36+
37+
await subject.DeleteStream(deleteStreamId);
38+
39+
var stream = await subject.ReadStreamForwards(keepStreamId);
40+
Assert.That(stream.Count, Is.EqualTo(1));
41+
}
42+
}
43+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using SimpleEventStore.InMemory;
4+
5+
namespace SimpleEventStore.Tests.InMemory
6+
{
7+
[TestFixture]
8+
public class InMemoryEventStoreDeletingStream : EventStoreDeletingStream
9+
{
10+
protected override Task<IStorageEngine> CreateStorageEngine()
11+
{
12+
return Task.FromResult((IStorageEngine)new InMemoryStorageEngine());
13+
}
14+
}
15+
}

SimpleEventStore.sln

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 14
4-
VisualStudioVersion = 14.0.25420.1
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.28010.2041
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleEventStore", "SimpleEventStore\SimpleEventStore.csproj", "{73235465-69BF-4762-B8C5-20C8E45795FF}"
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEventStore", "SimpleEventStore\SimpleEventStore.csproj", "{73235465-69BF-4762-B8C5-20C8E45795FF}"
77
EndProject
8-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CD241C9A-0A56-42C9-8309-D68890C78B64}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEventStore.Tests", "SimpleEventStore.Tests\SimpleEventStore.Tests.csproj", "{ACA6B3AE-FCB9-45F4-9D6B-66196F98F819}"
99
EndProject
10-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleEventStore.Tests", "SimpleEventStore.Tests\SimpleEventStore.Tests.csproj", "{ACA6B3AE-FCB9-45F4-9D6B-66196F98F819}"
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEventStore.AzureDocumentDb", "SimpleEventStore.AzureDocumentDb\SimpleEventStore.AzureDocumentDb.csproj", "{48C71940-D9B0-446A-9F3D-E6275CD43440}"
1111
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleEventStore.AzureDocumentDb", "SimpleEventStore.AzureDocumentDb\SimpleEventStore.AzureDocumentDb.csproj", "{48C71940-D9B0-446A-9F3D-E6275CD43440}"
13-
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleEventStore.AzureDocumentDb.Tests", "SimpleEventStore.AzureDocumentDb.Tests\SimpleEventStore.AzureDocumentDb.Tests.csproj", "{205A7F81-A496-4400-9A97-D156F88B7883}"
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEventStore.AzureDocumentDb.Tests", "SimpleEventStore.AzureDocumentDb.Tests\SimpleEventStore.AzureDocumentDb.Tests.csproj", "{205A7F81-A496-4400-9A97-D156F88B7883}"
1513
EndProject
1614
Global
1715
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,8 +37,7 @@ Global
3937
GlobalSection(SolutionProperties) = preSolution
4038
HideSolutionNode = FALSE
4139
EndGlobalSection
42-
GlobalSection(NestedProjects) = preSolution
43-
{ACA6B3AE-FCB9-45F4-9D6B-66196F98F819} = {CD241C9A-0A56-42C9-8309-D68890C78B64}
44-
{205A7F81-A496-4400-9A97-D156F88B7883} = {CD241C9A-0A56-42C9-8309-D68890C78B64}
40+
GlobalSection(ExtensibilityGlobals) = postSolution
41+
SolutionGuid = {936D1C6C-3474-4DFB-A983-FF729D21B72B}
4542
EndGlobalSection
4643
EndGlobal

SimpleEventStore/EventStore.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public Task AppendToStream(string streamId, int expectedVersion, params EventDat
2020
var storageEvents = new List<StorageEvent>();
2121
var eventVersion = expectedVersion;
2222

23-
for (int i = 0; i < events.Length; i++)
23+
for (var i = 0; i < events.Length; i++)
2424
{
2525
storageEvents.Add(new StorageEvent(streamId, events[i], ++eventVersion));
2626
}
@@ -41,5 +41,12 @@ public Task<IReadOnlyCollection<StorageEvent>> ReadStreamForwards(string streamI
4141

4242
return engine.ReadStreamForwards(streamId, startPosition, numberOfEventsToRead);
4343
}
44+
45+
public Task DeleteStream(string streamId)
46+
{
47+
Guard.IsNotNullOrEmpty(nameof(streamId), streamId);
48+
49+
return engine.DeleteStream(streamId);
50+
}
4451
}
4552
}

SimpleEventStore/IStorageEngine.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ public interface IStorageEngine
1111
Task<IReadOnlyCollection<StorageEvent>> ReadStreamForwards(string streamId, int startPosition, int numberOfEventsToRead);
1212

1313
Task<IStorageEngine> Initialise();
14+
15+
Task DeleteStream(string streamId);
1416
}
1517
}

SimpleEventStore/InMemory/InMemoryStorageEngine.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ public Task AppendToStream(string streamId, IEnumerable<StorageEvent> events)
3333
});
3434
}
3535

36+
public Task DeleteStream(string streamId)
37+
{
38+
if (!streams.ContainsKey(streamId))
39+
{
40+
return Task.CompletedTask;
41+
}
42+
43+
foreach (var @event in streams[streamId])
44+
{
45+
allEvents.Remove(@event);
46+
}
47+
48+
streams.TryRemove(streamId, out var removedStream);
49+
50+
return Task.CompletedTask;
51+
}
52+
3653
private void AddEventsToAllStream(IEnumerable<StorageEvent> events)
3754
{
3855
foreach (var e in events)

0 commit comments

Comments
 (0)