Skip to content

Commit

Permalink
History apis (#18)
Browse files Browse the repository at this point in the history
the apis to get objects at a given time/commit were poor, so I've changed that
  • Loading branch information
hahn-kev authored Nov 7, 2024
1 parent c7dea1f commit 481acfc
Show file tree
Hide file tree
Showing 11 changed files with 505 additions and 103 deletions.
19 changes: 15 additions & 4 deletions src/SIL.Harmony.Core/QueryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,21 @@ public static IQueryable<T> WhereAfter<T>(this IQueryable<T> queryable, T after)
|| (after.HybridDateTime.DateTime == c.HybridDateTime.DateTime && after.HybridDateTime.Counter == c.HybridDateTime.Counter && after.Id < c.Id));
}

public static IQueryable<T> WhereBefore<T>(this IQueryable<T> queryable, T after) where T : CommitBase
public static IQueryable<T> WhereBefore<T>(this IQueryable<T> queryable, T before, bool inclusive = false) where T : CommitBase
{
return queryable.Where(c => c.HybridDateTime.DateTime < after.HybridDateTime.DateTime
|| (c.HybridDateTime.DateTime == after.HybridDateTime.DateTime && c.HybridDateTime.Counter < after.HybridDateTime.Counter)
|| (c.HybridDateTime.DateTime == after.HybridDateTime.DateTime && c.HybridDateTime.Counter == after.HybridDateTime.Counter && c.Id < after.Id));
if (inclusive)
{

return queryable.Where(c => c.HybridDateTime.DateTime < before.HybridDateTime.DateTime
|| (c.HybridDateTime.DateTime == before.HybridDateTime.DateTime &&
c.HybridDateTime.Counter < before.HybridDateTime.Counter)
|| (c.HybridDateTime.DateTime == before.HybridDateTime.DateTime &&
c.HybridDateTime.Counter == before.HybridDateTime.Counter &&
c.Id < before.Id)
|| c.Id == before.Id);
}
return queryable.Where(c => c.HybridDateTime.DateTime < before.HybridDateTime.DateTime
|| (c.HybridDateTime.DateTime == before.HybridDateTime.DateTime && c.HybridDateTime.Counter < before.HybridDateTime.Counter)
|| (c.HybridDateTime.DateTime == before.HybridDateTime.DateTime && c.HybridDateTime.Counter == before.HybridDateTime.Counter && c.Id < before.Id));
}
}
4 changes: 2 additions & 2 deletions src/SIL.Harmony.Tests/DataModelPerformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ internal static async Task BulkInsertChanges(DataModelTestBase dataModelTest, in
};
commit.SetParentHash(parentHash);
parentHash = commit.Hash;
dataModelTest.DbContext.Commits.Add(commit);
dataModelTest.DbContext.Snapshots.Add(new ObjectSnapshot(await change.NewEntity(commit, null!), commit, true));
dataModelTest.DbContext.Add(commit);
dataModelTest.DbContext.Add(new ObjectSnapshot(await change.NewEntity(commit, null!), commit, true));
}

await dataModelTest.DbContext.SaveChangesAsync();
Expand Down
6 changes: 3 additions & 3 deletions src/SIL.Harmony.Tests/DataModelReferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public async Task SnapshotsDontGetMutatedByADelete()
{
var refAdd = await WriteNextChange(new AddAntonymReferenceChange(_word1Id, _word2Id));
await WriteNextChange(new DeleteChange<Word>(_word2Id));
var entitySnapshot1 = await DataModel.GetEntitySnapshotAtTime(refAdd.DateTime, _word1Id);
entitySnapshot1.Should().NotBeNull();
entitySnapshot1!.Entity.Is<Word>().AntonymId.Should().Be(_word2Id);
var word = await DataModel.GetAtCommit<Word>(refAdd.Id, _word1Id);
word.Should().NotBeNull();
word.AntonymId.Should().Be(_word2Id);
}

[Fact]
Expand Down
166 changes: 164 additions & 2 deletions src/SIL.Harmony.Tests/Db/QueryHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class QueryHelperTests
private Commit Commit(Guid id, HybridDateTime hybridDateTime) =>
new(id) { ClientId = Guid.Empty, HybridDateTime = hybridDateTime };

