Skip to content

Update entry search records if morph type tokens change#2378

Open
rmunn wants to merge 10 commits into
developfrom
feat/update-fts-table-when-morph-type-tokens-change
Open

Update entry search records if morph type tokens change#2378
rmunn wants to merge 10 commits into
developfrom
feat/update-fts-table-when-morph-type-tokens-change

Conversation

@rmunn

@rmunn rmunn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #2213.

I chose to hook into the existing interceptor rather than creating a new one. That way if entries and morph types change in the same "transaction" (or the EF equivalent of a transaction), we can avoid duplicating the search record updates for entries that changed (and we can use the changed entries, rather than their original values, when calculating the updated headwords with the new morph types — if we used a separate interceptor then we wouldn't be able to guarantee that these two steps ran in the desired order).

P.S. This is #2246 recreated on top of current develop.

@rmunn rmunn self-assigned this Jun 25, 2026
@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0fd2fad6-9be3-46ae-9f2c-b78c988e86db

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Entry search regeneration now uses a static rebuild helper, and save-changes interception now routes MorphType token changes to a full table regeneration while keeping incremental updates for other entry changes.

Changes

Entry search maintenance

Layer / File(s) Summary
Regeneration overload
backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs
The instance overload delegates to a new static regeneration method that truncates the entry search table in a transaction and reloads writing systems and morph types.
Interceptor routing
backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs
Save-changes interception now accepts a nullable DbContext, filters added writing systems directly, and switches to full regeneration when morph-type prefix or postfix tokens change.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

A rabbit hops by, ears tall and keen,
To rebuild the search where the headwords gleam.
If morph-tokens twitch, I give a grand restart,
If entries just nudge, I update with art.
Thump-thump, the table shivers, then sings anew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main change: recalculating entry search records when morph type tokens change.
Description check ✅ Passed The description is directly about updating entry search records for morph type token changes and reusing the existing interceptor.
Linked Issues check ✅ Passed The interceptor now handles morph-type token changes by regenerating search rows, and entry updates remain covered for morph-type changes.
Out of Scope Changes check ✅ Passed The changes stay focused on entry search synchronization and interceptor flow, with no obvious unrelated additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/update-fts-table-when-morph-type-tokens-change

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@argos-ci

argos-ci Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 26, 2026, 4:58 AM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs (1)

284-301: 🗄️ Data Integrity & Integration | 🔴 Critical

Move regeneration to after commit and remove explicit transaction.

Invoking RegenerateEntrySearchTable within SavingChangesAsync guarantees stale data because the database reads for WritingSystemsOrdered and MorphTypes (marked .AsNoTracking() in LcmCrdtDbContext.cs) execute before the pending changes are committed. Consequently, the regeneration uses outdated token mappings. Additionally, calling BeginTransactionAsync inside the SaveChanges pipeline conflicts with EF Core's internal transaction management, risking runtime failures.

The logic must be moved to SavedChangesAsync or handled in a background worker that runs only after the transaction commits, ensuring all tables reflect the new state before reading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs` around lines 284
- 301, `RegenerateEntrySearchTable` is running too early and should not open its
own transaction inside the SaveChanges flow. Move the call out of
`SavingChangesAsync` into `SavedChangesAsync` (or an equivalent post-commit
background path) so `WritingSystemsOrdered` and `MorphTypes` are read after the
commit and reflect the latest state. Also remove the explicit
`BeginTransactionAsync`/`CommitAsync` handling from
`RegenerateEntrySearchTable`, since EF Core already manages the save transaction
and nested transaction usage can fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs`:
- Around line 60-68: SavingChangesAsync in UpdateEntrySearchTableInterceptor is
triggering RegenerateEntrySearchTable(dbContext) before EF Core has committed
MorphType, entry, and writing-system changes, so the rebuild reads stale data
and can drop same-save updates. Move the full regeneration out of
SavingChangesAsync and defer it until after the commit (for example via
SavedChangesAsync or equivalent post-commit handling), preserving the
changedMorphTypes decision so EntrySearchService.RegenerateEntrySearchTable runs
only on committed database state and includes the latest newWritingSystems,
toUpdate, and toRemove values.

---

Outside diff comments:
In `@backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs`:
- Around line 284-301: `RegenerateEntrySearchTable` is running too early and
should not open its own transaction inside the SaveChanges flow. Move the call
out of `SavingChangesAsync` into `SavedChangesAsync` (or an equivalent
post-commit background path) so `WritingSystemsOrdered` and `MorphTypes` are
read after the commit and reflect the latest state. Also remove the explicit
`BeginTransactionAsync`/`CommitAsync` handling from
`RegenerateEntrySearchTable`, since EF Core already manages the save transaction
and nested transaction usage can fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9376e548-8f2b-4223-81fa-2723d03fbaa8

📥 Commits

Reviewing files that changed from the base of the PR and between 5ae0554 and b5dc52e.

📒 Files selected for processing (2)
  • backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs
  • backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs

Comment thread backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs Outdated
rmunn added 2 commits June 25, 2026 15:33
Also fixed two bugs:

1. SQLite doesn't allow nested transactions: RegenerateEntrySearchTable
needed to check if we were already in a transaction before starting one.

2. By moving the entry table regeneration into a SavedChangesAsync event
handler, we can ensure that the MorphType changes have already been
applied to the database and will be read correctly.
@rmunn rmunn requested review from hahn-kev and myieye June 25, 2026 08:47

@myieye myieye left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally happy with this. 👍

But, I'm expecting my test suggestions to uncover one missing case 😉

Comment thread backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs Outdated
Comment thread backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs Outdated
Comment thread backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs Outdated
@rmunn

rmunn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit is suggesting adding void SaveChangesFailed(DbContextErrorEventData eventData) and Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = default) handlers that will reset the EntryTableNeedsRegeneration flag to false. But if saving changes failed, it's quite likely that the regeneration will still need to happen, so having it happen the next time changes are successfully saved seems like a good idea.

