Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
fdd8a30
WIP (not working yet): sync morph type data
rmunn Mar 5, 2026
e3276d3
Implement CRUD for MorphTypeData, add DB migration
rmunn Mar 6, 2026
d32903f
Morph type prefixes and suffixes are nullable
rmunn Mar 6, 2026
04e685d
Update one test to new API shape
rmunn Mar 6, 2026
5f2d632
Register JsonPatchChange for morphTypeData
rmunn Mar 6, 2026
078e9b8
Update expected data for various snapshot tests
rmunn Mar 6, 2026
5fbfbe8
One more data change to a snapshot test
rmunn Mar 6, 2026
e2a4803
Rename CreateMorphTypeData change for consistency
rmunn Mar 6, 2026
fa075ef
Actually use the change in UseChangesTests
rmunn Mar 6, 2026
92fbb3a
Teach Resumable test how to create mock morph type
rmunn Mar 9, 2026
4f99bd7
Update expected value after change type rename
rmunn Mar 9, 2026
7563c10
Update regression test data for change type rename
rmunn Mar 9, 2026
447b91c
Fix another bit of test data
rmunn Mar 9, 2026
2689909
Fix live sena-3 sync test
rmunn Mar 9, 2026
0921309
Update verified Sena-3 snapshot with morph types
rmunn Mar 9, 2026
62d9e43
Finally fix LiveSena3Sync test
rmunn Mar 10, 2026
89f63f4
Add missing comma in verified snapshot file
rmunn Mar 10, 2026
25763e5
Prevent creating duplicate MorphTypeData objects
rmunn Mar 10, 2026
48d4696
Add unique index on MorphType column
rmunn Mar 10, 2026
4ffc8e3
Update expected VerifyDbModel snapshot
rmunn Mar 10, 2026
66c97f6
Fix AssertSena3Snapshots test on Linux
rmunn Mar 10, 2026
442dde3
Replace DB migration with hand-written SQL
rmunn Mar 11, 2026
d0c5348
Reject MorphTypeData updates that change MorphType
rmunn Mar 11, 2026
c7aeea5
Keep both UpdateMorphTypeData behaviors consistent
rmunn Mar 11, 2026
abc3a04
Create MorphTypeData keyed by MorphType, not Id
rmunn Mar 11, 2026
cfa2ecb
ResumableTests should also test MorphTypeData
rmunn Mar 11, 2026
df55b0a
Update ResumableTests to check MorphTypeData
rmunn Mar 11, 2026
cbac9a3
ResumableTests needs to retry just a little more
rmunn Mar 11, 2026
264f6d6
Do not allow ANY JsonPatch changes to MorphType
rmunn Mar 11, 2026
e6a5bb1
Address review comments from CodeRabbit
rmunn Mar 11, 2026
b4c082f
Make both historical sena-3 snapshots consistent
rmunn Mar 11, 2026
1e73447
Make FwDataMiniLcmApi updates consistent with Crdt
rmunn Mar 11, 2026
0443641
Improve ResumableTests by resuming morph types
rmunn Mar 12, 2026
9a1b9de
Restore old sena-3 snapshots, create new one
rmunn Mar 12, 2026
4979c4f
CreateMorphTypeDataChange takes object, not params
rmunn Mar 12, 2026
411fd94
No longer need raw SQL in the migration
rmunn Mar 12, 2026
ad2a9fd
Fix CreateMorphTypeDataChange missing Abbreviation
rmunn Mar 12, 2026
01da8aa
Improve MorphType enumeration in ResumableTests
rmunn Mar 13, 2026
f64c0b3
Prepare for great renaming (tests will fail)
rmunn Mar 14, 2026
710741a
Step 1: Rename MorphType enum to MorphTypeKind
rmunn Mar 14, 2026
5ddfa89
Step 2: rename MorphTypeData to MorphType
rmunn Mar 14, 2026
aaecf58
Hand-edit verified files before running tests
rmunn Mar 14, 2026
21ea220
Add DB migration back now that renames are done
rmunn Mar 14, 2026
b6122ab
New verified Sena-3 Sqlite DB after new migration
rmunn Mar 14, 2026
339af6a
Now rename MorphType enum to MorphTypeKind in UI
rmunn Mar 14, 2026
080c2a9
Rename two more MorphType uses to MorphTypeKind
rmunn Mar 14, 2026
0ff6cb4
A couple more MorphTypeKind renames
rmunn Mar 14, 2026
ecef97c
Merge 'origin/develop' into feat/sync-morph-types
rmunn Mar 14, 2026
9b1f5c7
Rename MorphType to MorphTypeKind in new demo entry
rmunn Mar 14, 2026
f511c6c
Mark MorphType properties as required
rmunn Mar 16, 2026
fd91b96
Tell C# CreateMorphTypeChange constructor is okay
rmunn Mar 16, 2026
ae339ea
LeadingToken -> Prefix, TrailingToken -> Postfix
rmunn Mar 16, 2026
b59d155
temp: enable fw-lite ci on morph-type feature branch
myieye Mar 17, 2026
4944300
Validate morph types in validation wrapper API
rmunn Mar 18, 2026
f072cda
Removed MorphTypeKind.Other
rmunn Mar 18, 2026
aeccdb1
Add tests for syncing morph types
rmunn Mar 18, 2026
8ef30c6
Test that MorphTypeKind updates are forbidden
rmunn Mar 18, 2026
c62728c
Trigger sync on morph type creation or updates
rmunn Mar 18, 2026
26054ac
Remove test that expects syncing to use validation wrappers.
myieye Mar 27, 2026
d0a0529
Respect entry morph-type when filtering and sorting (#2202)
myieye Mar 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fw-lite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ on:
branches:
- develop
- main
- shadcn-ui-main #just for now ensure PRs to this branch have checks run
- feat/sync-morph-types #just for now ensure PRs to this branch have checks run
env:
VIEWER_BUILD_OUTPUT_DIR: backend/FwLite/FwLiteShared/wwwroot/viewer
jobs:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.LcmUtils;
using FwDataMiniLcmBridge.Tests.Fixtures;
using MiniLcm.Models;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

Expand All @@ -9,4 +12,42 @@ protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("sorting-test", "en", "en"));
}

