From 3d72253a033eba67307fa3da7fc40adb1462cb80 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 19 May 2026 20:53:49 +0200 Subject: [PATCH] Use named GUIDs for predefined-data seed commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 2 +- backend/FwLite/LcmCrdt/CrdtProjectsService.cs | 14 ++--- .../FwLite/LcmCrdt/CurrentProjectService.cs | 6 +-- backend/FwLite/LcmCrdt/LcmCrdt.csproj | 1 + .../FwLite/LcmCrdt/Objects/PreDefinedData.cs | 51 ++++++++++++------- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index d7f2e0f984..3218031489 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -77,7 +77,7 @@ await CrdtProjectsService.InitProjectDb(_crdtDbContext, projectData); await currentProjectService.RefreshProjectData(); // CreateProject would also seed morph types — so we need to do it manually here - await PreDefinedData.AddPredefinedMorphTypes(_services.ServiceProvider.GetRequiredService(), projectData.ClientId); + await PreDefinedData.AddPredefinedMorphTypes(_services.ServiceProvider.GetRequiredService(), projectData); if (_seedWs) { await Api.CreateWritingSystem(new WritingSystem() diff --git a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs index 08c03b2455..f6ab9cf490 100644 --- a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -169,9 +169,9 @@ public virtual async Task CreateProject(CreateProjectRequest reques // Morph types are predefined system data that must always exist — seed them // unconditionally so they're available before AfterCreate (e.g. import) runs. var dataModel = serviceScope.ServiceProvider.GetRequiredService(); - await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData.ClientId); + await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData); if (request.SeedNewProjectData) - await SeedSystemData(dataModel, projectData.ClientId); + await SeedSystemData(dataModel, projectData); await (request.AfterCreate?.Invoke(serviceScope.ServiceProvider, crdtProject) ?? Task.CompletedTask); } catch (Exception e) @@ -243,13 +243,13 @@ internal static async Task InitProjectDb(LcmCrdtDbContext db, ProjectData data) await db.SaveChangesAsync(); } - internal static async Task SeedSystemData(DataModel dataModel, Guid clientId) + internal static async Task SeedSystemData(DataModel dataModel, ProjectData projectData) { // Note: AddPredefinedMorphTypes is seeded unconditionally in CreateProject, not here. - await PreDefinedData.AddPredefinedComplexFormTypes(dataModel, clientId); - await PreDefinedData.AddPredefinedPartsOfSpeech(dataModel, clientId); - await PreDefinedData.AddPredefinedSemanticDomains(dataModel, clientId); - await PreDefinedData.AddPredefinedCustomViews(dataModel, clientId); + await PreDefinedData.AddPredefinedComplexFormTypes(dataModel, projectData); + await PreDefinedData.AddPredefinedPartsOfSpeech(dataModel, projectData); + await PreDefinedData.AddPredefinedSemanticDomains(dataModel, projectData); + await PreDefinedData.AddPredefinedCustomViews(dataModel, projectData); } [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")] diff --git a/backend/FwLite/LcmCrdt/CurrentProjectService.cs b/backend/FwLite/LcmCrdt/CurrentProjectService.cs index 90a57f8d70..36a886c5a5 100644 --- a/backend/FwLite/LcmCrdt/CurrentProjectService.cs +++ b/backend/FwLite/LcmCrdt/CurrentProjectService.cs @@ -111,11 +111,11 @@ async Task Execute() // Seed morph-types if missing (for existing projects created before morph-type support). // Must happen BEFORE FTS regeneration so headwords include morph-type tokens. // (querying Commits instead of MorphTypes, because the commit may not be projected yet) - if (!await dbContext.Set().AsNoTracking().AnyAsync(c => c.Id == PreDefinedData.MorphTypesSeedCommitId)) + var projectData = await dbContext.ProjectData.AsNoTracking().FirstAsync(); + if (!await dbContext.Set().AsNoTracking().AnyAsync(c => c.Id == PreDefinedData.MorphTypesSeedCommitId(projectData.Id))) { var dataModel = services.GetRequiredService(); - var projectData = await dbContext.ProjectData.AsNoTracking().FirstAsync(); - await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData.ClientId); + await PreDefinedData.AddPredefinedMorphTypes(dataModel, projectData); } if (EntrySearchServiceFactory is not null) diff --git a/backend/FwLite/LcmCrdt/LcmCrdt.csproj b/backend/FwLite/LcmCrdt/LcmCrdt.csproj index 8703f0f4cb..4f9c5b714f 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdt.csproj +++ b/backend/FwLite/LcmCrdt/LcmCrdt.csproj @@ -19,6 +19,7 @@ + diff --git a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs index 88f5849e6c..f319de8921 100644 --- a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs +++ b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs @@ -1,5 +1,6 @@ using LcmCrdt.Changes; using SIL.Harmony; +using UUIDNext; namespace LcmCrdt.Objects; @@ -15,20 +16,38 @@ public static class PreDefinedData public static readonly Guid AdjectivePartOfSpeechId = new("30d07580-5052-4d91-bc24-469b8b2d7df9"); public static readonly Guid AdverbPartOfSpeechId = new("46e4fe08-ffa0-4c8b-bf98-2c56f38904d9"); - internal static async Task AddPredefinedComplexFormTypes(DataModel dataModel, Guid clientId) + // Seed commit-ids are derived per-project (UUIDv5 namespaced on projectId) so each project + // owns its own row in LexBox's CrdtCommits table — a shared constant id would collide on the + // primary key and the seed would get attributed to whichever project pushed first. + public static Guid ComplexFormTypesSeedCommitId(Guid projectId) => + Uuid.NewNameBased(projectId, "complex-form-types-seed"); + + public static Guid SemanticDomainsSeedCommitId(Guid projectId) => + Uuid.NewNameBased(projectId, "semantic-domains-seed"); + + public static Guid PartsOfSpeechSeedCommitId(Guid projectId) => + Uuid.NewNameBased(projectId, "parts-of-speech-seed"); + + public static Guid CustomViewsSeedCommitId(Guid projectId) => + Uuid.NewNameBased(projectId, "custom-views-seed"); + + public static Guid MorphTypesSeedCommitId(Guid projectId) => + Uuid.NewNameBased(projectId, "morph-types-seed"); + + internal static async Task AddPredefinedComplexFormTypes(DataModel dataModel, ProjectData projectData) { - await dataModel.AddChanges(clientId, + await dataModel.AddChanges(projectData.ClientId, [ new CreateComplexFormType(CompoundComplexFormTypeId, new MultiString() { { "en", "Compound" } } ), new CreateComplexFormType(UnspecifiedComplexFormTypeId, new MultiString() { { "en", "Unspecified" } }) ], - new Guid("dc60d2a9-0cc2-48ed-803c-a238a14b6eae")); + ComplexFormTypesSeedCommitId(projectData.Id)); } - internal static async Task AddPredefinedSemanticDomains(DataModel dataModel, Guid clientId) + internal static async Task AddPredefinedSemanticDomains(DataModel dataModel, ProjectData projectData) { //todo load from xml instead of hardcoding and use real IDs - await dataModel.AddChanges(clientId, + await dataModel.AddChanges(projectData.ClientId, [ new CreateSemanticDomainChange(new Guid("63403699-07c1-43f3-a47c-069d6e4316e5"), new MultiString() { { "en", "Universe, Creation" } }, "1", true), new CreateSemanticDomainChange(new Guid("999581c4-1611-4acb-ae1b-5e6c1dfe6f0c"), new MultiString() { { "en", "Sky" } }, "1.1", true), @@ -38,25 +57,25 @@ await dataModel.AddChanges(clientId, new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", false), new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", false), ], - new Guid("023faebb-711b-4d2f-a14f-a15621fc66bc")); + SemanticDomainsSeedCommitId(projectData.Id)); } - public static async Task AddPredefinedPartsOfSpeech(DataModel dataModel, Guid clientId) + public static async Task AddPredefinedPartsOfSpeech(DataModel dataModel, ProjectData projectData) { //todo load from xml instead of hardcoding - await dataModel.AddChanges(clientId, + await dataModel.AddChanges(projectData.ClientId, [ new CreatePartOfSpeechChange(NounPartOfSpeechId, new MultiString() { { "en", "Noun" } }, true), new CreatePartOfSpeechChange(VerbPartOfSpeechId, new MultiString() { { "en", "Verb" } }, true), new CreatePartOfSpeechChange(AdjectivePartOfSpeechId, new MultiString() { { "en", "Adjective" } }, true), new CreatePartOfSpeechChange(AdverbPartOfSpeechId, new MultiString() { { "en", "Adverb" } }, true), ], - new Guid("023faebb-711b-4d2f-b34f-a15621fc66bb")); + PartsOfSpeechSeedCommitId(projectData.Id)); } - internal static async Task AddPredefinedCustomViews(DataModel dataModel, Guid clientId) + internal static async Task AddPredefinedCustomViews(DataModel dataModel, ProjectData projectData) { - await dataModel.AddChanges(clientId, + await dataModel.AddChanges(projectData.ClientId, [ new CreateCustomViewChange( new Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890"), @@ -82,15 +101,13 @@ await dataModel.AddChanges(clientId, Analysis = [new ViewWritingSystem { WsId = "en" }] }) ], - new Guid("b2c3d4e5-f6a7-8901-bcde-f12345678901")); + CustomViewsSeedCommitId(projectData.Id)); } - public static readonly Guid MorphTypesSeedCommitId = new("a7b2c3d4-e5f6-4a8b-9c0d-1e2f3a4b5c6d"); - - internal static async Task AddPredefinedMorphTypes(DataModel dataModel, Guid clientId) + internal static async Task AddPredefinedMorphTypes(DataModel dataModel, ProjectData projectData) { - await dataModel.AddChanges(clientId, + await dataModel.AddChanges(projectData.ClientId, [.. CanonicalMorphTypes.All.Values.Select(mt => new CreateMorphTypeChange(mt))], - MorphTypesSeedCommitId); + MorphTypesSeedCommitId(projectData.Id)); } }