diff --git a/.changeset/angry-carrots-lay.md b/.changeset/angry-carrots-lay.md new file mode 100644 index 0000000..30d2a48 --- /dev/null +++ b/.changeset/angry-carrots-lay.md @@ -0,0 +1,5 @@ +--- +"@journeyapps/react-native-quick-sqlite": minor +--- + +Added `refreshSchema()` to bindings. Will cause all connections to be aware of a schema change. diff --git a/cpp/ConnectionPool.cpp b/cpp/ConnectionPool.cpp index 03b2f4b..fff9e6f 100644 --- a/cpp/ConnectionPool.cpp +++ b/cpp/ConnectionPool.cpp @@ -168,6 +168,22 @@ void ConnectionPool::closeAll() { } } +std::future ConnectionPool::refreshSchema() { + std::vector> futures; + + futures.push_back(writeConnection.refreshSchema()); + + for (unsigned int i = 0; i < maxReads; i++) { + futures.push_back(readConnections[i]->refreshSchema()); + } + + return std::async(std::launch::async, [futures = std::move(futures)]() mutable { + for (auto& future : futures) { + future.get(); + } + }); +} + SQLiteOPResult ConnectionPool::attachDatabase(std::string const dbFileName, std::string const docPath, std::string const alias) { diff --git a/cpp/ConnectionPool.h b/cpp/ConnectionPool.h index 28d9b1f..31df22b 100644 --- a/cpp/ConnectionPool.h +++ b/cpp/ConnectionPool.h @@ -3,6 +3,7 @@ #include "sqlite3.h" #include #include +#include #ifndef ConnectionPool_h #define ConnectionPool_h @@ -126,6 +127,11 @@ class ConnectionPool { */ void closeAll(); + /** + * Refreshes the schema for all connections. + */ + std::future refreshSchema(); + /** * Attaches another database to all connections */ diff --git a/cpp/ConnectionState.cpp b/cpp/ConnectionState.cpp index d50dfee..2a64dca 100644 --- a/cpp/ConnectionState.cpp +++ b/cpp/ConnectionState.cpp @@ -44,6 +44,25 @@ bool ConnectionState::matchesLock(const ConnectionLockId &lockId) { bool ConnectionState::isEmptyLock() { return _currentLockId == EMPTY_LOCK_ID; } +std::future ConnectionState::refreshSchema() { + auto promise = std::make_shared>(); + auto future = promise->get_future(); + + queueWork([promise](sqlite3* db) { + try { + int rc = sqlite3_exec(db, "PRAGMA table_info('sqlite_master')", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) { + throw std::runtime_error("Failed to refresh schema"); + } + promise->set_value(); + } catch (...) { + promise->set_exception(std::current_exception()); + } + }); + + return future; +} + void ConnectionState::close() { waitFinished(); // So that the thread can stop (if not already) diff --git a/cpp/ConnectionState.h b/cpp/ConnectionState.h index 2eda1a4..f4d459f 100644 --- a/cpp/ConnectionState.h +++ b/cpp/ConnectionState.h @@ -6,6 +6,7 @@ #include #include #include +#include #ifndef ConnectionState_h #define ConnectionState_h @@ -41,6 +42,7 @@ class ConnectionState { bool matchesLock(const ConnectionLockId &lockId); bool isEmptyLock(); + std::future refreshSchema(); void close(); void queueWork(std::function task); diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index 99cbfb6..aa24d29 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -286,6 +286,51 @@ void osp::install(jsi::Runtime &rt, return {}; }); + auto refreshSchema = HOSTFN("refreshSchema", 1) { + if (count == 0) { + throw jsi::JSError(rt, "[react-native-quick-sqlite][refreshSchema] database name is required"); + } + + if (!args[0].isString()) { + throw jsi::JSError(rt, "[react-native-quick-sqlite][refreshSchema] database name must be a string"); + } + + std::string dbName = args[0].asString(rt).utf8(rt); + + auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); + auto jsPromise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) { + auto resolve = std::make_shared(rt, args[0]); + auto reject = std::make_shared(rt, args[1]); + + try { + auto future = sqliteRefreshSchema(dbName); + + // Waiting for the future to complete in a separate thread + std::thread([future = std::move(future), &rt, resolve, reject]() mutable { + try { + future.get(); + invoker->invokeAsync([&rt, resolve] { + resolve->asObject(rt).asFunction(rt).call(rt); + }); + } catch (const std::exception& exc) { + invoker->invokeAsync([&rt, reject, exc] { + auto errorCtr = rt.global().getPropertyAsFunction(rt, "Error"); + auto error = errorCtr.callAsConstructor(rt, jsi::String::createFromUtf8(rt, exc.what())); + reject->asObject(rt).asFunction(rt).call(rt, error); + }); + } + }).detach(); + + } catch (const std::exception& exc) { + invoker->invokeAsync([&rt, &exc] { jsi::JSError(rt, exc.what()); }); + } + + return {}; + })); + + return jsPromise; + }); + auto executeInContext = HOSTFN("executeInContext", 3) { if (count < 4) { throw jsi::JSError(rt, @@ -500,6 +545,7 @@ void osp::install(jsi::Runtime &rt, module.setProperty(rt, "releaseLock", move(releaseLock)); module.setProperty(rt, "executeInContext", move(executeInContext)); module.setProperty(rt, "close", move(close)); + module.setProperty(rt, "refreshSchema", move(refreshSchema)); module.setProperty(rt, "attach", move(attach)); module.setProperty(rt, "detach", move(detach)); diff --git a/cpp/sqliteBridge.cpp b/cpp/sqliteBridge.cpp index 875b9f1..3f39eb7 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -60,6 +60,17 @@ sqliteOpenDb(string const dbName, string const docPath, }; } +std::future sqliteRefreshSchema(const std::string& dbName) { + if (dbMap.count(dbName) == 0) { + std::promise promise; + promise.set_value(); + return promise.get_future(); + } + + ConnectionPool* connection = dbMap[dbName]; + return connection->refreshSchema(); +} + SQLiteOPResult sqliteCloseDb(string const dbName) { if (dbMap.count(dbName) == 0) { return generateNotOpenResult(dbName); diff --git a/cpp/sqliteBridge.h b/cpp/sqliteBridge.h index 06fd59f..2020931 100644 --- a/cpp/sqliteBridge.h +++ b/cpp/sqliteBridge.h @@ -11,6 +11,7 @@ #include "JSIHelper.h" #include "sqlite3.h" #include +#include #ifndef SQLiteBridge_h #define SQLiteBridge_h @@ -33,6 +34,8 @@ sqliteOpenDb(std::string const dbName, std::string const docPath, const TransactionCallbackPayload *event), uint32_t numReadConnections); +std::future sqliteRefreshSchema(const std::string& dbName); + SQLiteOPResult sqliteCloseDb(string const dbName); void sqliteCloseAll(); diff --git a/src/setup-open.ts b/src/setup-open.ts index c62b0a6..229907f 100644 --- a/src/setup-open.ts +++ b/src/setup-open.ts @@ -225,6 +225,7 @@ export function setupOpen(QuickSQLite: ISQLite) { // Return the concurrent connection object return { close: () => QuickSQLite.close(dbName), + refreshSchema: () => QuickSQLite.refreshSchema(dbName), execute: (sql: string, args?: any[]) => writeLock((context) => context.execute(sql, args)), readLock, readTransaction: async (callback: (context: TransactionContext) => Promise, options?: LockOptions) => diff --git a/src/types.ts b/src/types.ts index c4c29ac..901301a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -124,6 +124,7 @@ export interface ISQLite { open: Open; close: (dbName: string) => void; delete: (dbName: string, location?: string) => void; + refreshSchema: (dbName: string) => Promise; requestLock: (dbName: string, id: ContextLockID, type: ConcurrentLockType) => QueryResult; releaseLock(dbName: string, id: ContextLockID): void; @@ -151,6 +152,7 @@ export interface TransactionContext extends LockContext { export type QuickSQLiteConnection = { close: () => void; + refreshSchema: () => Promise; execute: (sql: string, args?: any[]) => Promise; readLock: (callback: (context: LockContext) => Promise, options?: LockOptions) => Promise; readTransaction: (callback: (context: TransactionContext) => Promise, options?: LockOptions) => Promise;