[Theory]
[InlineData("aaaa", SortField.Headword)] // FTS
[InlineData("a", SortField.Headword)] // non-FTS
[InlineData("aaaa", SortField.SearchRelevance)] // FTS
[InlineData("a", SortField.SearchRelevance)] // non-FTS
public async Task SecondaryOrder_DefaultsToStem(string query, SortField sortField)
{
var unknownMorphTypeEntryId = Guid.NewGuid();
Entry[] expected = [
new() { Id = unknownMorphTypeEntryId, LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Unknown }, // SecondaryOrder defaults to Stem = 1
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.BoundStem }, // SecondaryOrder = 2
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Suffix }, // SecondaryOrder = 6
];

var ids = expected.Select(e => e.Id).ToHashSet();

foreach (var entry in Faker.Faker.Random.Shuffle(expected))
await Api.CreateEntry(entry);

var fwDataApi = (BaseApi as FwDataMiniLcmApi)!;
await fwDataApi.Cache.DoUsingNewOrCurrentUOW("Clear morph type",
"Revert morph type",
() =>
{
// the fwdata api doesn't allow creating entries with MorphType.Other or Unknown, so we force it
var unknownMorphTypeEntry = fwDataApi.EntriesRepository.GetObject(unknownMorphTypeEntryId);
unknownMorphTypeEntry.LexemeFormOA.MorphTypeRA = null;
return ValueTask.CompletedTask;
});

var results = (await Api.SearchEntries(query, new(new(sortField))).ToArrayAsync())
.Where(e => ids.Contains(e.Id))
.ToList();

results.Should().BeEquivalentTo(expected,
options => options.WithStrictOrdering());
}
}
63 changes: 36 additions & 27 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ await Cache.DoUsingNewOrCurrentUOW("Delete Complex Form Type",
});
}