@myieye

myieye commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

CodeRabbit is suggesting adding void SaveChangesFailed(DbContextErrorEventData eventData) and Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = default) handlers that will reset the EntryTableNeedsRegeneration flag to false. But if saving changes failed, it's quite likely that the regeneration will still need to happen, so having it happen the next time changes are successfully saved seems like a good idea.

@rmunn Yes, it's quite likely, but I think that should still be re-determined by the changeset. If it's necessary the next time, then your change checking code will turn the flag back on. I think resetting the flag in failure situations makes sense.

@rmunn

rmunn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@rmunn Yes, it's quite likely, but I think that should still be re-determined by the changeset. If it's necessary the next time, then your change checking code will turn the flag back on. I think resetting the flag in failure situations makes sense.

Done in commit f6f7b8b.

Changing the MorphType of an entry successfully regenerates its entry
search record.
@rmunn

rmunn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

New test case added in 6eedb36 to cover the first scenario @myieye metnioned ("An entry's morph-type is changed so that its morph-tokens are different"). Passes; it's covered by the interceptor because the interceptor is updating the headword for any change to an entry, not merely looking to see whether its lexeme or citation forms changed. So this is already working. Still, it's good to have it in there so that we don't accidentally break that functionality during an attempted optimization.

Now going to work on the second test, updating many things at once.

rmunn added 2 commits June 25, 2026 16:31
Regenerated search table entry successfully gets new morph type prefix.
@rmunn

rmunn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@myieye - Is commit 3c3a4fa what you had in mind for your second test?

@rmunn rmunn requested a review from myieye June 25, 2026 09:40
@myieye

myieye commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@myieye - Is commit 3c3a4fa what you had in mind for your second test?

I guess what I had in mind probably requires using the raw DbContext. That's maybe the only way to batch all 3 changes into a single DbContext.SaveChanges() batch.

I suppose that's not a particularly realistic scenario, but it would still be nice to be confident that we don't have any loop holes in our change detection.

@myieye myieye left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment to fix.

Also, I'd personally still like one more test with multiple changes applied directly to db-context entities, so they actually get persisted at the same time.

Actually, batches of changes in a single dbcontext.SaveChanges() call are not entirely unrealistic, because batching stuff is one pretty interesting optimization idea for the whole CRDT side of a fw-headless sync.

Comment thread backend/FwLite/LcmCrdt.Tests/FullTextSearch/EntrySearchServiceTests.cs Outdated
Just to make sure everything still works when multiple changes are added
to the DbContext all at once, let's add two more tests making those
changes directly in the DbContext instead of via the API. That will be a
pretty good simulation of syncing a bunch of changes, without needing to
set up an entire sync environment.
@rmunn

rmunn commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

One comment to fix.

Also, I'd personally still like one more test with multiple changes applied directly to db-context entities, so they actually get persisted at the same time.

Actually, batches of changes in a single dbcontext.SaveChanges() call are not entirely unrealistic, because batching stuff is one pretty interesting optimization idea for the whole CRDT side of a fw-headless sync.

Added two more tests in commit 88ffb3b doing that, one changing the entry first and then the morph type, and the other changing the morph type first and then the entry. Those should both have the same effect, and in fact I'd be shocked if one failed while the other one passed. But you never know what weirdness we might manage to cause by getting something wrong in a save-changes hook, so I figured best to be sure about it.

The new tests pass when run locally. Once they also pass in CI, I'll merge the branch.

@hahn-kev hahn-kev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I'd like to see the order of operations change in the interceptor.

Comment thread backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs Outdated
Comment thread backend/FwLite/LcmCrdt/FullTextSearch/UpdateEntrySearchTableInterceptor.cs Outdated
rmunn added 2 commits June 26, 2026 11:46
Both add or remove a postfix token. In theory we should also test a
prefix token, but there's no real reason to do that because it's pretty
much impossible that we would change prefix token handling yet forget to
do the same thing to postfix tokens.
If morph-type prefix or postfix tokens are ever altered, then we would
be skipping the entry changes, so we might as well skip looking through
the list for writing system and entry changes anyway.
@rmunn rmunn requested a review from hahn-kev June 26, 2026 04:58
if (maybeDbContext is not LcmCrdtDbContext dbContext) return;
// Morph types with changes to prefix or postfix tokens will require updated entry search records
// (Note that morph types can't be added or deleted, so we only need to catch changes, which will be rare)
var changedMorphTypes = dbContext.ChangeTracker.Entries<MorphType>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we actually need a list of morph types? or can we just .Where(...).Any() to avoid allocating a list just to see if it's empty or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what, that's correct. I had allocated a list because I was going through and finding entries that referred to those morph types in order to only change those entries. But when it (surprisingly) turned out to be faster to simply delete and recreate the search table, I didn't need the list anymore.

I'll change that to a simple .Any() query and then merge this.

@hahn-kev hahn-kev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one small suggestion, otherwise I'm happy with it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update headwords when Entry.MorphType or MorphType tokens change

3 participants