Skip to content

Commit 2d34539

Browse files
myieyeclaude
andcommitted
Use named GUIDs for predefined-data seed commits
Each AddPredefined* now derives its seed commit-id per-project via UUIDv5 (Uuid.NewNameBased), so the LexBox CrdtCommits PK no longer collides across projects — the seed row gets attributed to its own project instead of whichever client pushed first. AddPredefined* signatures now take ProjectData instead of (clientId, projectId). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b1ab821 commit 2d34539

5 files changed

Lines changed: 52 additions & 28 deletions

File tree

backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ await CrdtProjectsService.InitProjectDb(_crdtDbContext,
7777
projectData);
7878
await currentProjectService.RefreshProjectData();
7979
// CreateProject would also seed morph types — so we need to do it manually here
80-
await PreDefinedData.AddPredefinedMorphTypes(_services.ServiceProvider.GetRequiredService<DataModel>(), projectData.ClientId);
80+
await PreDefinedData.AddPredefinedMorphTypes(_services.ServiceProvider.GetRequiredService<DataModel>(), projectData);
8181
if (_seedWs)
8282
{
8383
await Api.CreateWritingSystem(new WritingSystem()

backend/FwLite/LcmCrdt/CrdtProjectsService.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ public virtual async Task<CrdtProject> CreateProject(CreateProjectRequest reques
169169
// Morph types are predefined system data that must always exist — seed them
170170
// unconditionally so they're available before AfterCreate (e.g. import) runs.
171171
var dataModel = serviceScope.ServiceProvider.GetRequiredService<DataModel>();
172-
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData.ClientId);
172+
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData);
173173
if (request.SeedNewProjectData)
174-
await SeedSystemData(dataModel, projectData.ClientId);
174+
await SeedSystemData(dataModel, projectData);
175175
await (request.AfterCreate?.Invoke(serviceScope.ServiceProvider, crdtProject) ?? Task.CompletedTask);
176176
}
177177
catch (Exception e)
@@ -243,13 +243,13 @@ internal static async Task InitProjectDb(LcmCrdtDbContext db, ProjectData data)
243243
await db.SaveChangesAsync();
244244
}
245245

246-
internal static async Task SeedSystemData(DataModel dataModel, Guid clientId)
246+
internal static async Task SeedSystemData(DataModel dataModel, ProjectData projectData)
247247
{
248248
// Note: AddPredefinedMorphTypes is seeded unconditionally in CreateProject, not here.
249-
await PreDefinedData.AddPredefinedComplexFormTypes(dataModel, clientId);
250-
await PreDefinedData.AddPredefinedPartsOfSpeech(dataModel, clientId);
251-
await PreDefinedData.AddPredefinedSemanticDomains(dataModel, clientId);
252-
await PreDefinedData.AddPredefinedCustomViews(dataModel, clientId);
249+
await PreDefinedData.AddPredefinedComplexFormTypes(dataModel, projectData);
250+
await PreDefinedData.AddPredefinedPartsOfSpeech(dataModel, projectData);
251+
await PreDefinedData.AddPredefinedSemanticDomains(dataModel, projectData);
252+
await PreDefinedData.AddPredefinedCustomViews(dataModel, projectData);
253253
}
254254

255255
[GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")]

backend/FwLite/LcmCrdt/CurrentProjectService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,11 @@ async Task Execute()
111111
// Seed morph-types if missing (for existing projects created before morph-type support).
112112
// Must happen BEFORE FTS regeneration so headwords include morph-type tokens.
113113
// (querying Commits instead of MorphTypes, because the commit may not be projected yet)
114-
if (!await dbContext.Set<Commit>().AsNoTracking().AnyAsync(c => c.Id == PreDefinedData.MorphTypesSeedCommitId))
114+
var projectData = await dbContext.ProjectData.AsNoTracking().FirstAsync();
115+
if (!await dbContext.Set<Commit>().AsNoTracking().AnyAsync(c => c.Id == PreDefinedData.MorphTypesSeedCommitId(projectData.Id)))
115116
{
116117
var dataModel = services.GetRequiredService<DataModel>();
117-
var projectData = await dbContext.ProjectData.AsNoTracking().FirstAsync();
118-
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData.ClientId);
118+
await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData);
119119
}
120120