public IAsyncEnumerable<MorphTypeData> GetAllMorphTypeData()
public IAsyncEnumerable<MorphType> GetMorphTypes()
{
return
MorphTypeRepository
Expand All @@ -550,35 +550,44 @@ public IAsyncEnumerable<MorphTypeData> GetAllMorphTypeData()
.Select(FromLcmMorphType);
}

public Task<MorphTypeData?> GetMorphTypeData(Guid id)
public Task<MorphType?> GetMorphType(Guid id)
{
MorphTypeRepository.TryGetObject(id, out var lcmMorphType);
if (lcmMorphType is null) return Task.FromResult<MorphTypeData?>(null);
return Task.FromResult<MorphTypeData?>(FromLcmMorphType(lcmMorphType));
if (lcmMorphType is null) return Task.FromResult<MorphType?>(null);
return Task.FromResult<MorphType?>(FromLcmMorphType(lcmMorphType));
}

internal MorphTypeData FromLcmMorphType(IMoMorphType morphType)
public Task<MorphType?> GetMorphType(MorphTypeKind kind)
{
return new MorphTypeData
var guid = LcmHelpers.ToLcmMorphTypeId(kind);
if (guid is null) return Task.FromResult<MorphType?>(null);
MorphTypeRepository.TryGetObject(guid.Value, out var lcmMorphType);
if (lcmMorphType is null) return Task.FromResult<MorphType?>(null);
return Task.FromResult<MorphType?>(FromLcmMorphType(lcmMorphType));
}

internal MorphType FromLcmMorphType(IMoMorphType morphType)
{
return new MorphType
{
Id = morphType.Guid,
MorphType = LcmHelpers.FromLcmMorphType(morphType),
Kind = LcmHelpers.FromLcmMorphType(morphType),
Name = FromLcmMultiString(morphType.Name),
Abbreviation = FromLcmMultiString(morphType.Abbreviation),
Description = FromLcmMultiString(morphType.Description),
LeadingToken = morphType.Prefix,
TrailingToken = morphType.Postfix,
Prefix = morphType.Prefix,
Postfix = morphType.Postfix,
SecondaryOrder = morphType.SecondaryOrder,
};
}

public Task<MorphTypeData> CreateMorphTypeData(MorphTypeData morphTypeData)
public Task<MorphType> CreateMorphType(MorphType morphType)
{
// Creating new morph types not allowed in FwData projects, so silently ignore operation
return Task.FromResult(morphTypeData);
return Task.FromResult(morphType);
}

public Task<MorphTypeData> UpdateMorphTypeData(Guid id, UpdateObjectInput<MorphTypeData> update)
public Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
{
var lcmMorphType = MorphTypeRepository.GetObject(id);
if (lcmMorphType is null) throw new NullReferenceException($"unable to find morph type with id {id}");
Expand All @@ -587,19 +596,19 @@ public Task<MorphTypeData> UpdateMorphTypeData(Guid id, UpdateObjectInput<MorphT
Cache.ServiceLocator.ActionHandler,
() =>
{
var updateProxy = new UpdateMorphTypeDataProxy(lcmMorphType, this);
var updateProxy = new UpdateMorphTypeProxy(lcmMorphType, this);
update.Apply(updateProxy);
});
return Task.FromResult(FromLcmMorphType(lcmMorphType));
}

public async Task<MorphTypeData> UpdateMorphTypeData(MorphTypeData before, MorphTypeData after, IMiniLcmApi? api = null)
public async Task<MorphType> UpdateMorphType(MorphType before, MorphType after, IMiniLcmApi? api = null)
{
await MorphTypeDataSync.Sync(before, after, api ?? this);
return await GetMorphTypeData(after.Id) ?? throw new NullReferenceException("unable to find morph type with id " + after.Id);
await MorphTypeSync.Sync(before, after, api ?? this);
return await GetMorphType(after.Id) ?? throw new NullReferenceException("unable to find morph type with id " + after.Id);
}

