Skip to content

Commit a179986

Browse files
myieyehahn-kev
andauthored
Project only the latest snapshot of each entity in a single batch (#25)
* Introduce snapshot scope for better managing what gets projected * simplify snapshot scope, remove AddIfNew since it didn't seem to make a difference which was used * fix issue of inserting snapshots which were already created * Remove snapshot scope and project all latest snapshots in single batch * add failed sync dump which will export the commits, the entity which the update failed on and the error message * Don't make assumptions about the order of snapshots when projecting * Verify new snapshots * write test to ensure that a word reference gets updated even when it's in a root snapshot that was created during this snapshot update * only loop over snapshots once when adding them --------- Co-authored-by: Kevin Hahn <[email protected]>
1 parent 939481f commit a179986

15 files changed

+273
-91
lines changed

src/SIL.Harmony.Sample/Changes/NewWordChange.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
namespace SIL.Harmony.Sample.Changes;
66

7-
public class NewWordChange(Guid entityId, string text, string? note = null) : CreateChange<Word>(entityId), ISelfNamedType<NewWordChange>
7+
public class NewWordChange(Guid entityId, string text, string? note = null, Guid? antonymId = null) : CreateChange<Word>(entityId), ISelfNamedType<NewWordChange>
88
{
99
public string Text { get; } = text;
1010
public string? Note { get; } = note;
11+
public Guid? AntonymId { get; } = antonymId;
1112

12-
public override ValueTask<Word> NewEntity(Commit commit, ChangeContext context)
13+
public override async ValueTask<Word> NewEntity(Commit commit, ChangeContext context)
1314
{
14-
return new(new Word { Text = Text, Note = Note, Id = EntityId });
15+
var antonymShouldBeNull = AntonymId is null || (await context.IsObjectDeleted(AntonymId.Value));
16+
return (new Word { Text = Text, Note = Note, Id = EntityId, AntonymId = antonymShouldBeNull ? null : AntonymId });
1517
}
1618
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using SIL.Harmony.Changes;
2+
using SIL.Harmony.Entities;
3+
using SIL.Harmony.Sample.Models;
4+
5+
namespace SIL.Harmony.Sample.Changes;
6+
7+
public class SetTagChange(Guid entityId, string text) : Change<Tag>(entityId), ISelfNamedType<SetTagChange>
8+
{
9+
public string Text { get; } = text;
10+
11+
public override ValueTask<Tag> NewEntity(Commit commit, ChangeContext context)
12+
{
13+
return new(new Tag()
14+
{
15+
Id = EntityId,
16+
Text = Text
17+
});
18+
}
19+
20+
21+
public override ValueTask ApplyChange(Tag entity, ChangeContext context)
22+
{
23+
entity.Text = Text;
24+
return ValueTask.CompletedTask;
25+
}
26+
}

src/SIL.Harmony.Sample/CrdtSampleKernel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi
4848
.Add<AddWordImageChange>()
4949
.Add<SetOrderChange<Definition>>()
5050
.Add<SetDefinitionPartOfSpeechChange>()
51+
.Add<SetTagChange>()
5152
.Add<DeleteChange<Word>>()
5253
.Add<DeleteChange<Definition>>()
5354
.Add<DeleteChange<Example>>()
55+
.Add<DeleteChange<Tag>>()
5456
;
5557
config.ObjectTypeListBuilder.DefaultAdapter()
5658
.Add<Word>()
@@ -61,7 +63,11 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi
6163
.HasForeignKey(d => d.WordId)
6264
.OnDelete(DeleteBehavior.Cascade);
6365
})
64-
.Add<Example>();
66+
.Add<Example>()
67+
.Add<Tag>(builder =>
68+
{
69+
builder.HasIndex(tag => tag.Text).IsUnique();
70+
});
6571
});
6672
return services;
6773
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using SIL.Harmony.Entities;
2+
3+
namespace SIL.Harmony.Sample.Models;
4+
5+
public class Tag : IObjectBase<Tag>
6+
{
7+
public required string Text { get; set; }
8+
9+
public Guid Id { get; init; }
10+
public DateTimeOffset? DeletedAt { get; set; }
11+
12+
public Guid[] GetReferences()
13+
{
14+
return [];
15+
}
16+
17+
public void RemoveReference(Guid id, Commit commit)
18+
{
19+
}
20+
21+
public IObjectBase Copy()
22+
{
23+
return new Tag
24+
{
25+
Id = Id,
26+
Text = Text,
27+
DeletedAt = DeletedAt
28+
};
29+
}
30+
}