121121
if (EntrySearchServiceFactory is not null)

backend/FwLite/LcmCrdt/LcmCrdt.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
2020
<PackageReference Include="Refit" />
2121
<PackageReference Include="Refit.HttpClientFactory" />
22+
<PackageReference Include="UUIDNext" />
2223
</ItemGroup>
2324
<ItemGroup Condition="$([MSBuild]::IsOsPlatform('linux'))">
2425
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />

backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using LcmCrdt.Changes;
22
using SIL.Harmony;
3+
using UUIDNext;
34

45
namespace LcmCrdt.Objects;
56

@@ -15,20 +16,44 @@ public static class PreDefinedData
1516
public static readonly Guid AdjectivePartOfSpeechId = new("30d07580-5052-4d91-bc24-469b8b2d7df9");
1617
public static readonly Guid AdverbPartOfSpeechId = new("46e4fe08-ffa0-4c8b-bf98-2c56f38904d9");
1718

18-
internal static async Task AddPredefinedComplexFormTypes(DataModel dataModel, Guid clientId)
19+
// Per-kind UUIDv5 namespaces for deriving per-project seed commit-ids.
20+
// Per-project ids avoid the LexBox CrdtCommits PK collision a shared constant would cause —
21+
// otherwise the seed row gets attributed to whichever project pushed first.
22+
private static readonly Guid ComplexFormTypesSeedNamespace = new("dc60d2a9-0cc2-48ed-803c-a238a14b6eae");
23+
private static readonly Guid SemanticDomainsSeedNamespace = new("023faebb-711b-4d2f-a14f-a15621fc66bc");
24+
private static readonly Guid PartsOfSpeechSeedNamespace = new("023faebb-711b-4d2f-b34f-a15621fc66bb");
25+
private static readonly Guid CustomViewsSeedNamespace = new("b2c3d4e5-f6a7-8901-bcde-f12345678901");
26+
private static readonly Guid MorphTypesSeedNamespace = new("a7b2c3d4-e5f6-4a8b-9c0d-1e2f3a4b5c6d");
27+
28+
public static Guid ComplexFormTypesSeedCommitId(Guid projectId) =>
29+
Uuid.NewNameBased(ComplexFormTypesSeedNamespace, projectId.ToString());
30+
31+
public static Guid SemanticDomainsSeedCommitId(Guid projectId) =>
32+
Uuid.NewNameBased(SemanticDomainsSeedNamespace, projectId.ToString());
33+
34+
public static Guid PartsOfSpeechSeedCommitId(Guid projectId) =>
35+
Uuid.NewNameBased(PartsOfSpeechSeedNamespace, projectId.ToString());
36+
37+
public static Guid CustomViewsSeedCommitId(Guid projectId) =>
38+
Uuid.NewNameBased(CustomViewsSeedNamespace, projectId.ToString());
39+
40+
public static Guid MorphTypesSeedCommitId(Guid projectId) =>
41+
Uuid.NewNameBased(MorphTypesSeedNamespace, projectId.ToString());
42+
43+
internal static async Task AddPredefinedComplexFormTypes(DataModel dataModel, ProjectData projectData)
1944
{
20-
await dataModel.AddChanges(clientId,
45+
await dataModel.AddChanges(projectData.ClientId,
2146
[
2247
new CreateComplexFormType(CompoundComplexFormTypeId, new MultiString() { { "en", "Compound" } } ),
2348
new CreateComplexFormType(UnspecifiedComplexFormTypeId, new MultiString() { { "en", "Unspecified" } })
2449
],
25-
new Guid("dc60d2a9-0cc2-48ed-803c-a238a14b6eae"));
50+
ComplexFormTypesSeedCommitId(projectData.Id));
2651
}
2752

