b2a0f3a77c
Two new public query methods plus an internal EscapeFtsTerm helper: - FullTextSearch(term, limit) runs MATCH against messages_fts and returns hex-encoded GUIDs sorted by FTS5 rank. Empty/whitespace short-circuits to an empty list so callers can fall back to the local page filter. - LoadByGuids(hexIds) resolves the hex GUIDs back to Message rows via WHERE Id IN (...). Chunked at 500 to stay below SQLite's 999-parameter cap, and the BLOB-PK autoindex means the join is O(log n) per id. - EscapeFtsTerm wraps user input in double-quotes so multi-word queries match as a phrase, not as per-word AND. Users opt into raw MATCH syntax by writing their own quotes. Plus _readLock serialises every Connection-touching internal method (UpsertMessage, MessageCount, all readers, retention writers, etc.). The DbViewer filter worker now runs FullTextSearch on a Task.Run thread while the PendingMessageThread keeps calling UpsertMessage; SqliteConnection is not safe for concurrent use, so this single lock is the minimal architecture change that closes the race. The Lazy-Enumerator methods (StreamForExport, GetDateRange, GetPagedDateRange) hold the lock only through command-setup + ExecuteReader; v1.4.8 doc-notes the caveat for the v1.5.x DI cycle to address with a snapshot-to-list or connection pool. RebuildFtsIndex stays outside the lock -- it owns its own SqliteConnection via OpenSecondaryConnection.