From 785137b7b80fdf68ad312edd939248df66680a77 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 11:30:06 +0700 Subject: [PATCH 1/2] Add DataModel.GetBeforeCommit to get entity state before a commit Adds GetBeforeCommit(Guid commitId, Guid entityId) and GetBeforeCommit(Commit commit, Guid entityId), mirroring GetAtCommit but returning the entity state as it was immediately before the given commit was applied. The methods return default when there is no state before the commit (i.e. the commit is the first commit, or it is the commit that created the entity). The snapshot-resolution logic shared with GetAtCommit is extracted into a private GetSnapshotAtCommit helper. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01MveQ25eqoruavs2yTPTpFB --- src/SIL.Harmony.Tests/ModelSnapshotTests.cs | 22 +++++++++++++++ src/SIL.Harmony/DataModel.cs | 30 +++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs index a638445..c829320 100644 --- a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs +++ b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs @@ -54,6 +54,28 @@ public async Task CanGetWordForASpecificCommit() thirdWord.Text.Should().Be("third"); } + [Fact] + public async Task CanGetWordBeforeASpecificCommit() + { + 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(); + + //there's no state before the first commit created the entity + var beforeFirst = await DataModel.GetBeforeCommit(firstCommit.Id, entityId); + beforeFirst.Should().BeNull(); + + var beforeSecond = await DataModel.GetBeforeCommit(secondCommit.Id, entityId); + beforeSecond.Should().NotBeNull(); + beforeSecond!.Text.Should().Be("first"); + + var beforeThird = await DataModel.GetBeforeCommit(thirdCommit.Id, entityId); + beforeThird.Should().NotBeNull(); + beforeThird!.Text.Should().Be("second"); + } + [Fact] public async Task CanGetWordForASpecificTime() { diff --git a/src/SIL.Harmony/DataModel.cs b/src/SIL.Harmony/DataModel.cs index 8707fcb..b33cf91 100644 --- a/src/SIL.Harmony/DataModel.cs +++ b/src/SIL.Harmony/DataModel.cs @@ -326,11 +326,37 @@ public async Task GetAtCommit(Guid commitId, Guid entityId) } public async Task GetAtCommit(Commit commit, Guid entityId) + { + var snapshot = await GetSnapshotAtCommit(commit, entityId); + ArgumentNullException.ThrowIfNull(snapshot); + return (T)snapshot.Entity.DbObject; + } + + public async Task GetBeforeCommit(Guid commitId, Guid entityId) + { + await using var repo = await _crdtRepositoryFactory.CreateRepository(); + return await GetBeforeCommit(await repo.CurrentCommits().SingleAsync(c => c.Id == commitId), + entityId); + } + + public async Task GetBeforeCommit(Commit commit, Guid entityId) + { + await using var repo = await _crdtRepositoryFactory.CreateRepository(); + var previousCommit = await repo.FindPreviousCommit(commit); + //there's no state before the first commit + if (previousCommit is null) return default; + var snapshot = await GetSnapshotAtCommit(previousCommit, entityId); + //the entity did not exist before the given commit + if (snapshot is null) return default; + return (T)snapshot.Entity.DbObject; + } + + private async Task GetSnapshotAtCommit(Commit commit, Guid entityId) { await using var repo = await _crdtRepositoryFactory.CreateRepository(); var repository = repo.GetScopedRepository(commit); var snapshot = await repository.GetCurrentSnapshotByObjectId(entityId, false); - ArgumentNullException.ThrowIfNull(snapshot); + if (snapshot is null) return null; var newCommits = await repository.CurrentCommits() .Include(c => c.ChangeEntities) .WhereAfter(snapshot.Commit) @@ -347,7 +373,7 @@ public async Task GetAtCommit(Commit commit, Guid entityId) snapshot = snapshots[snapshot.EntityId]; } - return (T)snapshot.Entity.DbObject; + return snapshot; } public async Task GetSyncState() From a31bc8c2f727b8faa93d7ac261579097840e21d2 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 16 Jul 2026 11:41:48 +0700 Subject: [PATCH 2/2] Create CrdtRepository once and thread it through commit lookups Previously each of GetAtCommit/GetBeforeCommit and the shared GetSnapshotAtCommit helper created its own repository from the factory, so a single call created two or three repositories. Create the repository at the public entry point and pass it through to the private helpers instead. --- src/SIL.Harmony/DataModel.cs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/SIL.Harmony/DataModel.cs b/src/SIL.Harmony/DataModel.cs index b33cf91..69455bb 100644 --- a/src/SIL.Harmony/DataModel.cs +++ b/src/SIL.Harmony/DataModel.cs @@ -321,13 +321,19 @@ public async Task GetAtTime(DateTimeOffset time, Guid entityId) public async Task GetAtCommit(Guid commitId, Guid entityId) { await using var repo = await _crdtRepositoryFactory.CreateRepository(); - return await GetAtCommit(await repo.CurrentCommits().SingleAsync(c => c.Id == commitId), - entityId); + var commit = await repo.CurrentCommits().SingleAsync(c => c.Id == commitId); + return await GetAtCommit(commit, entityId, repo); } public async Task GetAtCommit(Commit commit, Guid entityId) { - var snapshot = await GetSnapshotAtCommit(commit, entityId); + await using var repo = await _crdtRepositoryFactory.CreateRepository(); + return await GetAtCommit(commit, entityId, repo); + } + + private async Task GetAtCommit(Commit commit, Guid entityId, CrdtRepository repo) + { + var snapshot = await GetSnapshotAtCommit(commit, entityId, repo); ArgumentNullException.ThrowIfNull(snapshot); return (T)snapshot.Entity.DbObject; } @@ -335,25 +341,29 @@ public async Task GetAtCommit(Commit commit, Guid entityId) public async Task GetBeforeCommit(Guid commitId, Guid entityId) { await using var repo = await _crdtRepositoryFactory.CreateRepository(); - return await GetBeforeCommit(await repo.CurrentCommits().SingleAsync(c => c.Id == commitId), - entityId); + var commit = await repo.CurrentCommits().SingleAsync(c => c.Id == commitId); + return await GetBeforeCommit(commit, entityId, repo); } public async Task GetBeforeCommit(Commit commit, Guid entityId) { await using var repo = await _crdtRepositoryFactory.CreateRepository(); + return await GetBeforeCommit(commit, entityId, repo); + } + + private async Task GetBeforeCommit(Commit commit, Guid entityId, CrdtRepository repo) + { var previousCommit = await repo.FindPreviousCommit(commit); //there's no state before the first commit if (previousCommit is null) return default; - var snapshot = await GetSnapshotAtCommit(previousCommit, entityId); + var snapshot = await GetSnapshotAtCommit(previousCommit, entityId, repo); //the entity did not exist before the given commit if (snapshot is null) return default; return (T)snapshot.Entity.DbObject; } - private async Task GetSnapshotAtCommit(Commit commit, Guid entityId) + private async Task GetSnapshotAtCommit(Commit commit, Guid entityId, CrdtRepository repo) { - await using var repo = await _crdtRepositoryFactory.CreateRepository(); var repository = repo.GetScopedRepository(commit); var snapshot = await repository.GetCurrentSnapshotByObjectId(entityId, false); if (snapshot is null) return null;