28-
internal static async Task AddPredefinedSemanticDomains(DataModel dataModel, Guid clientId)
53+
internal static async Task AddPredefinedSemanticDomains(DataModel dataModel, ProjectData projectData)
2954
{
3055
//todo load from xml instead of hardcoding and use real IDs
31-
await dataModel.AddChanges(clientId,
56+
await dataModel.AddChanges(projectData.ClientId,
3257
[
3358
new CreateSemanticDomainChange(new Guid("63403699-07c1-43f3-a47c-069d6e4316e5"), new MultiString() { { "en", "Universe, Creation" } }, "1", true),
3459
new CreateSemanticDomainChange(new Guid("999581c4-1611-4acb-ae1b-5e6c1dfe6f0c"), new MultiString() { { "en", "Sky" } }, "1.1", true),
@@ -38,25 +63,25 @@ await dataModel.AddChanges(clientId,
3863
new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", false),
3964
new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", false),
4065
],
41-
new Guid("023faebb-711b-4d2f-a14f-a15621fc66bc"));
66+
SemanticDomainsSeedCommitId(projectData.Id));
4267
}
4368

44-
public static async Task AddPredefinedPartsOfSpeech(DataModel dataModel, Guid clientId)
69+
public static async Task AddPredefinedPartsOfSpeech(DataModel dataModel, ProjectData projectData)
4570
{
4671
//todo load from xml instead of hardcoding
47-
await dataModel.AddChanges(clientId,
72+
await dataModel.AddChanges(projectData.ClientId,
4873
[
4974
new CreatePartOfSpeechChange(NounPartOfSpeechId, new MultiString() { { "en", "Noun" } }, true),
5075
new CreatePartOfSpeechChange(VerbPartOfSpeechId, new MultiString() { { "en", "Verb" } }, true),
5176
new CreatePartOfSpeechChange(AdjectivePartOfSpeechId, new MultiString() { { "en", "Adjective" } }, true),
5277
new CreatePartOfSpeechChange(AdverbPartOfSpeechId, new MultiString() { { "en", "Adverb" } }, true),
5378
],
54-
new Guid("023faebb-711b-4d2f-b34f-a15621fc66bb"));
79+
PartsOfSpeechSeedCommitId(projectData.Id));
5580
}
5681

57-
internal static async Task AddPredefinedCustomViews(DataModel dataModel, Guid clientId)
82+
internal static async Task AddPredefinedCustomViews(DataModel dataModel, ProjectData projectData)
5883
{
59-
await dataModel.AddChanges(clientId,
84+
await dataModel.AddChanges(projectData.ClientId,
6085
[
6186
new CreateCustomViewChange(
6287
new Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
@@ -82,15 +107,13 @@ await dataModel.AddChanges(clientId,
82107
Analysis = [new ViewWritingSystem { WsId = "en" }]
83108
})
84109
],
85-
new Guid("b2c3d4e5-f6a7-8901-bcde-f12345678901"));
110+
CustomViewsSeedCommitId(projectData.Id));
86111
}
87112

88-
public static readonly Guid MorphTypesSeedCommitId = new("a7b2c3d4-e5f6-4a8b-9c0d-1e2f3a4b5c6d");
89-
90-
internal static async Task AddPredefinedMorphTypes(DataModel dataModel, Guid clientId)
113+
internal static async Task AddPredefinedMorphTypes(DataModel dataModel, ProjectData projectData)
91114
{
92-
await dataModel.AddChanges(clientId,
115+
await dataModel.AddChanges(projectData.ClientId,
93116
[.. CanonicalMorphTypes.All.Values.Select(mt => new CreateMorphTypeChange(mt))],
94-
MorphTypesSeedCommitId);
117+
MorphTypesSeedCommitId(projectData.Id));
95118
}
96119
}

0 commit comments

Comments
 (0)