private ObjectSnapshot Snapshot(Guid commitId, HybridDateTime hybridDateTime) => ObjectSnapshot.ForTesting(Commit(commitId, hybridDateTime));
private ObjectSnapshot Snapshot(Guid commitId, HybridDateTime hybridDateTime) =>
ObjectSnapshot.ForTesting(Commit(commitId, hybridDateTime));

private IQueryable<Commit> Commits(IEnumerable<Commit> commits) => commits.AsQueryable();
private IQueryable<ObjectSnapshot> Snapshots(IEnumerable<ObjectSnapshot> snapshots) => snapshots.AsQueryable();
Expand Down Expand Up @@ -283,6 +284,111 @@ public void WhereAfterSnapshot_FiltersOnId()
]);
}



[Fact]
public void WhereBeforeSnapshot_FiltersOnDate()
{
var filterCommit = Commit(Guid.NewGuid(), Time(2, 0));
var snapshots = Snapshots([
Snapshot(id1, Time(1, 0)),
Snapshot(id2, Time(3, 0)),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime),
Snapshot(id3, Time(4, 0)),
]);
snapshots.WhereBefore(filterCommit).Select(c => c.CommitId).Should().BeEquivalentTo([
id1
]);
}

[Fact]
public void WhereBeforeSnapshotInclusive_FiltersOnDate()
{
var filterCommit = Commit(Guid.NewGuid(), Time(2, 0));
var snapshots = Snapshots([
Snapshot(id1, Time(1, 0)),
Snapshot(id2, Time(3, 0)),
Snapshot(id3, Time(4, 0)),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime)
]);
snapshots.WhereBefore(filterCommit, inclusive: true).Select(c => c.CommitId).Should().BeEquivalentTo([
id1,
filterCommit.Id
]);
}

[Fact]
public void WhereBeforeSnapshot_FiltersOnCounter()
{
var filterCommit = Commit(Guid.NewGuid(), Time(1, 2));
var snapshots = Snapshots([
Snapshot(id1, Time(1, 1)),
Snapshot(id2, Time(1, 3)),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime),
Snapshot(id3, Time(1, 4)),
]);
snapshots.WhereBefore(filterCommit).Select(c => c.CommitId).Should().BeEquivalentTo([
id1,
]);
}

[Fact]
public void WhereBeforeSnapshotInclusive_FiltersOnCounter()
{
var filterCommit = Commit(Guid.NewGuid(), Time(1, 2));
var snapshots = Snapshots([
Snapshot(id1, Time(1, 1)),
Snapshot(id2, Time(1, 3)),
Snapshot(id3, Time(1, 4)),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime),
]);
snapshots.WhereBefore(filterCommit, inclusive: true).Select(c => c.CommitId).Should().BeEquivalentTo([
id1,
filterCommit.Id
]);
}

[Fact]
public void WhereBeforeSnapshot_FiltersOnId()
{
var hybridDateTime = Time(1, 1);
Guid[] ids = OrderedIds(4);
var commitId1 = ids[0];
var filterCommit = Commit(ids[1], hybridDateTime);
var commitId2 = ids[2];
var commitId3 = ids[3];
var snapshots = Snapshots([
Snapshot(commitId1, hybridDateTime),
Snapshot(commitId2, hybridDateTime),
Snapshot(commitId3, hybridDateTime),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime),
]);
snapshots.WhereBefore(filterCommit).Select(c => c.CommitId).Should().BeEquivalentTo([
commitId1
]);
}

[Fact]
public void WhereBeforeSnapshotInclusive_FiltersOnId()
{
var hybridDateTime = Time(1, 1);
Guid[] ids = OrderedIds(4);
var commitId1 = ids[0];
var filterCommit = Commit(ids[1], hybridDateTime);
var commitId2 = ids[2];
var commitId3 = ids[3];
var snapshots = Snapshots([
Snapshot(commitId1, hybridDateTime),
Snapshot(filterCommit.Id, filterCommit.HybridDateTime),
Snapshot(commitId2, hybridDateTime),
Snapshot(commitId3, hybridDateTime),
]);
snapshots.WhereBefore(filterCommit, inclusive: true).Select(c => c.CommitId).Should().BeEquivalentTo([
commitId1,
filterCommit.Id
]);
}

