Skip to content

Commit 3235bd4

Browse files
hahn-kevmyieye
andauthored
Use random commit id when migrating to add morph types (#2344)
* add a new debug helper to diagnose issue cloning a project * tweak the download projects test to fail due to morph type migrations. * only seed morphtypes when needed, Modify CreateMorphTypeChange to handle creating the same morph type multiple times due to migrations. * Implement CreateMorphType in MiniLcm * Add #2350 comments for follow up work * Return existing morph type if found in `CreateMorphType` to avoid duplicates. --------- Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
1 parent 4ccd57f commit 3235bd4

18 files changed

Lines changed: 140 additions & 80 deletions

File tree

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ internal MorphType FromLcmMorphType(IMoMorphType morphType)
592592
};
593593
}
594594

595+
public Task<MorphType> CreateMorphType(MorphType morphType)
596+
{
597+
throw new NotSupportedException("Morph types cannot be created in fwdata; they are predefined");
598+
}
599+
595600
public Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
596601
{
597602
var lcmMorphType = MorphTypeRepository.GetObject(id);

backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ public Task DeleteComplexFormType(Guid id)
126126
return Task.CompletedTask;
127127
}
128128

129+
public Task<MorphType> CreateMorphType(MorphType morphType)
130+
{
131+
DryRunRecords.Add(new DryRunRecord(nameof(CreateMorphType), $"Create morph type {morphType.Kind} ({morphType.Id})"));
132+
return Task.FromResult(morphType);
133+
}
134+
129135
public async Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
130136
{
131137
DryRunRecords.Add(new DryRunRecord(nameof(UpdateMorphType), $"Update morph type {id}"));

backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ async Task<SemanticDomain> IMiniLcmWriteApi.CreateSemanticDomain(SemanticDomain
6262
{
6363
return await HasCreated(semanticDomain, _api.GetSemanticDomains(), () => _api.CreateSemanticDomain(semanticDomain));
6464
}
65+
async Task<MorphType> IMiniLcmWriteApi.CreateMorphType(MorphType morphType)
66+
{
67+
return await HasCreated(morphType, _api.GetMorphTypes(), () => _api.CreateMorphType(morphType));
68+
}
6569
async Task<Publication> IMiniLcmWriteApi.CreatePublication(Publication publication)
6670
{
6771
return await HasCreated(publication, _api.GetPublications(), () => _api.CreatePublication(publication));

backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in
7272
}
7373

7474
// Morph types are created automatically for CRDT projects, so we update them instead of creating them
75+
// Optimize this to a simple foreach like above in #2350
7576
var importFromMorphTypes = await importFrom.GetMorphTypes().ToArrayAsync();
7677
var existingMorphTypes = await importTo.GetMorphTypes().ToArrayAsync();
7778
await MorphTypeSync.Sync(existingMorphTypes, importFromMorphTypes, importTo);

backend/FwLite/LcmCrdt.Tests/Data/DownloadProjectTests.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
using Microsoft.EntityFrameworkCore;
12
using Microsoft.Extensions.DependencyInjection;
2-
using Microsoft.Extensions.Logging;
3+
using SIL.Harmony.Core;
34

45
namespace LcmCrdt.Tests.Data;
56

67
public class DownloadProjectTests : IAsyncLifetime
78
{
89
private readonly RegressionTestHelper _helper = new("DownloadProject");
9-
private readonly MiniLcmApiFixture _apiFixture = MiniLcmApiFixture.Create(false);
10+
private static readonly Guid _projectId = new("B467051E-A492-4E5B-9C17-858D7797292C");//internal project Id of v2 project
11+
private readonly MiniLcmApiFixture _apiFixture = MiniLcmApiFixture.Create(false, _projectId);
1012

1113
public async Task InitializeAsync()
1214
{
13-
await _helper.InitializeAsync();
15+
await _helper.InitializeAsync(RegressionTestHelper.RegressionVersion.v2, withDataMigrations: true);
16+
//add a change after migration which creates MorphTypes
17+
await _helper.Services.GetRequiredService<IMiniLcmApi>().CreateEntry(new Entry()
18+
{
19+
LexemeForm = {{"en", "test"}}
20+
});
1421
await _apiFixture.InitializeAsync();
1522
}
1623

@@ -24,6 +31,17 @@ public async Task DisposeAsync()
2431
public async Task CanCreateANewProjectViaSync()
2532
{
2633
var remoteModel = _helper.Services.GetRequiredService<DataModel>();
34+
var remoteDb = await _helper.Services.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContextAsync();
35+
2736
await _apiFixture.DataModel.SyncWith(remoteModel);
37+
var localCommits = await _apiFixture.DbContext.Set<Commit>().DefaultOrder().ToListAsync();
38+
var remoteCommits = await remoteDb.Set<Commit>().DefaultOrder().ToListAsync();
39+
localCommits.Count.Should().Be(remoteCommits.Count);
40+
for (var i = localCommits.Count - 1; i >= 0; i--)
41+
{
42+
var localCommit = localCommits[i];
43+
var remoteCommit = remoteCommits[i];
44+
localCommit.Should().BeEquivalentTo(remoteCommit, "commit index {0} should be the same", i);
45+
}
2846
}
2947
}

backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,19 @@
66

77
namespace LcmCrdt.Tests.Data;
88

9-
public class RegressionTestHelper(string dbName): IAsyncLifetime
9+
public class RegressionTestHelper(string projectName) : IAsyncLifetime
1010
{
1111
private IHost _host = null!;
1212
private AsyncServiceScope _asyncScope;
13+
//unique db path per instance, so CurrentProjectService doesn't think a db has already run migrations
14+
private readonly CrdtProject _crdtProject = new(projectName, $"{projectName}-{Guid.NewGuid():N}.sqlite");
1315
public IServiceProvider Services => _asyncScope.ServiceProvider;
1416

1517
private async Task InitDbFromScripts(RegressionVersion version)
1618
{
1719
var initialSqlFile = GetFilePath($"Scripts/{version}.sql");
1820
var projectsService = _asyncScope.ServiceProvider.GetRequiredService<CurrentProjectService>();
19-
var crdtProject = new CrdtProject(dbName, $"{dbName}.sqlite");
20-
if (File.Exists(crdtProject.DbPath))
21-
{
22-
using var clearConn = new SqliteConnection($"Data Source={crdtProject.DbPath}");
23-
SqliteConnection.ClearPool(clearConn);
24-
File.Delete(crdtProject.DbPath);
25-
}
26-
projectsService.SetupProjectContextForNewDb(crdtProject);
21+
projectsService.SetupProjectContextForNewDb(_crdtProject);
2722
await using var lcmCrdtDbContext = await _asyncScope.ServiceProvider.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContextAsync();
2823
var sql = await File.ReadAllTextAsync(initialSqlFile);
2924
var dbConnection = lcmCrdtDbContext.Database.GetDbConnection();
@@ -48,14 +43,24 @@ public Task InitializeAsync()
4843
return InitializeAsync(RegressionVersion.v2);
4944
}
5045

51-
public async Task InitializeAsync(RegressionVersion version)
46+
public async Task InitializeAsync(RegressionVersion version, bool withDataMigrations = false)
5247
{
5348
var builder = Host.CreateEmptyApplicationBuilder(null);
5449
builder.Services.AddTestLcmCrdtClient();
5550
_host = builder.Build();
5651
var services = _host.Services;
5752
_asyncScope = services.CreateAsyncScope();
5853
await InitDbFromScripts(version);
54+
55+
// Data migrations are already on their way out. It doesn't really make sense for all tests to run them,
56+
// which would change a bunch of verified project snapshots.
57+
// When data migrations are removed (#2350), we should run this code unconditionally
58+
// at the end of InitDbFromScripts (instead of the current db-migration and refresh-project-data approach)
59+
// Because that's closer to the prod code.
60+
if (withDataMigrations)
61+
{
62+
await Services.GetRequiredService<CurrentProjectService>().SetupProjectContext(_crdtProject);
63+
}
5964
}
6065

6166
public async Task DisposeAsync()
@@ -66,6 +71,13 @@ public async Task DisposeAsync()
6671
{
6772
_host.Dispose();
6873
}
74+
75+
if (File.Exists(_crdtProject.DbPath))
76+
{
77+
using var connection = new SqliteConnection($"Data Source={_crdtProject.DbPath}");
78+
SqliteConnection.ClearPool(connection);
79+
File.Delete(_crdtProject.DbPath);
80+
}
6981
}
7082

7183
private static string GetFilePath(string name, [CallerFilePath] string sourceFile = "")

backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace LcmCrdt.Tests;
1414
public class MiniLcmApiFixture : IAsyncLifetime, IAsyncDisposable
1515
{
1616
private readonly bool _seedWs = true;
17+
private readonly Guid? _projectId;
1718
private AsyncServiceScope _services;
1819
private LcmCrdtDbContext? _crdtDbContext;
1920
public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService<IMiniLcmApi>();
@@ -31,22 +32,23 @@ public MiniLcmApiFixture()
3132
{
3233
}
3334

34-
public static MiniLcmApiFixture Create(bool seedWs = true)
35+
public static MiniLcmApiFixture Create(bool seedWs = true, Guid? projectId = null)
3536
{
36-
return new MiniLcmApiFixture(seedWs);
37+
return new MiniLcmApiFixture(seedWs, projectId);
3738
}
3839

39-
private MiniLcmApiFixture(bool seedWs = true)
40+
private MiniLcmApiFixture(bool seedWs = true, Guid? projectId = null)
4041
{
4142
_seedWs = seedWs;
43+
_projectId = projectId;
4244
}
4345

4446
public async Task InitializeAsync()
4547
{
46-
await InitializeAsync("sena-3");
48+
await InitializeAsync("sena-3", _projectId);
4749
}
4850

49-
public async Task InitializeAsync(string projectName)
51+
public async Task InitializeAsync(string projectName, Guid? projectId = null)
5052
{
5153
var db = $"file:{Guid.NewGuid():N}?mode=memory&cache=shared";
5254
if (Debugger.IsAttached)
@@ -72,12 +74,10 @@ public async Task InitializeAsync(string projectName)
7274
_crdtDbContext = await _services.ServiceProvider.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContextAsync();
7375
await _crdtDbContext.Database.OpenConnectionAsync();
7476
//can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db.
75-
var projectData = new ProjectData("Sena 3", projectName, Guid.NewGuid(), null, Guid.NewGuid());
76-
await CrdtProjectsService.InitProjectDb(_crdtDbContext,
77-
projectData);
78-
await currentProjectService.RefreshProjectData();
79-
// CreateProject would also seed morph types — so we need to do it manually here
80-
await PreDefinedData.AddPredefinedMorphTypes(_services.ServiceProvider.GetRequiredService<DataModel>(), projectData);
77+
var projectData = new ProjectData("Sena 3", projectName, projectId ?? Guid.NewGuid(), null, Guid.NewGuid());
78+
await CrdtProjectsService.InitProjectDb(_crdtDbContext, projectData);
79+
// Also trigger "data migrations" that CreateProject runs
80+
await currentProjectService.SetupProjectContext(crdtProject);
8181
if (_seedWs)
8282
{
8383
await Api.CreateWritingSystem(new WritingSystem()

backend/FwLite/LcmCrdt.Tests/MiniLcmTests/MorphTypeTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ public override async Task DisposeAsync()
1616
await base.DisposeAsync();
1717
await _fixture.DisposeAsync();
1818
}
19+
20+
// CRDT only, because fwdata does not support creating morph-types
21+
[Fact]
22+
public async Task CreateMorphType_Works()
23+
{
24+
var morphType = CanonicalMorphTypes.All.First().Value.Copy();
25+
var createdMorphType = await Api.CreateMorphType(morphType);
26+
createdMorphType.Should().NotBeNull();
27+
createdMorphType.Should().BeEquivalentTo(morphType);
28+
}
1929
}

backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,14 @@ public async IAsyncEnumerable<MorphType> GetMorphTypes()
373373
return await repo.MorphTypes.SingleOrDefaultAsync(m => m.Kind == kind);
374374
}
375375

376+
public async Task<MorphType> CreateMorphType(MorphType morphType)
377+
{
378+
//I don't like returning a different object than what the user requested, it feels very unexpected, however this is pretty much what happens in the change anyway and that can't be avoided
379+
if (await GetMorphType(morphType.Kind) is {} actualMorphType) return actualMorphType;
380+
await AddChange(new CreateMorphTypeChange(morphType));
381+
return await GetMorphType(morphType.Id) ?? throw NotFoundException.ForType<MorphType>(morphType.Id);
382+
}
383+
376384
public async Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
377385
{
378386
await AddChange(new JsonPatchChange<MorphType>(id, update.Patch));

backend/FwLite/LcmCrdt/CrdtProjectsService.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,15 @@ public virtual async Task<CrdtProject> CreateProject(CreateProjectRequest reques
166166
crdtProject.Data = projectData;
167167
await InitProjectDb(db, projectData);
168168
await currentProjectService.RefreshProjectData();
169-
// Morph types are predefined system data that must always exist — seed them
170-
// unconditionally so they're available before AfterCreate (e.g. import) runs.
171169
var dataModel = serviceScope.ServiceProvider.GetRequiredService<DataModel>();
172-
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData);
173170
if (request.SeedNewProjectData)
174171
await SeedSystemData(dataModel, projectData);
175172
await (request.AfterCreate?.Invoke(serviceScope.ServiceProvider, crdtProject) ?? Task.CompletedTask);
173+
// Ensure "data migrations" are executed on project creation (e.g. seeding morph types)
174+
// These should happen AFTER the initial download, so they can be run conditionally based on
175+
// the current state of the project.
176+
// probably just remove this in #2350
177+
await currentProjectService.SetupProjectContext(crdtProject);
176178
}
177179
catch (Exception e)
178180
{
@@ -245,7 +247,7 @@ internal static async Task InitProjectDb(LcmCrdtDbContext db, ProjectData data)
245247

246248
internal static async Task SeedSystemData(DataModel dataModel, ProjectData projectData)
247249
{
248-
// Note: AddPredefinedMorphTypes is seeded unconditionally in CreateProject, not here.
250+
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData);
249251
await PreDefinedData.AddPredefinedComplexFormTypes(dataModel, projectData);
250252
await PreDefinedData.AddPredefinedPartsOfSpeech(dataModel, projectData);
251253
await PreDefinedData.AddPredefinedSemanticDomains(dataModel, projectData);

0 commit comments

Comments
 (0)