Skip to content

Add canonical morph-types and refresh headwords #2212

Description

@myieye

Add morph-types to existing CRDT projects and refresh FTS headwords to respect those morph-types.

Implementation Steps

Step 1: Create CanonicalMorphTypes static class

File: backend/FwLite/MiniLcm/Models/CanonicalMorphTypes.cs (NEW)

Define all 19 canonical morph types (one per MorphTypeKind enum value, excluding Unknown) as a static array of MorphType objects. Include: GUIDs (matching MoMorphTypeTags.kguidMorph* from SIL.LCModel), Kind, Name/Abbreviation (en), Prefix, Postfix, SecondaryOrder.

Data source: Hardcode from LibLCM GitHub source. Add a Sena3 verification test (see Step 7).

Pattern: Follow CanonicalGuidsPartOfSpeech (MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs) for the frozen-set pattern, but include full MorphType objects since we need prefix, postfix, name, etc. for seeding.

Step 2: Add PredefinedMorphTypes to PreDefinedData

File: backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs (MODIFY)

Add PredefinedMorphTypes(DataModel dataModel, Guid clientId) method following the exact pattern of PredefinedComplexFormTypes (line 8-16). Use CreateMorphTypeChange for each canonical morph type. Use a fixed commit GUID for deterministic CRDT commits.

Note: CreateMorphTypeChange.NewEntity() (line 40-53 of LcmCrdt/Changes/CreateMorphTypeChange.cs) already handles duplicates gracefully by creating pre-deleted entities, so this is safe for CRDT-to-CRDT sync scenarios.

Step 3: Call PredefinedMorphTypes for new projects

File: backend/FwLite/LcmCrdt/CrdtProjectsService.cs (MODIFY)

Add call to PreDefinedData.PredefinedMorphTypes in SeedSystemData() (line 242-247):

internal static async Task SeedSystemData(DataModel dataModel, Guid clientId)
{
    await PreDefinedData.PredefinedComplexFormTypes(dataModel, clientId);
    await PreDefinedData.PredefinedPartsOfSpeech(dataModel, clientId);
    await PreDefinedData.PredefinedSemanticDomains(dataModel, clientId);
    await PreDefinedData.PredefinedMorphTypes(dataModel, clientId); // NEW
}

Step 4: Seed morph-types for existing projects in MigrateDb (BEFORE FTS refresh)

File: backend/FwLite/LcmCrdt/CurrentProjectService.cs (MODIFY)

Modify MigrateDb() (line 95-121) to seed morph-types between MigrateAsync() and RegenerateIfMissing():

async Task Execute()
{
    try
    {
        await using var dbContext = await DbContextFactory.CreateDbContextAsync();
        await dbContext.Database.MigrateAsync();

        // Seed morph-types if missing (for existing projects created before morph-type support)
        // Must happen BEFORE FTS regeneration so headwords include morph-type tokens
        if (!await dbContext.MorphTypes.AnyAsync())
        {
            var dataModel = serviceProvider.GetRequiredService<DataModel>();
            var clientId = serviceProvider.GetRequiredService<CurrentProjectService>().ProjectData.ClientId;
            await PreDefinedData.PredefinedMorphTypes(dataModel, clientId);
        }

        if (EntrySearchServiceFactory is not null)
        {
            await using var ess = EntrySearchServiceFactory.CreateSearchService(dbContext);
            await ess.RegenerateIfMissing();
        }
    }
    catch (Exception e) { ... }
}

Ordering guarantees:

  1. MigrateAsync() → runs EF migrations (including FTS clear migration from Step 5)
  2. Morph-type seeding → canonical morph types now in CRDT
  3. RegenerateIfMissing() → FTS rebuilt with morph-type-aware headwords via EntryQueryHelpers.ComputeHeadwords()

Note: Need to inject IServiceProvider or DataModel into CurrentProjectService (check existing DI setup). The MigrationTasks.GetOrAdd guard ensures this runs at most once per project per process lifetime.

Step 5: EF Migration to clear FTS table

File: backend/FwLite/LcmCrdt/Migrations/{timestamp}_RegenerateSearchTableForMorphTypes.cs (NEW)

Generate via dotnet ef migrations add RegenerateSearchTableForMorphTypes --project LcmCrdt:

protected override void Up(MigrationBuilder migrationBuilder)
{
    // Force FTS rebuild so headwords include morph-type prefix/postfix tokens
    migrationBuilder.Sql("DELETE FROM EntrySearchRecord;");
}

Step 6: Patch legacy snapshots in sync layer (CRITICAL - addresses snapshot problem)