[Fact]
public void WhereBeforeCommit_FiltersOnDate()
{
Expand All @@ -291,12 +397,29 @@ public void WhereBeforeCommit_FiltersOnDate()
Commit(id1, Time(1, 0)),
Commit(id2, Time(3, 0)),
Commit(id3, Time(4, 0)),
filterCommit
]);
commits.WhereBefore(filterCommit).Select(c => c.Id).Should().BeEquivalentTo([
id1
]);
}

[Fact]
public void WhereBeforeCommitInclusive_FiltersOnDate()
{
var filterCommit = Commit(Guid.NewGuid(), Time(2, 0));
var commits = Commits([
Commit(id1, Time(1, 0)),
Commit(id2, Time(3, 0)),
Commit(id3, Time(4, 0)),
filterCommit
]);
commits.WhereBefore(filterCommit, inclusive: true).Select(c => c.Id).Should().BeEquivalentTo([
id1,
filterCommit.Id
]);
}

[Fact]
public void WhereBeforeCommit_FiltersOnCounter()
{
Expand All @@ -305,9 +428,26 @@ public void WhereBeforeCommit_FiltersOnCounter()
Commit(id1, Time(1, 1)),
Commit(id2, Time(1, 3)),
Commit(id3, Time(1, 4)),
filterCommit
]);
commits.WhereBefore(filterCommit).Select(c => c.Id).Should().BeEquivalentTo([
id1
id1,
]);
}

[Fact]
public void WhereBeforeCommitInclusive_FiltersOnCounter()
{
var filterCommit = Commit(Guid.NewGuid(), Time(1, 2));
var commits = Commits([
Commit(id1, Time(1, 1)),
Commit(id2, Time(1, 3)),
Commit(id3, Time(1, 4)),
filterCommit
]);
commits.WhereBefore(filterCommit, inclusive: true).Select(c => c.Id).Should().BeEquivalentTo([
id1,
filterCommit.Id
]);
}

Expand All @@ -324,9 +464,31 @@ public void WhereBeforeCommit_FiltersOnId()
Commit(commitId1, hybridDateTime),
Commit(commitId2, hybridDateTime),
Commit(commitId3, hybridDateTime),
filterCommit
]);
commits.WhereBefore(filterCommit).Select(c => c.Id).Should().BeEquivalentTo([
commitId1
]);
}

[Fact]
public void WhereBeforeCommitInclusive_FiltersOnId()
{
var hybridDateTime = Time(1, 1);
Guid[] ids = OrderedIds(4);
var commitId1 = ids[0];
var filterCommit = Commit(ids[1], hybridDateTime);
var commitId2 = ids[2];
var commitId3 = ids[3];
var commits = Commits([
Commit(commitId1, hybridDateTime),
filterCommit,
Commit(commitId2, hybridDateTime),
Commit(commitId3, hybridDateTime),
]);
commits.WhereBefore(filterCommit, inclusive: true).Select(c => c.Id).Should().BeEquivalentTo([
commitId1,
filterCommit.Id
]);
}
}
6 changes: 3 additions & 3 deletions src/SIL.Harmony.Tests/DbContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task CanRoundTripDatesFromEf(int offset)
ClientId = Guid.NewGuid(),
HybridDateTime = new HybridDateTime(expectedDateTime, 0)
};
DbContext.Commits.Add(commit);
DbContext.Add(commit);
await DbContext.SaveChangesAsync();
var actualCommit = await DbContext.Commits.AsNoTracking().SingleOrDefaultAsyncEF(c => c.Id == commitId);
actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF");
Expand All @@ -46,7 +46,7 @@ public async Task CanRoundTripDatesFromLinq2Db(int offset)
var commitId = Guid.NewGuid();
var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset));