src/SIL.Harmony.Tests/DataModelReferenceTests.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using SIL.Harmony.Changes;
1+
using Microsoft.EntityFrameworkCore;
2+
using SIL.Harmony.Changes;
23
using SIL.Harmony.Sample.Changes;
34
using SIL.Harmony.Sample.Models;
45
using SIL.Harmony.Tests;
@@ -65,4 +66,22 @@ public async Task DeleteRetroactivelyRemovesRefs()
6566
var entry = await DataModel.GetLatest<Word>(entityId3);
6667
entry!.AntonymId.Should().BeNull();
6768
}
68-
}
69+
70+
[Fact]
71+
public async Task DeleteDoesNotEffectARootSnapshotCreatedBeforeTheDelete()
72+
{
73+
var wordId = Guid.NewGuid();
74+
var initialWordCommit = await WriteNextChange(new NewWordChange(wordId, "entity1", antonymId: _word1Id), add: false);
75+
var deleteWordCommit = await WriteNextChange(DeleteWord(_word1Id), add: false);
76+
await AddCommitsViaSync([
77+
initialWordCommit,
78+
deleteWordCommit
79+
]);
80+
var snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == initialWordCommit.Id);
81+
var initialWord = (Word) snapshot.Entity;
82+
initialWord.AntonymId.Should().Be(_word1Id);
83+
snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == deleteWordCommit.Id && s.EntityId == wordId);
84+
var wordWithoutRef = (Word) snapshot.Entity;
85+
wordWithoutRef.AntonymId.Should().BeNull();
86+
}
87+
}