File: backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs (MODIFY)

Problem: Projects that synced before morph-type support have snapshots with MorphTypes: []. After seeding, the CRDT has 19 morph-types but the snapshot still has []. When MorphTypeSync.Sync(snapshot.MorphTypes, currentFwDataMorphTypes, crdtApi) runs, it diffs [] vs [19 types], sees 19 additions, and calls crdtApi.CreateMorphType() which throws DuplicateObjectException.

Fix: In Sync() method (around line 81-83), before calling SyncInternal, patch the snapshot if it has empty MorphTypes but the CRDT has morph-types:

// Patch legacy snapshots that were created before morph-type support
if (projectSnapshot is not null && projectSnapshot.MorphTypes.Length == 0)
{
    var currentCrdtMorphTypes = await crdtApi.GetMorphTypes().ToArrayAsync();
    if (currentCrdtMorphTypes.Length > 0)
    {
        projectSnapshot = projectSnapshot with { MorphTypes = currentCrdtMorphTypes };
    }
}

Why this works:

  • ProjectSnapshot is a positional record → supports with expressions
  • Seeded morph-types use canonical GUIDs → they match FwData's morph-type GUIDs → diff sees no changes
  • After sync completes, the snapshot is regenerated from CRDT (by the caller), so future syncs won't need this fixup
  • For CRDT-only projects (no FwData): no snapshot exists, so this code path is never hit
  • For projects that first sync with FwData: goes through Import path (snapshot is null), ResumableImportApi handles duplicates by Kind

Step 7: Tests

7a. Verification test: Add Sena3-based test asserting hardcoded canonical morph-type values match FwData project values via FwDataMiniLcmApi.GetMorphTypes().

7b. Seeding tests (LcmCrdt.Tests/):

  • New project with SeedNewProjectData: true has all 19 canonical morph types
  • Existing project with empty MorphType table gets canonical morph types after open
  • Seeding is idempotent (opening project twice doesn't duplicate)

7c. Sync snapshot test (FwLiteProjectSync.Tests/SyncTests.cs):

  • Sync with legacy snapshot (empty MorphTypes) after morph-type seeding → no errors, no duplicate morph types

7d. Migration regression: v3 SQL dump and updated regression tests per existing pattern.

Step 8: v3 SQL regression dump

File: backend/FwLite/LcmCrdt.Tests/Data/Scripts/v3.sql (NEW)
File: backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs (MODIFY)
File: backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs (MODIFY)

Follow instructions in generating-versions.MD. Add v3 to RegressionVersion enum and wire up test data.


Files Summary

File Action Purpose
MiniLcm/Models/CanonicalMorphTypes.cs CREATE Canonical morph-type definitions
LcmCrdt/Objects/PreDefinedData.cs MODIFY Add PredefinedMorphTypes
LcmCrdt/CrdtProjectsService.cs MODIFY Call PredefinedMorphTypes in SeedSystemData
LcmCrdt/CurrentProjectService.cs MODIFY Seed morph-types in MigrateDb before FTS refresh
LcmCrdt/Migrations/*_RegenerateSearchTableForMorphTypes.cs CREATE Clear FTS for rebuild
FwLiteProjectSync/CrdtFwdataProjectSyncService.cs MODIFY Patch legacy snapshots before sync
LcmCrdt.Tests/Data/Scripts/v3.sql CREATE Regression dump
LcmCrdt.Tests/Data/RegressionTestHelper.cs MODIFY Add v3
LcmCrdt.Tests/Data/MigrationTests.cs MODIFY Add v3 tests
Various test files MODIFY/CREATE Tests per Step 7

Key Existing Code to Reuse

  • PreDefinedData.PredefinedComplexFormTypes pattern (LcmCrdt/Objects/PreDefinedData.cs)
  • CreateMorphTypeChange (LcmCrdt/Changes/CreateMorphTypeChange.cs) - handles duplicate Kind by pre-deleting
  • EntrySearchService.RegenerateIfMissing (LcmCrdt/FullTextSearch/EntrySearchService.cs) - already in MigrateDb flow
  • EntryQueryHelpers.ComputeHeadwords (LcmCrdt/Data/EntryQueryHelpers.cs) - already morph-type-aware
  • CanonicalGuidsPartOfSpeech pattern (MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs)

Verification

  1. dotnet test backend/FwLite/LcmCrdt.Tests - seeding, migration regression
  2. dotnet test backend/FwLite/FwLiteProjectSync.Tests - sync with legacy snapshots
  3. Verify ordering: morph-types exist before FTS regeneration in MigrateDb logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions