From b7d9c636da315c7a305cb292219a8ffe193ed795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Huy?= Date: Sun, 23 Aug 2026 01:24:36 +0700 Subject: [PATCH] fix: reject duplicate database opens --- .../tests/unit/specs/DatabaseQueue.spec.ts | 92 ++++++++++++++++++- .../cpp/NitroSQLiteException.hpp | 5 + .../cpp/operations.cpp | 23 +++-- 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/example/tests/unit/specs/DatabaseQueue.spec.ts b/example/tests/unit/specs/DatabaseQueue.spec.ts index d80c5acb..0c1520f5 100644 --- a/example/tests/unit/specs/DatabaseQueue.spec.ts +++ b/example/tests/unit/specs/DatabaseQueue.spec.ts @@ -7,12 +7,31 @@ import { } from '@tests/unit/common' import { describe, it } from '@tests/TestApi' import { testDb, testDbQueue } from '@tests/db' -import type { BatchQueryCommand } from 'react-native-nitro-sqlite' +import { + NitroSQLite, + NitroSQLiteError, + open, + type BatchQueryCommand, +} from 'react-native-nitro-sqlite' const TEST_QUERY = 'SELECT * FROM [User];' const TEST_BATCH_COMMANDS: BatchQueryCommand[] = [{ query: TEST_QUERY }] +function dropDatabaseIfExists(dbName: string, location?: string) { + try { + NitroSQLite.native.drop(dbName, location) + } catch (error) { + if ( + error instanceof Error && + error.message.includes('Database file not found') + ) { + return + } + throw error + } +} + export default function registerDatabaseQueueUnitTests() { describe('Database Queue', () => { it('multiple transactions are queued', async () => { @@ -178,5 +197,76 @@ export default function registerDatabaseQueueUnitTests() { } } }) + + it('rejects a duplicate session open without replacing the original connection', () => { + const dbName = 'duplicate-session-open' + dropDatabaseIfExists(dbName) + dropDatabaseIfExists(dbName, '..') + + const db = open({ name: dbName }) + + try { + db.execute('CREATE TABLE ConnectionMarker (value TEXT NOT NULL)') + db.execute('INSERT INTO ConnectionMarker (value) VALUES (?)', [ + 'original', + ]) + + let duplicateError: unknown + try { + open({ name: dbName, location: '..' }) + } catch (error) { + duplicateError = error + } + + expect(duplicateError).toBeInstanceOf(NitroSQLiteError) + expect((duplicateError as Error).message).toContain('already open') + expect( + db.execute<{ value: string }>('SELECT value FROM ConnectionMarker') + .results, + ).toEqual([{ value: 'original' }]) + } finally { + db.close() + dropDatabaseIfExists(dbName) + dropDatabaseIfExists(dbName, '..') + } + }) + + it('rejects duplicate direct native opens', () => { + const dbName = 'duplicate-native-open' + dropDatabaseIfExists(dbName) + + NitroSQLite.native.open(dbName) + + try { + NitroSQLite.execute( + dbName, + 'CREATE TABLE ConnectionMarker (value TEXT NOT NULL)', + ) + NitroSQLite.execute( + dbName, + 'INSERT INTO ConnectionMarker (value) VALUES (?)', + ['original'], + ) + + let duplicateError: unknown + try { + NitroSQLite.native.open(dbName) + } catch (error) { + duplicateError = error + } + + expect(duplicateError).toBeInstanceOf(Error) + expect((duplicateError as Error).message).toContain('already open') + expect( + NitroSQLite.execute<{ value: string }>( + dbName, + 'SELECT value FROM ConnectionMarker', + ).results, + ).toEqual([{ value: 'original' }]) + } finally { + NitroSQLite.native.close(dbName) + dropDatabaseIfExists(dbName) + } + }) }) } diff --git a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteException.hpp b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteException.hpp index e63f5d70..3c13b1fd 100644 --- a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteException.hpp +++ b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteException.hpp @@ -46,6 +46,11 @@ class NitroSQLiteException : public std::exception { return this->_exceptionString.c_str(); } + static NitroSQLiteException DatabaseAlreadyOpen(const std::string& dbName) { + return NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, + "Database " + dbName + " is already open. There is already a connection to the database."); + } + static NitroSQLiteException DatabaseNotOpen(const std::string& dbName) { return NitroSQLiteException(NitroSQLiteExceptionType::UnableToAttachToDatabase, dbName + " is not open"); } diff --git a/packages/react-native-nitro-sqlite/cpp/operations.cpp b/packages/react-native-nitro-sqlite/cpp/operations.cpp index ae61dc65..0feafeb3 100644 --- a/packages/react-native-nitro-sqlite/cpp/operations.cpp +++ b/packages/react-native-nitro-sqlite/cpp/operations.cpp @@ -32,6 +32,10 @@ static constexpr double kInt64UpperBoundAsDouble = -kInt64MinAsDouble; std::map dbMap = std::map(); void sqliteOpenDb(const std::string& dbName, const std::string& docPath) { + if (dbMap.contains(dbName)) { + throw NitroSQLiteException::DatabaseAlreadyOpen(dbName); + } + #ifdef NITRO_SQLITE_VEC // Register before opening so the connection exposes vec0 + vec_*. margelo::rnnitrosqlitevec::registerVectorExtensions(); @@ -41,15 +45,20 @@ void sqliteOpenDb(const std::string& dbName, const std::string& docPath) { int sqlOpenFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX; - sqlite3* db; - int exit = 0; - exit = sqlite3_open_v2(dbPath.c_str(), &db, sqlOpenFlags, nullptr); + sqlite3* rawDatabase = nullptr; + const int openStatus = sqlite3_open_v2(dbPath.c_str(), &rawDatabase, sqlOpenFlags, nullptr); + std::unique_ptr database(rawDatabase, sqlite3_close_v2); + + if (openStatus != SQLITE_OK) { + const std::string errorMessage = rawDatabase == nullptr ? sqlite3_errstr(openStatus) : sqlite3_errmsg(rawDatabase); + throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, errorMessage); + } - if (exit != SQLITE_OK) { - throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, sqlite3_errmsg(db)); - } else { - dbMap[dbName] = db; + const bool inserted = dbMap.emplace(dbName, database.get()).second; + if (!inserted) { + throw NitroSQLiteException::DatabaseAlreadyOpen(dbName); } + database.release(); } void sqliteCloseDb(const std::string& dbName) {