await DbContext.Commits.ToLinqToDBTable().AsValueInsertable()
await DbContext.Set<Commit>().ToLinqToDBTable().AsValueInsertable()
.Value(c => c.Id, commitId)
.Value(c => c.ClientId, Guid.NewGuid())
.Value(c => c.HybridDateTime.DateTime, expectedDateTime)
Expand Down Expand Up @@ -74,7 +74,7 @@ public async Task CanFilterCommitsByDateTime(double scale)
for (int i = 0; i < 50; i++)
{
var offset = new TimeSpan((long)(i * scale));
DbContext.Commits.Add(new Commit
DbContext.Add(new Commit
{
ClientId = Guid.NewGuid(),
HybridDateTime = new HybridDateTime(baseDateTime.Add(offset), 0)
Expand Down
57 changes: 55 additions & 2 deletions src/SIL.Harmony.Tests/ModelSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,59 @@ public async Task ModelSnapshotShowsMultipleChanges()
snapshot.LastChange.Should().Be(secondChange.DateTime);
}

[Fact]
public async Task CanGetWordForASpecificCommit()
{
var entityId = Guid.NewGuid();
var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
await ClearNonRootSnapshots();
var firstWord = await DataModel.GetAtCommit<Word>(firstCommit.Id, entityId);
firstWord.Should().NotBeNull();
firstWord.Text.Should().Be("first");

var secondWord = await DataModel.GetAtCommit<Word>(secondCommit.Id, entityId);
secondWord.Should().NotBeNull();
secondWord.Text.Should().Be("second");

var thirdWord = await DataModel.GetAtCommit<Word>(thirdCommit.Id, entityId);
thirdWord.Should().NotBeNull();
thirdWord.Text.Should().Be("third");
}

[Fact]
public async Task CanGetWordForASpecificTime()
{
var entityId = Guid.NewGuid();
var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
//ensures that SnapshotWorker.ApplyCommitsToSnapshots will be called when getting the snapshots
await ClearNonRootSnapshots();
var firstWord = await DataModel.GetAtTime<Word>(firstCommit.DateTime.AddMinutes(5), entityId);
firstWord.Should().NotBeNull();
firstWord.Text.Should().Be("first");

var secondWord = await DataModel.GetAtTime<Word>(secondCommit.DateTime.AddMinutes(5), entityId);
secondWord.Should().NotBeNull();
secondWord.Text.Should().Be("second");

//just before the 3rd commit should still be second
secondWord = await DataModel.GetAtTime<Word>(thirdCommit.DateTime.Subtract(TimeSpan.FromSeconds(5)), entityId);
secondWord.Should().NotBeNull();
secondWord.Text.Should().Be("second");

var thirdWord = await DataModel.GetAtTime<Word>(thirdCommit.DateTime.AddMinutes(5), entityId);
thirdWord.Should().NotBeNull();
thirdWord.Text.Should().Be("third");
}

private Task ClearNonRootSnapshots()
{
return DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync();
}

[Theory]
[InlineData(10)]
[InlineData(100)]
Expand All @@ -56,7 +109,7 @@ public async Task CanGetSnapshotFromEarlier(int changeCount)

for (int i = 0; i < changeCount; i++)
{
var snapshots = await DataModel.GetSnapshotsAt(changes[i].DateTime);
var snapshots = await DataModel.GetSnapshotsAtCommit(changes[i]);
var entry = snapshots[entityId].Entity.Is<Word>();
entry.Text.Should().Be($"change {i}");
snapshots.Values.Should().HaveCount(1 + i);
Expand All @@ -77,7 +130,7 @@ await AddCommitsViaSync(Enumerable.Range(0, changeCount)
//delete snapshots so when we get at then we need to re-apply
await DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync();

var computedModelSnapshots = await DataModel.GetSnapshotsAt(latestSnapshot.Commit.DateTime);
var computedModelSnapshots = await DataModel.GetSnapshotsAtCommit(latestSnapshot.Commit);

var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value;
entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject));
Expand Down
Loading

0 comments on commit 481acfc

Please sign in to comment.