From da57efdc3d022cdd9e96068446fbd36c058b3dd5 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Tue, 29 Oct 2024 16:59:45 +0200 Subject: [PATCH 1/6] Added refreshSchema to bindings. Will cause all connections to be aware of a schema change. --- .changeset/angry-carrots-lay.md | 5 +++++ cpp/ConnectionPool.cpp | 8 ++++++++ cpp/ConnectionPool.h | 5 +++++ cpp/ConnectionState.cpp | 6 ++++++ cpp/ConnectionState.h | 1 + cpp/bindings.cpp | 15 +++++++++++++++ cpp/sqliteBridge.cpp | 11 +++++++++++ cpp/sqliteBridge.h | 2 ++ src/setup-open.ts | 1 + src/types.ts | 2 ++ 10 files changed, 56 insertions(+) create mode 100644 .changeset/angry-carrots-lay.md diff --git a/.changeset/angry-carrots-lay.md b/.changeset/angry-carrots-lay.md new file mode 100644 index 0000000..cc42a06 --- /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..1ba2170 100644 --- a/cpp/ConnectionPool.cpp +++ b/cpp/ConnectionPool.cpp @@ -168,6 +168,14 @@ void ConnectionPool::closeAll() { } } +void ConnectionPool::refreshSchema() { + writeConnection.refreshSchema(); + + for (unsigned int i = 0; i < maxReads; i++) { + readConnections[i]->refreshSchema(); + } +} + 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..d76b651 100644 --- a/cpp/ConnectionPool.h +++ b/cpp/ConnectionPool.h @@ -126,6 +126,11 @@ class ConnectionPool { */ void closeAll(); + /** + * Refreshes the schema for all connections. + */ + void refreshSchema(); + /** * Attaches another database to all connections */ diff --git a/cpp/ConnectionState.cpp b/cpp/ConnectionState.cpp index d50dfee..86ae13f 100644 --- a/cpp/ConnectionState.cpp +++ b/cpp/ConnectionState.cpp @@ -44,6 +44,12 @@ bool ConnectionState::matchesLock(const ConnectionLockId &lockId) { bool ConnectionState::isEmptyLock() { return _currentLockId == EMPTY_LOCK_ID; } +void ConnectionState::refreshSchema() { + queueWork([this](sqlite3* db) { + sqlite3_exec(db, "PRAGMA table_info('sqlite_master')", nullptr, nullptr, nullptr); + }); +} + 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..9ba0fe6 100644 --- a/cpp/ConnectionState.h +++ b/cpp/ConnectionState.h @@ -41,6 +41,7 @@ class ConnectionState { bool matchesLock(const ConnectionLockId &lockId); bool isEmptyLock(); + void refreshSchema(); void close(); void queueWork(std::function task); diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index 99cbfb6..de05670 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -286,6 +286,20 @@ 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"); + } + + string dbName = args[0].asString(rt).utf8(rt); + + sqliteRefreshSchema(dbName); +}); + auto executeInContext = HOSTFN("executeInContext", 3) { if (count < 4) { throw jsi::JSError(rt, @@ -500,6 +514,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..5c2e0d3 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -60,6 +60,17 @@ sqliteOpenDb(string const dbName, string const docPath, }; } +void sqliteRefreshSchema(string const dbName) { + if (dbMap.count(dbName) == 0) { + // Do nothing + return; + } + + ConnectionPool *connection = dbMap[dbName]; + 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..ae65001 100644 --- a/cpp/sqliteBridge.h +++ b/cpp/sqliteBridge.h @@ -33,6 +33,8 @@ sqliteOpenDb(std::string const dbName, std::string const docPath, const TransactionCallbackPayload *event), uint32_t numReadConnections); +void sqliteRefreshSchema(string const 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..b75f1f9 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) => void; 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: () => void; execute: (sql: string, args?: any[]) => Promise; readLock: (callback: (context: LockContext) => Promise, options?: LockOptions) => Promise; readTransaction: (callback: (context: TransactionContext) => Promise, options?: LockOptions) => Promise; From ced1af6ecc58704ed6ad11537ddf418d69ea044a Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Tue, 29 Oct 2024 19:47:07 +0200 Subject: [PATCH 2/6] Return for binding function. --- cpp/bindings.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index de05670..ec06062 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -298,6 +298,8 @@ auto refreshSchema = HOSTFN("refreshSchema", 1) { string dbName = args[0].asString(rt).utf8(rt); sqliteRefreshSchema(dbName); + + return {}; }); auto executeInContext = HOSTFN("executeInContext", 3) { From 2cb4a1aa882df79503c04c6a197c865432d22beb Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 31 Oct 2024 10:09:31 +0200 Subject: [PATCH 3/6] Formatting. --- cpp/bindings.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index ec06062..829dca3 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -286,21 +286,21 @@ 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"); - } + 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"); - } + if (!args[0].isString()) { + throw jsi::JSError(rt, "[react-native-quick-sqlite][refreshSchema] database name must be a string"); + } - string dbName = args[0].asString(rt).utf8(rt); + string dbName = args[0].asString(rt).utf8(rt); - sqliteRefreshSchema(dbName); + sqliteRefreshSchema(dbName); - return {}; -}); + return {}; + }); auto executeInContext = HOSTFN("executeInContext", 3) { if (count < 4) { From 2c42f0cfa51e9abf36784f281129ea61ca651ae5 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 31 Oct 2024 12:28:39 +0200 Subject: [PATCH 4/6] Changing refreshSchema to return a promise. --- cpp/ConnectionPool.cpp | 21 ++++++++++++----- cpp/ConnectionPool.h | 3 ++- cpp/ConnectionState.cpp | 24 ++++++++++++++++---- cpp/ConnectionState.h | 3 ++- cpp/bindings.cpp | 50 +++++++++++++++++++++++++++++++++-------- cpp/sqliteBridge.cpp | 17 ++++++++------ cpp/sqliteBridge.h | 3 ++- src/types.ts | 4 ++-- 8 files changed, 95 insertions(+), 30 deletions(-) diff --git a/cpp/ConnectionPool.cpp b/cpp/ConnectionPool.cpp index 1ba2170..7f7d0be 100644 --- a/cpp/ConnectionPool.cpp +++ b/cpp/ConnectionPool.cpp @@ -168,12 +168,23 @@ void ConnectionPool::closeAll() { } } -void ConnectionPool::refreshSchema() { - writeConnection.refreshSchema(); +std::future ConnectionPool::refreshSchema() { + std::vector> futures; - for (unsigned int i = 0; i < maxReads; i++) { - readConnections[i]->refreshSchema(); - } + // Collect the future from the write connection + futures.push_back(writeConnection.refreshSchema()); + + // Collect the futures from the read connections + for (unsigned int i = 0; i < maxReads; i++) { + futures.push_back(readConnections[i]->refreshSchema()); + } + + // Return a future that completes when all collected futures are done + return std::async(std::launch::async, [futures = std::move(futures)]() mutable { + for (auto& fut : futures) { + fut.get(); // This will block until the future is ready and rethrow exceptions if any + } + }); } SQLiteOPResult ConnectionPool::attachDatabase(std::string const dbFileName, diff --git a/cpp/ConnectionPool.h b/cpp/ConnectionPool.h index d76b651..074d949 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 @@ -129,7 +130,7 @@ class ConnectionPool { /** * Refreshes the schema for all connections. */ - void refreshSchema(); + std::future ConnectionPool::refreshSchema(); /** * Attaches another database to all connections diff --git a/cpp/ConnectionState.cpp b/cpp/ConnectionState.cpp index 86ae13f..78b4d91 100644 --- a/cpp/ConnectionState.cpp +++ b/cpp/ConnectionState.cpp @@ -2,6 +2,8 @@ #include "fileUtils.h" #include "sqlite3.h" + + const std::string EMPTY_LOCK_ID = ""; SQLiteOPResult genericSqliteOpenDb(string const dbName, string const docPath, @@ -44,12 +46,26 @@ bool ConnectionState::matchesLock(const ConnectionLockId &lockId) { bool ConnectionState::isEmptyLock() { return _currentLockId == EMPTY_LOCK_ID; } -void ConnectionState::refreshSchema() { - queueWork([this](sqlite3* db) { - sqlite3_exec(db, "PRAGMA table_info('sqlite_master')", nullptr, nullptr, nullptr); - }); +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 9ba0fe6..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,7 +42,7 @@ class ConnectionState { bool matchesLock(const ConnectionLockId &lockId); bool isEmptyLock(); - void refreshSchema(); + std::future refreshSchema(); void close(); void queueWork(std::function task); diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index 829dca3..8d40095 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -286,20 +286,52 @@ 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 (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"); - } + if (!args[0].isString()) { + throw jsi::JSError(rt, "[react-native-quick-sqlite][refreshSchema] database name must be a string"); + } - string dbName = args[0].asString(rt).utf8(rt); + std::string dbName = args[0].asString(rt).utf8(rt); - sqliteRefreshSchema(dbName); + 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]); - return {}; + 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) { + 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); + } + + return {}; + })); + + return jsPromise; }); auto executeInContext = HOSTFN("executeInContext", 3) { diff --git a/cpp/sqliteBridge.cpp b/cpp/sqliteBridge.cpp index 5c2e0d3..824853c 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -60,14 +60,17 @@ sqliteOpenDb(string const dbName, string const docPath, }; } -void sqliteRefreshSchema(string const dbName) { - if (dbMap.count(dbName) == 0) { - // Do nothing - return; - } - ConnectionPool *connection = dbMap[dbName]; - connection->refreshSchema(); +std::future sqliteRefreshSchema(const std::string& dbName) { + if (dbMap.count(dbName) == 0) { + // Database not found, return a future that's already set + std::promise promise; + promise.set_value(); + return promise.get_future(); + } + + ConnectionPool* connection = dbMap[dbName]; + return connection->refreshSchema(); } diff --git a/cpp/sqliteBridge.h b/cpp/sqliteBridge.h index ae65001..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,7 +34,7 @@ sqliteOpenDb(std::string const dbName, std::string const docPath, const TransactionCallbackPayload *event), uint32_t numReadConnections); -void sqliteRefreshSchema(string const dbName); +std::future sqliteRefreshSchema(const std::string& dbName); SQLiteOPResult sqliteCloseDb(string const dbName); diff --git a/src/types.ts b/src/types.ts index b75f1f9..901301a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -124,7 +124,7 @@ export interface ISQLite { open: Open; close: (dbName: string) => void; delete: (dbName: string, location?: string) => void; - refreshSchema: (dbName: string) => void; + refreshSchema: (dbName: string) => Promise; requestLock: (dbName: string, id: ContextLockID, type: ConcurrentLockType) => QueryResult; releaseLock(dbName: string, id: ContextLockID): void; @@ -152,7 +152,7 @@ export interface TransactionContext extends LockContext { export type QuickSQLiteConnection = { close: () => void; - refreshSchema: () => 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; From 99a3761c0d4aa2a16771674520ef52b174a0a947 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 31 Oct 2024 13:22:22 +0200 Subject: [PATCH 5/6] Fixed header defitinion. --- cpp/ConnectionPool.cpp | 7 ++----- cpp/ConnectionPool.h | 2 +- cpp/ConnectionState.cpp | 3 --- cpp/bindings.cpp | 5 +---- cpp/sqliteBridge.cpp | 3 --- 5 files changed, 4 insertions(+), 16 deletions(-) diff --git a/cpp/ConnectionPool.cpp b/cpp/ConnectionPool.cpp index 7f7d0be..fff9e6f 100644 --- a/cpp/ConnectionPool.cpp +++ b/cpp/ConnectionPool.cpp @@ -171,18 +171,15 @@ void ConnectionPool::closeAll() { std::future ConnectionPool::refreshSchema() { std::vector> futures; - // Collect the future from the write connection futures.push_back(writeConnection.refreshSchema()); - // Collect the futures from the read connections for (unsigned int i = 0; i < maxReads; i++) { futures.push_back(readConnections[i]->refreshSchema()); } - // Return a future that completes when all collected futures are done return std::async(std::launch::async, [futures = std::move(futures)]() mutable { - for (auto& fut : futures) { - fut.get(); // This will block until the future is ready and rethrow exceptions if any + for (auto& future : futures) { + future.get(); } }); } diff --git a/cpp/ConnectionPool.h b/cpp/ConnectionPool.h index 074d949..31df22b 100644 --- a/cpp/ConnectionPool.h +++ b/cpp/ConnectionPool.h @@ -130,7 +130,7 @@ class ConnectionPool { /** * Refreshes the schema for all connections. */ - std::future ConnectionPool::refreshSchema(); + std::future refreshSchema(); /** * Attaches another database to all connections diff --git a/cpp/ConnectionState.cpp b/cpp/ConnectionState.cpp index 78b4d91..2a64dca 100644 --- a/cpp/ConnectionState.cpp +++ b/cpp/ConnectionState.cpp @@ -2,8 +2,6 @@ #include "fileUtils.h" #include "sqlite3.h" - - const std::string EMPTY_LOCK_ID = ""; SQLiteOPResult genericSqliteOpenDb(string const dbName, string const docPath, @@ -65,7 +63,6 @@ std::future ConnectionState::refreshSchema() { return future; } - void ConnectionState::close() { waitFinished(); // So that the thread can stop (if not already) diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index 8d40095..aa24d29 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -286,7 +286,6 @@ 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"); @@ -323,9 +322,7 @@ void osp::install(jsi::Runtime &rt, }).detach(); } catch (const std::exception& 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); + invoker->invokeAsync([&rt, &exc] { jsi::JSError(rt, exc.what()); }); } return {}; diff --git a/cpp/sqliteBridge.cpp b/cpp/sqliteBridge.cpp index 824853c..3f39eb7 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -60,10 +60,8 @@ sqliteOpenDb(string const dbName, string const docPath, }; } - std::future sqliteRefreshSchema(const std::string& dbName) { if (dbMap.count(dbName) == 0) { - // Database not found, return a future that's already set std::promise promise; promise.set_value(); return promise.get_future(); @@ -73,7 +71,6 @@ std::future sqliteRefreshSchema(const std::string& dbName) { return connection->refreshSchema(); } - SQLiteOPResult sqliteCloseDb(string const dbName) { if (dbMap.count(dbName) == 0) { return generateNotOpenResult(dbName); From 99e62c3084dfdc3094a44879666923b4d736e7e6 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 31 Oct 2024 15:10:29 +0200 Subject: [PATCH 6/6] Minor changeset change. --- .changeset/angry-carrots-lay.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/angry-carrots-lay.md b/.changeset/angry-carrots-lay.md index cc42a06..30d2a48 100644 --- a/.changeset/angry-carrots-lay.md +++ b/.changeset/angry-carrots-lay.md @@ -2,4 +2,4 @@ "@journeyapps/react-native-quick-sqlite": minor --- -Added refreshSchema to bindings. Will cause all connections to be aware of a schema change. +Added `refreshSchema()` to bindings. Will cause all connections to be aware of a schema change.