diff --git a/lib/reducers/db-ops-reducer.js b/lib/reducers/db-ops-reducer.js --- a/lib/reducers/db-ops-reducer.js +++ b/lib/reducers/db-ops-reducer.js @@ -22,6 +22,9 @@ return store; } +// There is a similar code for creating a search index after restore. +// Before making any changes here, please reference +// `copyContentFromDatabase` in `SQLiteQueryExecutor.cpp`. function getMessageSearchStoreOps( messageStoreOps: ?$ReadOnlyArray, ): $ReadOnlyArray { diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -1714,6 +1714,49 @@ sql << "INSERT OR IGNORE INTO " << tableName << " SELECT *" << " FROM sourceDB." << tableName << ";" << std::endl; } + + // There is a similar code for "live" index processing. Before making any + // changes here, please reference `getMessageSearchStoreOps` in + // `lib/reducers/db-ops-reducer.js`. + int textTypeInt = static_cast(MessageType::TEXT); + int editTypeInt = static_cast(MessageType::EDIT_MESSAGE); + int deleteTypeInt = static_cast(MessageType::DELETE_MESSAGE); + + // Populate message_search table for copied TEXT messages + sql << "INSERT OR IGNORE INTO message_search (" + << " original_message_id, message_id, processed_content) " + << "SELECT id, id, content " + << "FROM sourceDB.backup_messages " + << "WHERE type = " << textTypeInt << ";" << std::endl; + + // Update message_search table for EDIT_MESSAGE entries + sql << "UPDATE message_search " + << "SET " + << " message_id = b.id," + << " processed_content = IIF(" + << " JSON_VALID(b.content)," + << " JSON_EXTRACT(b.content, '$.text')," + << " NULL" + << " )" + << "FROM sourceDB.backup_messages AS b " + << "WHERE message_search.original_message_id = IIF(" + << " JSON_VALID(b.content)," + << " JSON_EXTRACT(b.content, '$.targetMessageID')," + << " NULL" + << ") " + << " AND b.type = " << editTypeInt << ";" << std::endl; + + // Delete message_search entries for DELETE_MESSAGE + sql << "DELETE FROM message_search " + << "WHERE original_message_id IN (" + << " SELECT IIF(" + << " JSON_VALID(content)," + << " JSON_EXTRACT(content, '$.targetMessageID')," + << " NULL" + << " ) " + << " FROM sourceDB.backup_messages " + << " WHERE type = " << deleteTypeInt << ");" << std::endl; + sql << "DETACH DATABASE sourceDB;"; executeQuery(this->getConnection(), sql.str()); } diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ { + const threadID = '40db5619-feb2-4e5f-bd0c-1f9a709d366e'; + const messageID = 'test-message-id'; + const messageContent = 'Hello world test message'; + + const message: WebMessage = { + id: messageID, + localID: null, + thread: threadID, + user: '111', + type: messageTypes.TEXT, + futureType: null, + content: messageContent, + time: BigInt(123), + }; + + // Add message to the backup database + backupQueryExecutor.replaceMessage( + message, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + + // Verify message is not searchable in main database before copy + let searchResults = mainQueryExecutor.searchMessages( + 'Hello', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(0); + + // Copy content from backup to main + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH, null); + + // Verify message is now searchable in main database + searchResults = mainQueryExecutor.searchMessages( + 'Hello', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(1); + expect(searchResults[0].message.id).toBe(messageID); + expect(searchResults[0].message.content).toBe(messageContent); + }); + + it('populates message_search table for EDIT_MESSAGE messages during copy', () => { + const threadID = '40db5619-feb2-4e5f-bd0c-1f9a709d366e'; + const originalMessageID = 'original-message-id'; + const editMessageID = 'edit-message-id'; + const editContent = JSON.stringify({ + targetMessageID: originalMessageID, + text: 'edited text', + }); + + const originalMessage: WebMessage = { + id: originalMessageID, + localID: null, + thread: threadID, + user: '111', + type: messageTypes.TEXT, + futureType: null, + content: 'Original content', + time: BigInt(100), + }; + + const editMessage: WebMessage = { + id: editMessageID, + localID: null, + thread: threadID, + user: '111', + type: messageTypes.EDIT_MESSAGE, + futureType: null, + content: editContent, + time: BigInt(200), + }; + + // Add messages to backup database + backupQueryExecutor.replaceMessage( + originalMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + backupQueryExecutor.replaceMessage( + editMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + + // Copy content from backup to main + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH, null); + + // Verify edit message is searchable + // Should find the edit and original message content + const searchResults = mainQueryExecutor.searchMessages( + 'edited', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(2); + expect(searchResults[0].message.id).toBe(originalMessageID); + expect(searchResults[1].message.id).toBe(editMessageID); + }); + + it('removes DELETE_MESSAGE entries from existing message_search during copy', () => { + const threadID = '40db5619-feb2-4e5f-bd0c-1f9a709d366e'; + const messageID = 'message-to-delete'; + + // First, add a TEXT message that gets indexed + const textMessage: WebMessage = { + id: messageID, + localID: null, + thread: threadID, + user: '111', + type: messageTypes.TEXT, + futureType: null, + content: 'This message will be deleted', + time: BigInt(100), + }; + + mainQueryExecutor.replaceMessage( + textMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + mainQueryExecutor.updateMessageSearchIndex( + messageID, + messageID, + 'This message will be deleted', + ); + + // Verify message is searchable + let searchResults = mainQueryExecutor.searchMessages( + 'deleted', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(1); + + const deleteMessage: WebMessage = { + id: 'delete-message-id', + localID: null, + thread: threadID, + user: '111', + type: messageTypes.DELETE_MESSAGE, + futureType: null, + content: JSON.stringify({ targetMessageID: messageID }), + time: BigInt(300), + }; + + backupQueryExecutor.replaceMessage( + deleteMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + + // Copy content from backup to main + // This should remove the search index entry + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH, null); + + // Verify message is no longer searchable + searchResults = mainQueryExecutor.searchMessages( + 'deleted', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(0); + }); + + it('handles EDIT_MESSAGE and DELETE_MESSAGE with invalid JSON gracefully', () => { + const threadID = '40db5619-feb2-4e5f-bd0c-1f9a709d366e'; + const originalMessageID = 'original-message-id'; + + // Add original TEXT message that gets indexed + const originalMessage: WebMessage = { + id: originalMessageID, + localID: null, + thread: threadID, + user: '111', + type: messageTypes.TEXT, + futureType: null, + content: 'Original message to edit', + time: BigInt(100), + }; + + backupQueryExecutor.replaceMessage( + originalMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + + // Add EDIT_MESSAGE with invalid JSON + const invalidEditMessage: WebMessage = { + id: 'invalid-edit-id', + localID: null, + thread: threadID, + user: '111', + type: messageTypes.EDIT_MESSAGE, + futureType: null, + content: 'invalid json { broken', + time: BigInt(200), + }; + + // Add DELETE_MESSAGE with invalid JSON + const invalidDeleteMessage: WebMessage = { + id: 'invalid-delete-id', + localID: null, + thread: threadID, + user: '111', + type: messageTypes.DELETE_MESSAGE, + futureType: null, + content: 'also invalid json } broken', + time: BigInt(300), + }; + + backupQueryExecutor.replaceMessage( + invalidEditMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + backupQueryExecutor.replaceMessage( + invalidDeleteMessage, + !!getProtocolByThreadID(threadID)?.dataIsBackedUp, + ); + + // Copy content - should not fail despite invalid + // JSON in EDIT/DELETE messages + expect(() => { + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH, null); + }).not.toThrow(); + + // Original message should still be searchable (invalid EDIT/DELETE ignored) + const searchResults = mainQueryExecutor.searchMessages( + 'Original', + threadID, + null, + null, + ); + expect(searchResults.length).toBe(1); + expect(searchResults[0].message.id).toBe(originalMessageID); + }); });