src/SIL.Harmony.Tests/DataModelSimpleChanges.Writing2ChangesAtOnceWithMergedHistory.verified.txt

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,12 @@
5050
},
5151
{
5252
$type: Commit,
53-
Snapshots: [
54-
{
55-
$type: ObjectSnapshot,
56-
Id: Guid_5,
57-
TypeName: Word,
58-
Entity: {
59-
$type: Word,
60-
Text: first,
61-
Note: a word note,
62-
Id: Guid_2
63-
},
64-
EntityId: Guid_2,
65-
EntityIsDeleted: false,
66-
CommitId: Guid_6,
67-
IsRoot: false
68-
}
69-
],
7053
Hash: Hash_2,
7154
ParentHash: Hash_1,
7255
ChangeEntities: [
7356
{
7457
$type: ChangeEntity<IChange>,
75-
CommitId: Guid_6,
58+
CommitId: Guid_5,
7659
EntityId: Guid_2,
7760
Change: {
7861
$type: SetWordNoteChange,
@@ -85,9 +68,9 @@
8568
CompareKey: {
8669
$type: ValueTuple<DateTimeOffset, long,
8770
Item1: DateTimeOffset_2,
88-
Item3: Guid_6
71+
Item3: Guid_5
8972
},
90-
Id: Guid_6,
73+
Id: Guid_5,
9174
HybridDateTime: {
9275
$type: HybridDateTime,
9376
DateTime: DateTimeOffset_2
@@ -103,7 +86,7 @@
10386
Snapshots: [
10487
{
10588
$type: ObjectSnapshot,
106-
Id: Guid_7,
89+
Id: Guid_6,
10790
TypeName: Word,
10891
Entity: {
10992
$type: Word,
@@ -112,7 +95,7 @@
11295
},
11396
EntityId: Guid_2,
11497
EntityIsDeleted: false,
115-
CommitId: Guid_8,
98+
CommitId: Guid_7,
11699
IsRoot: false
117100
}
118101
],
@@ -121,7 +104,7 @@
121104
ChangeEntities: [
122105
{
123106
$type: ChangeEntity<IChange>,
124-
CommitId: Guid_8,
107+
CommitId: Guid_7,
125108
EntityId: Guid_2,
126109
Change: {
127110
$type: SetWordTextChange,
@@ -134,9 +117,9 @@
134117
CompareKey: {
135118
$type: ValueTuple<DateTimeOffset, long,
136119
Item1: DateTimeOffset_3,
137-
Item3: Guid_8
120+
Item3: Guid_7
138121
},
139-
Id: Guid_8,
122+
Id: Guid_7,
140123
HybridDateTime: {
141124
$type: HybridDateTime,
142125
DateTime: DateTimeOffset_3
@@ -152,7 +135,7 @@
152135
Snapshots: [
153136
{
154137
$type: ObjectSnapshot,
155-
Id: Guid_9,
138+
Id: Guid_8,
156139
TypeName: Word,
157140
Entity: {
158141
$type: Word,
@@ -162,7 +145,7 @@
162145
},
163146
EntityId: Guid_2,
164147
EntityIsDeleted: false,
165-
CommitId: Guid_10,
148+
CommitId: Guid_9,
166149
IsRoot: false
167150
}
168151
],
@@ -171,7 +154,7 @@
171154
ChangeEntities: [
172155
{
173156
$type: ChangeEntity<IChange>,
174-
CommitId: Guid_10,
157+
CommitId: Guid_9,
175158
EntityId: Guid_2,
176159
Change: {
177160
$type: SetWordTextChange,
@@ -184,9 +167,9 @@
184167
CompareKey: {
185168
$type: ValueTuple<DateTimeOffset, long,
186169
Item1: DateTimeOffset_4,
187-
Item3: Guid_10
170+
Item3: Guid_9
188171
},
189-
Id: Guid_10,
172+
Id: Guid_9,
190173
HybridDateTime: {
191174
$type: HybridDateTime,
192175
DateTime: DateTimeOffset_4

src/SIL.Harmony.Tests/DataModelTestBase.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ public IChange SetWord(Guid entityId, string value)
131131
return new SetWordTextChange(entityId, value);
132132
}
133133

134+
public IChange DeleteWord(Guid entityId)
135+
{
136+
return new DeleteChange<Word>(entityId);
137+
}
138+
139+
public IChange SetTag(Guid entityId, string value)
140+
{
141+
return new SetTagChange(entityId, value);
142+
}
143+
144+
public IChange DeleteTag(Guid entityId)
145+
{
146+
return new DeleteChange<Tag>(entityId);
147+
}
148+
134149
public IChange NewDefinition(Guid wordId,
135150
string text,
136151
string partOfSpeech,

src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,27 @@
162162
Relational:TableName: Example
163163
Relational:ViewName:
164164
Relational:ViewSchema:
165+
EntityType: Tag
166+
Properties:
167+
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
168+
DeletedAt (DateTimeOffset?)
169+
SnapshotId (no field, Guid?) Shadow FK Index
170+
Text (string) Required Index
171+
Keys:
172+
Id PK
173+
Foreign keys:
174+
Tag {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull
175+
Indexes:
176+
SnapshotId Unique
177+
Text Unique
178+
Annotations:
179+
DiscriminatorProperty:
180+
Relational:FunctionName:
181+
Relational:Schema:
182+
Relational:SqlQuery:
183+
Relational:TableName: Tag
184+
Relational:ViewName:
185+
Relational:ViewSchema:
165186
EntityType: Word
166187
Properties:
167188
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd

src/SIL.Harmony.Tests/SnapshotTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,35 @@ await WriteChange(_localClientId,
7474
]);
7575
DbContext.Snapshots.Should().ContainSingle();
7676
}
77+
78+
[Fact]
79+
public async Task DontAddTheSameSnapshotTwice()
80+
{
81+
var entityId = Guid.NewGuid();
82+
await WriteNextChange(SetWord(entityId, "test root"));
83+
await WriteNextChange(SetWord(entityId, "test non root"));
84+
85+
await AddCommitsViaSync([
86+
//the order here is important, the second commit was causing the snapshot for 'test non root' to attempt to be inserted again
87+
await WriteNextChange(SetWord(Guid.NewGuid(), "test 1"), add: false),
88+
await WriteNextChange(SetWord(entityId, "test 2"), add: false),
89+
]);
90+
}
91+
92+
[Fact]
93+
public async Task CanRecreateUniqueConstraintConflictingValueInOneCommit()
94+
{
95+
var entityId = Guid.NewGuid();
96+
await WriteChange(_localClientId,
97+
DateTimeOffset.Now,
98+
[
99+
SetTag(entityId, "tag-1"),
100+
]);
101+
await WriteChange(_localClientId,
102+
DateTimeOffset.Now,
103+
[
104+
DeleteTag(entityId),
105+
SetTag(Guid.NewGuid(), "tag-1"),
106+
]);
107+
}
77108
}

src/SIL.Harmony/CrdtConfig.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ private void JsonTypeModifier(JsonTypeInfo typeInfo)
7474

7575
public bool RemoteResourcesEnabled { get; private set; }
7676
public string LocalResourceCachePath { get; set; } = Path.GetFullPath("./localResourceCache");
77+
public string FailedSyncOutputPath { get; set; } = Path.GetFullPath("./failedSyncs");
78+
7779
public void AddRemoteResourceEntity(string? cachePath = null)
7880
{
7981
RemoteResourcesEnabled = true;

0 commit comments

Comments
 (0)