diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -151,6 +151,8 @@ std::string maxVersion) const = 0; virtual void restoreFromBackupLog(const std::vector &backupLog) const = 0; + virtual void + copyContentFromDatabase(const std::string databasePath) const = 0; virtual void addOutboundP2PMessages( const std::vector &messages) const = 0; virtual std::vector diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -29,10 +29,6 @@ std::vector processMessagesResults(SQLiteStatementWrapper &preparedSQL) const; std::string getThickThreadTypesList() const; - void copyTablesDataUsingAttach( - sqlite3 *db, - const std::string &sourceDbPath, - const std::vector &tableNames) const; public: SQLiteQueryExecutor(std::string sqliteFilePath); @@ -152,6 +148,7 @@ std::string maxVersion) const override; void restoreFromBackupLog( const std::vector &backupLog) const override; + void copyContentFromDatabase(const std::string databasePath) const override; void addOutboundP2PMessages( const std::vector &messages) const override; std::vector getOutboundP2PMessagesByID( 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 @@ -1518,28 +1518,6 @@ this->getConnection(), query, types); } -void SQLiteQueryExecutor::copyTablesDataUsingAttach( - sqlite3 *db, - const std::string &sourceDbPath, - const std::vector &tableNames) const { - if (!SQLiteUtils::fileExists(sourceDbPath)) { - std::stringstream errorMessage; - errorMessage << "Error: File does not exist at path: " << sourceDbPath - << std::endl; - Logger::log(errorMessage.str()); - throw std::runtime_error(errorMessage.str()); - } - - std::ostringstream sql; - sql << "ATTACH DATABASE '" << sourceDbPath << "' AS sourceDB KEY '';"; - for (const auto &tableName : tableNames) { - sql << "INSERT OR IGNORE INTO " << tableName << " SELECT *" - << " FROM sourceDB." << tableName << ";" << std::endl; - } - sql << "DETACH DATABASE sourceDB;"; - executeQuery(db, sql.str()); -} - void SQLiteQueryExecutor::restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey, @@ -1596,18 +1574,36 @@ std::vector tablesVector( SQLiteBackup::tablesAllowlist.begin(), SQLiteBackup::tablesAllowlist.end()); - copyTablesDataUsingAttach( - this->getConnection(), plaintextBackupPath, tablesVector); - - SQLiteUtils::attemptDeleteFile( - plaintextBackupPath, - "Failed to delete plaintext compaction file after successful restore."); SQLiteUtils::attemptDeleteFile( mainCompactionPath, "Failed to delete main compaction file after successful restore."); } +void SQLiteQueryExecutor::copyContentFromDatabase( + const std::string sourceDatabasePath) const { + std::vector tableNames( + SQLiteBackup::tablesAllowlist.begin(), + SQLiteBackup::tablesAllowlist.end()); + + if (!SQLiteUtils::fileExists(sourceDatabasePath)) { + std::stringstream errorMessage; + errorMessage << "Error: Source file does not exist at path: " + << sourceDatabasePath << std::endl; + Logger::log(errorMessage.str()); + throw std::runtime_error(errorMessage.str()); + } + + std::ostringstream sql; + sql << "ATTACH DATABASE '" << sourceDatabasePath << "' AS sourceDB KEY '';"; + for (const auto &tableName : tableNames) { + sql << "INSERT OR IGNORE INTO " << tableName << " SELECT *" + << " FROM sourceDB." << tableName << ";" << std::endl; + } + sql << "DETACH DATABASE sourceDB;"; + executeQuery(this->getConnection(), sql.str()); +} + void SQLiteQueryExecutor::restoreFromBackupLog( const std::vector &backupLog) const { this->connectionManager->restoreFromBackupLog(backupLog); diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -296,6 +296,9 @@ &SQLiteQueryExecutor::restoreFromMainCompaction) .function( "restoreFromBackupLog", &SQLiteQueryExecutor::restoreFromBackupLog) + .function( + "copyContentFromDatabase", + &SQLiteQueryExecutor::copyContentFromDatabase) .function( "addOutboundP2PMessages", &SQLiteQueryExecutor::addOutboundP2PMessages) 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$@ { + let mainQueryExecutor, backupQueryExecutor; + let dbModule; + + beforeAll(async () => { + dbModule = getDatabaseModule(); + }); + + beforeEach(() => { + if (!dbModule) { + throw new Error('Database module is missing'); + } + mainQueryExecutor = new dbModule.SQLiteQueryExecutor(MAIN_FILE_PATH); + if (!mainQueryExecutor) { + throw new Error('Main SQLiteQueryExecutor is missing'); + } + + backupQueryExecutor = new dbModule.SQLiteQueryExecutor(BACKUP_FILE_PATH); + if (!backupQueryExecutor) { + throw new Error('Backup SQLiteQueryExecutor is missing'); + } + + backupQueryExecutor.setMetadata( + METADATA_KEY, + METADATA_DEFAULT_VALUE_BACKUP, + ); + }); + + afterEach(() => { + if (!dbModule) { + return; + } + if (mainQueryExecutor) { + clearSensitiveData(dbModule, MAIN_FILE_PATH, mainQueryExecutor); + } + if (backupQueryExecutor) { + clearSensitiveData(dbModule, BACKUP_FILE_PATH, backupQueryExecutor); + } + }); + + it('changes in one database should not affect other', () => { + const customValue = 'custom_value;'; + mainQueryExecutor?.setMetadata(METADATA_KEY, customValue); + + expect(mainQueryExecutor.getMetadata(METADATA_KEY)).toBe(customValue); + + // set in `beforeEach` + expect(backupQueryExecutor.getMetadata(METADATA_KEY)).toBe( + METADATA_DEFAULT_VALUE_BACKUP, + ); + }); + + it('copies content between databases', () => { + expect(mainQueryExecutor.getMetadata(METADATA_KEY)).toBe( + METADATA_DEFAULT_VALUE_MAIN, + ); + + // should not copy content that is not in `tablesAllowlist` + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH); + expect(mainQueryExecutor.getMetadata(METADATA_KEY)).toBe( + METADATA_DEFAULT_VALUE_MAIN, + ); + + const draftKey = 'conversation_id'; + const draftContent = 'typed_message'; + backupQueryExecutor.updateDraft(draftKey, draftContent); + + let draft; + // present in backup + draft = backupQueryExecutor.getAllDrafts().find(d => d.key === draftKey); + expect(draft?.text).toBe(draftContent); + // missing in main + draft = mainQueryExecutor.getAllDrafts().find(d => d.key === draftKey); + expect(draft).toBeUndefined(); + + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH); + // present in backup + draft = backupQueryExecutor.getAllDrafts().find(d => d.key === draftKey); + expect(draft?.text).toBe(draftContent); + // present in backup + draft = mainQueryExecutor.getAllDrafts().find(d => d.key === draftKey); + expect(draft?.text).toBe(draftContent); + }); + + it('do not override content from main database', () => { + const draftKey = 'conversation_id'; + const mainDraftContent = 'main_draft'; + const backupDraftContent = 'backup_draft'; + mainQueryExecutor.updateDraft(draftKey, mainDraftContent); + backupQueryExecutor.updateDraft(draftKey, backupDraftContent); + + mainQueryExecutor.copyContentFromDatabase(BACKUP_FILE_PATH); + + const mainDraft = mainQueryExecutor + .getAllDrafts() + .find(d => d.key === draftKey); + + expect(mainDraft?.text).toBe(mainDraftContent); + + const backupDraft = backupQueryExecutor + .getAllDrafts() + .find(d => d.key === draftKey); + expect(backupDraft?.text).toBe(backupDraftContent); + }); +}); diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -182,6 +182,8 @@ restoreFromBackupLog(backupLog: Uint8Array): void; + copyContentFromDatabase(databasePath: string): void; + addOutboundP2PMessages(messages: $ReadOnlyArray): void; removeOutboundP2PMessage(confirmedMessageID: string, deviceID: string): void; removeAllOutboundP2PMessages(deviceID: string): void;