From eba0186ffc1f1c3ca120a35c87e05b78065318df Mon Sep 17 00:00:00 2001 From: alex choi Date: Fri, 9 Sep 2022 11:40:12 -0400 Subject: [PATCH 1/2] added readFromFile and writeToFile for db backup and restore --- native/shared/Database.cpp | 50 ++++++++++++++++++++++++++ native/shared/Database.h | 1 + native/shared/DatabaseInstallation.cpp | 8 +++++ src/Database/index.js | 10 ++++++ src/adapters/compat.js | 4 +++ src/adapters/lokijs/index.js | 4 +++ src/adapters/sqlite/index.d.ts | 2 ++ src/adapters/sqlite/index.js | 4 +++ src/adapters/sqlite/type.js | 1 + src/adapters/type.d.ts | 3 ++ src/adapters/type.js | 3 ++ 11 files changed, 90 insertions(+) diff --git a/native/shared/Database.cpp b/native/shared/Database.cpp index 1a1cabf43..4f29d8b05 100644 --- a/native/shared/Database.cpp +++ b/native/shared/Database.cpp @@ -914,4 +914,54 @@ jsi::Value Database::getLocal(jsi::String &key) { return jsi::String::createFromUtf8(rt, text); } +void Database::loadOrSaveDb(jsi::String &zFilename, int isSave) { + + auto &rt = getRt(); + sqlite3 *pInMemory = db_->sqlite; + + int rc; /* Function return code */ + sqlite3 *pFile; /* Database connection opened on zFilename */ + sqlite3_backup *pBackup; /* Backup object used to copy data */ + sqlite3 *pTo; /* Database to copy to (pFile or pInMemory) */ + sqlite3 *pFrom; /* Database to copy from (pFile or pInMemory) */ + + /* Open the database file identified by zFilename. Exit early if this fails + ** for any reason. */ + rc = sqlite3_open(zFilename.utf8(rt).c_str(), &pFile); + if( rc==SQLITE_OK ){ + + /* If this is a 'load' operation (isSave==0), then data is copied + ** from the database file just opened to database pInMemory. + ** Otherwise, if this is a 'save' operation (isSave==1), then data + ** is copied from pInMemory to pFile. Set the variables pFrom and + ** pTo accordingly. */ + pFrom = (isSave ? pInMemory : pFile); + pTo = (isSave ? pFile : pInMemory); + + /* Set up the backup procedure to copy from the "main" database of + ** connection pFile to the main database of connection pInMemory. + ** If something goes wrong, pBackup will be set to NULL and an error + ** code and message left in connection pTo. + ** + ** If the backup object is successfully created, call backup_step() + ** to copy data from pFile to pInMemory. Then call backup_finish() + ** to release resources associated with the pBackup object. If an + ** error occurred, then an error code and message will be left in + ** connection pTo. If no error occurred, then the error code belonging + ** to pTo is set to SQLITE_OK. + */ + pBackup = sqlite3_backup_init(pTo, "main", pFrom, "main"); + if( pBackup ){ + (void)sqlite3_backup_step(pBackup, -1); + (void)sqlite3_backup_finish(pBackup); + } + rc = sqlite3_errcode(pTo); + } + + /* Close the database connection opened on database file zFilename + ** and return the result of this function. */ + (void)sqlite3_close(pFile); +// return rc; +} + } // namespace watermelondb diff --git a/native/shared/Database.h b/native/shared/Database.h index 6c9bce496..6c0a697a2 100644 --- a/native/shared/Database.h +++ b/native/shared/Database.h @@ -32,6 +32,7 @@ class Database : public jsi::HostObject { void unsafeResetDatabase(jsi::String &schema, int schemaVersion); jsi::Value getLocal(jsi::String &key); void executeMultiple(std::string sql); + void loadOrSaveDb(jsi::String &fileName, int isSave); private: bool initialized_; diff --git a/native/shared/DatabaseInstallation.cpp b/native/shared/DatabaseInstallation.cpp index aa847e3d2..7387050fe 100644 --- a/native/shared/DatabaseInstallation.cpp +++ b/native/shared/DatabaseInstallation.cpp @@ -264,6 +264,14 @@ void Database::install(jsi::Runtime *runtime) { std::abort(); } }); + createMethod(rt, adapter, "loadOrSaveDb", 2, [database](jsi::Runtime &rt, const jsi::Value *args) { + assert(database->initialized_); + jsi::String filePath = args[0].getString(rt); + int isSave = (int)args[1].asBool(); + + database->loadOrSaveDb(filePath, isSave); + return jsi::Value::undefined(); + }); createMethod(rt, adapter, "unsafeClose", 0, [database](jsi::Runtime &rt, const jsi::Value *args) { assert(database->initialized_); database->destroy(); diff --git a/src/Database/index.js b/src/Database/index.js index df9ca4f01..1f0c3d652 100644 --- a/src/Database/index.js +++ b/src/Database/index.js @@ -269,6 +269,16 @@ export default class Database { } } + async writeToFile(filePath: string): Promise { + const result = await this.adapter.loadOrSaveDb(filePath, true) + return result + } + + async readFromFile(filePath: string): Promise { + const result = await this.adapter.loadOrSaveDb(filePath, false) + return result + } + _ensureInWriter(diagnosticMethodName: string): void { invariant( this._workQueue.isWriterRunning, diff --git a/src/adapters/compat.js b/src/adapters/compat.js index c5175c010..77372eeeb 100644 --- a/src/adapters/compat.js +++ b/src/adapters/compat.js @@ -97,6 +97,10 @@ export default class DatabaseAdapterCompat { return toPromise((callback) => this.underlyingAdapter.removeLocal(key, callback)) } + loadOrSaveDb(filePath: string, isSave: boolean): Promise { + return toPromise((callback) => this.underlyingAdapter.loadOrSaveDb(filePath, isSave, callback)) + } + // untyped - test-only code async testClone(options: any): Promise { // $FlowFixMe diff --git a/src/adapters/lokijs/index.js b/src/adapters/lokijs/index.js index 8ebbb8066..af0b94380 100644 --- a/src/adapters/lokijs/index.js +++ b/src/adapters/lokijs/index.js @@ -234,6 +234,10 @@ export default class LokiJSAdapter implements DatabaseAdapter { this._dispatcher.call('removeLocal', [key], callback) } + loadOrSaveDb(filePath: string, isSave: boolean, callback: ResultCallback): void { + callback({ error: new Error('loadOrSaveDb unavailable on LokiJS') }) + } + // dev/debug utility get _driver(): any { // $FlowFixMe diff --git a/src/adapters/sqlite/index.d.ts b/src/adapters/sqlite/index.d.ts index 35f32fd42..e7d07539a 100644 --- a/src/adapters/sqlite/index.d.ts +++ b/src/adapters/sqlite/index.d.ts @@ -59,6 +59,8 @@ declare module '@nozbe/watermelondb/adapters/sqlite' { setLocal(key: string, value: string): Promise + loadOrSaveDb(filePath: string, isSave: Boolean): Promise + unsafeClearCachedRecords(): Promise unsafeResetDatabase(): Promise diff --git a/src/adapters/sqlite/index.js b/src/adapters/sqlite/index.js index 80736fa82..e88107d38 100644 --- a/src/adapters/sqlite/index.js +++ b/src/adapters/sqlite/index.js @@ -375,6 +375,10 @@ export default class SQLiteAdapter implements DatabaseAdapter { this._dispatcher.call('batch', [[operation]], callback) } + loadOrSaveDb(filePath: string, isSave: boolean, callback: ResultCallback): void { + this._dispatcher.call('loadOrSaveDb', [filePath, isSave], callback) + } + removeLocal(key: string, callback: ResultCallback): void { const operation = [IGNORE_CACHE, null, `delete from "local_storage" where "key" == ?`, [[key]]] this._dispatcher.call('batch', [[operation]], callback) diff --git a/src/adapters/sqlite/type.js b/src/adapters/sqlite/type.js index 0847b0740..64af4c29b 100644 --- a/src/adapters/sqlite/type.js +++ b/src/adapters/sqlite/type.js @@ -73,6 +73,7 @@ export type SqliteDispatcherMethod = | 'unsafeResetDatabase' | 'getLocal' | 'unsafeExecuteMultiple' + | 'loadOrSaveDb' export interface SqliteDispatcher { call(methodName: SqliteDispatcherMethod, args: any[], callback: ResultCallback): void; diff --git a/src/adapters/type.d.ts b/src/adapters/type.d.ts index d5f3c05b7..583b6d791 100644 --- a/src/adapters/type.d.ts +++ b/src/adapters/type.d.ts @@ -77,4 +77,7 @@ export interface DatabaseAdapter { // Removes key from local storage removeLocal(key: string, callback: ResultCallback): void; + + // Backup / restore db: load from or save into filePath + loadOrSaveDb(filePath: string, isSave: Boolean, callback: ResultCallback): void; } diff --git a/src/adapters/type.js b/src/adapters/type.js index 892954103..42753fddf 100644 --- a/src/adapters/type.js +++ b/src/adapters/type.js @@ -79,4 +79,7 @@ export interface DatabaseAdapter { // Removes key from local storage removeLocal(key: string, callback: ResultCallback): void; + + // Backup / restore db: load from or save into filePath + loadOrSaveDb(filePath: string, isSave: boolean, callback: ResultCallback): void; } From fd95dfc8949ff51a004aa3f95789e214e086c087 Mon Sep 17 00:00:00 2001 From: alex choi Date: Mon, 12 Sep 2022 11:11:33 -0400 Subject: [PATCH 2/2] updated unreleased changelog --- CHANGELOG-Unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-Unreleased.md b/CHANGELOG-Unreleased.md index 26ead384e..1af8c3f2c 100644 --- a/CHANGELOG-Unreleased.md +++ b/CHANGELOG-Unreleased.md @@ -17,6 +17,7 @@ - [adapters] Adapter objects now returns `dbName` - [TypeScript] Add unsafeExecute method - [TypeScript] Add localStorage property to Database +- [iOS] Added ability to backup and restore sqlite db with `database.writeToFile(filePath)` and `database.readFromFile(filePath)`. ### Performance