public Task DeleteMorphTypeData(Guid id)
public Task DeleteMorphType(Guid id)
{
// Deleting morph types not allowed in FwData projects, so silently ignore operation
return Task.CompletedTask;
Expand Down Expand Up @@ -643,7 +652,7 @@ private Entry FromLexEntry(ILexEntry entry)
{
try
{
return new Entry
var result = new Entry
{
Id = entry.Guid,
Note = FromLcmMultiString(entry.Comment),
Expand All @@ -661,6 +670,7 @@ private Entry FromLexEntry(ILexEntry entry)
// ILexEntry.PublishIn is a virtual property that inverts DoNotPublishInRC against all publications
PublishIn = entry.PublishIn.Select(FromLcmPossibility).ToList(),
};
return result;
}
catch (Exception e)
{
Expand Down Expand Up @@ -716,24 +726,22 @@ private ComplexFormComponent ToEntryReference(ILexEntry component, ILexEntry com
return new ComplexFormComponent
{
ComponentEntryId = component.Guid,
ComponentHeadword = component.LexEntryHeadwordOrUnknown(),
ComponentHeadword = component.LexEntryHeadwordOrUnknown(applyMorphTokens: false), // match CRDT for now
ComplexFormEntryId = complexEntry.Guid,
ComplexFormHeadword = complexEntry.LexEntryHeadwordOrUnknown(),
ComplexFormHeadword = complexEntry.LexEntryHeadwordOrUnknown(applyMorphTokens: false), // match CRDT for now
Order = Order(component, complexEntry)
};
}



private ComplexFormComponent ToSenseReference(ILexSense componentSense, ILexEntry complexEntry)
{
return new ComplexFormComponent
{
ComponentEntryId = componentSense.Entry.Guid,
ComponentSenseId = componentSense.Guid,
ComponentHeadword = componentSense.Entry.LexEntryHeadwordOrUnknown(),
ComponentHeadword = componentSense.Entry.LexEntryHeadwordOrUnknown(applyMorphTokens: false), // match CRDT for now
ComplexFormEntryId = complexEntry.Guid,
ComplexFormHeadword = complexEntry.LexEntryHeadwordOrUnknown(),
ComplexFormHeadword = complexEntry.LexEntryHeadwordOrUnknown(applyMorphTokens: false), // match CRDT for now
Order = Order(componentSense, complexEntry)
};
}
Expand Down Expand Up @@ -930,12 +938,13 @@ private IEnumerable<ILexEntry> GetFilteredAndSortedEntries(Func<ILexEntry, bool>
private IEnumerable<ILexEntry> ApplySorting(SortOptions order, IEnumerable<ILexEntry> entries, string? query)
{
var sortWs = GetWritingSystemHandle(order.WritingSystem, WritingSystemType.Vernacular);
var stemSecondaryOrder = MorphTypeRepository.GetObject(MoMorphTypeTags.kguidMorphStem).SecondaryOrder;
if (order.Field == SortField.SearchRelevance)
{
return entries.ApplyRoughBestMatchOrder(order, sortWs, query);
return entries.ApplyRoughBestMatchOrder(order, sortWs, stemSecondaryOrder, query);
}

return order.ApplyOrder(entries, e => e.LexEntryHeadword(sortWs));
return entries.ApplyHeadwordOrder(order, sortWs, stemSecondaryOrder);
}

public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
Expand All @@ -947,7 +956,7 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
private Func<ILexEntry, bool>? EntrySearchPredicate(string? query = null)
{
if (string.IsNullOrEmpty(query)) return null;
return entry => entry.CitationForm.SearchValue(query) ||
return entry => entry.SearchHeadWord(query) || // CitationForm.SearchValue would be redundant
entry.LexemeFormOA?.Form.SearchValue(query) is true ||
entry.AllSenses.Any(s => s.Gloss.SearchValue(query));
}
Expand Down
Loading
Loading