Skip to content

Commit 6dd1049

Browse files
authored
fix: scope database connections to their NitroSQLite owner (#421)
* fix: scope database connections to their NitroSQLite owner * fix: skip empty batch parameter groups
1 parent fb11218 commit 6dd1049

14 files changed

Lines changed: 187 additions & 127 deletions

‎docs/content/docs/guides/batch-operations.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const { rowsAffected } = await db.executeBatchAsync(commands)
3030

3131
When the same SQL runs with different values, give one command an array of parameter arrays. The native batch code expands it into separate executions before starting the transaction.
3232

33+
An empty `params` array skips that command. If every command has an empty array, the batch throws or rejects because it has nothing to execute. Omit `params` for a statement that should run once without bindings, such as `CREATE TABLE`.
34+
3335
```ts
3436
await db.executeBatchAsync([
3537
{

‎docs/content/docs/guides/database-lifecycle.mdx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Open, close, delete, and place database files in an app directory.
55

66
A SQLite database usually lives in a file. Your app opens a connection to read or change that file, then closes the connection when it is done. Closing a connection leaves the data on disk; deleting the file removes the database.
77

8-
In NitroSQLite, `open({ name })` opens an existing SQLite file or creates a new one. A database name can have only one active default connection. Close it before opening another default connection with that name, or use `connection: 'independent'` for a separate handle to the same file. See [multiple connections](/docs/guides/multiple-connections).
8+
In NitroSQLite, `open({ name })` opens an existing SQLite file or creates a new one. A database name can have only one active default connection per native NitroSQLite root. Close it before opening another default connection with that name, or use `connection: 'independent'` for a separate handle to the same file. See [multiple connections](/docs/guides/multiple-connections).
99

1010
```ts
1111
import { open } from 'react-native-nitro-sqlite'
@@ -31,6 +31,14 @@ Both methods are synchronous. They fail if an operation on that connection is qu
3131

3232
On iOS, when a database is being moved from Documents to Application Support, deletion also cleans up copies and SQLite sidecar files from both locations.
3333

34+
## Runtime teardown
35+
36+
SQLite connections are native resources. A JavaScript runtime can be destroyed while the app process stays alive, such as when an app switches between React Native hosts. Closing a connection preserves committed data on disk and rolls back any unfinished transaction.
37+
38+
Each native NitroSQLite root owns its default and independent connections. When that root is destroyed, it closes its handles. A replacement runtime can open the same database names without restarting the process. An older root's cleanup does not close connections opened by a newer root.
39+
40+
Queued operations and prepared statements keep their original connection. After its owner closes it, those operations fail instead of using a replacement connection with the same name. Await pending work and finalize prepared statements before an intentional runtime teardown when your app controls that transition.
41+
3442
## File location and prepopulated databases
3543

3644
By default, the root is the app Documents directory on iOS, the app files directory on Android, and an app-specific Application Support directory on macOS. Use the optional `location` as a relative subdirectory under that root, not as an absolute file path:

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp‎

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ namespace {
1818

1919
constexpr const char* kIndependentPrefix = "nitro-sqlite:";
2020

21+
// File migration and deletion must account for handles owned by other runtimes.
22+
// Weak references observe those handles without extending their lifetime.
23+
struct ProcessConnections {
24+
std::recursive_mutex lifecycleMutex;
25+
std::vector<std::weak_ptr<SQLiteConnection>> connections;
26+
unsigned long long nextConnectionId = 0;
27+
};
28+
29+
ProcessConnections& processConnections() {
30+
static ProcessConnections state;
31+
return state;
32+
}
33+
2134
int readOnlyAuthorizer(void*, int action, const char*, const char*, const char*, const char*) {
2235
return action == SQLITE_ATTACH ? SQLITE_DENY : SQLITE_OK;
2336
}
@@ -152,6 +165,12 @@ void SQLiteConnection::close() noexcept {
152165
database = nullptr;
153166
}
154167

168+
DatabaseConnections::DatabaseConnections() : lifecycleMutex(processConnections().lifecycleMutex) {}
169+
170+
DatabaseConnections::~DatabaseConnections() {
171+
closeAll();
172+
}
173+
155174
void DatabaseConnections::open(const std::string& key, const fs::path& path, bool readOnly,
156175
const std::optional<std::string>& encryptionKey) {
157176
std::lock_guard lock(lifecycleMutex);
@@ -173,8 +192,8 @@ std::string DatabaseConnections::openIndependent(const fs::path& path, bool read
173192
}
174193
// NUL cannot occur in a filesystem name. The ID cannot collide with a legacy database key.
175194
const auto physicalPath = canonicalDatabasePath(path);
176-
const std::string key = std::string(1, '\0') + kIndependentPrefix + std::to_string(++nextConnectionId) + std::string(1, '\0') +
177-
(readOnly ? "r" : "w") + physicalPath.string();
195+
const std::string key = std::string(1, '\0') + kIndependentPrefix + std::to_string(++processConnections().nextConnectionId) +
196+
std::string(1, '\0') + (readOnly ? "r" : "w") + physicalPath.string();
178197
openKey(key, path, readOnly, encryptionKey);
179198
return key;
180199
}
@@ -201,6 +220,9 @@ void DatabaseConnections::openKey(const std::string& key, const fs::path& path,
201220
}
202221
auto connection = std::make_shared<SQLiteConnection>(connectionLabel(key), physicalPath, readOnly, database.get());
203222
database.release();
223+
auto& liveConnections = processConnections().connections;
224+
std::erase_if(liveConnections, [](const auto& weak) { return weak.expired(); });
225+
liveConnections.emplace_back(connection);
204226
connections.emplace(key, std::move(connection));
205227
}
206228

@@ -250,8 +272,9 @@ std::optional<fs::path> DatabaseConnections::physicalPathForKey(const std::strin
250272
std::optional<fs::path> DatabaseConnections::findLivePath(const fs::path& first, const fs::path& second) {
251273
std::optional<fs::path> found;
252274
withConnectionsLocked([&]() {
253-
for (const auto& [_, connection] : connections) {
254-
if (connection->database == nullptr) {
275+
for (const auto& weak : processConnections().connections) {
276+
const auto connection = weak.lock();
277+
if (!connection || connection->database == nullptr) {
255278
continue;
256279
}
257280
anyDatabasePath(connection->database, [&](const fs::path& candidate) {
@@ -275,9 +298,15 @@ std::optional<fs::path> DatabaseConnections::findLivePath(const fs::path& first,
275298

276299
void DatabaseConnections::withConnectionsLocked(const std::function<void()>& action) {
277300
std::lock_guard lifecycleLock(lifecycleMutex);
301+
std::vector<SQLiteConnectionPtr> liveConnections;
278302
std::vector<std::unique_lock<std::recursive_mutex>> locks;
279-
locks.reserve(connections.size());
280-
for (const auto& [_, connection] : connections) {
303+
for (const auto& weak : processConnections().connections) {
304+
if (auto connection = weak.lock()) {
305+
liveConnections.push_back(std::move(connection));
306+
}
307+
}
308+
locks.reserve(liveConnections.size());
309+
for (const auto& connection : liveConnections) {
281310
locks.emplace_back(connection->mutex);
282311
}
283312
action();
@@ -313,7 +342,7 @@ void DatabaseConnections::drop(const std::string& dbName, const fs::path& path,
313342

314343
const SQLiteConnectionPtr connectionToClose = live == connections.end() ? nullptr : live->second;
315344
withConnectionsLocked([&]() {
316-
if (isPathInUse(target, key) || (otherPath && isPathInUse(*otherPath, key))) {
345+
if (isPathInUse(target, connectionToClose) || (otherPath && isPathInUse(*otherPath, connectionToClose))) {
317346
throw NitroSQLiteException(NitroSQLiteExceptionType::SqlExecutionError, "Database is in use by another connection");
318347
}
319348
if (!fs::exists(target)) {
@@ -331,9 +360,10 @@ void DatabaseConnections::drop(const std::string& dbName, const fs::path& path,
331360
});
332361
}
333362

334-
bool DatabaseConnections::isPathInUse(const fs::path& path, const std::string& excludedKey) const {
335-
for (const auto& [key, connection] : connections) {
336-
if (key == excludedKey) {
363+
bool DatabaseConnections::isPathInUse(const fs::path& path, const SQLiteConnectionPtr& excludedConnection) const {
364+
for (const auto& weak : processConnections().connections) {
365+
const auto connection = weak.lock();
366+
if (!connection || connection == excludedConnection) {
337367
continue;
338368
}
339369
if (connection->database == nullptr) {
@@ -346,11 +376,6 @@ bool DatabaseConnections::isPathInUse(const fs::path& path, const std::string& e
346376
return false;
347377
}
348378

349-
DatabaseConnections& databaseConnections() {
350-
static DatabaseConnections registry;
351-
return registry;
352-
}
353-
354379
void validateDatabaseName(const std::string& dbName) {
355380
if (dbName.find('\0') != std::string::npos) {
356381
throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, "Database name contains a NUL byte");

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.hpp‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,18 @@ struct SQLiteConnection final : std::enable_shared_from_this<SQLiteConnection> {
4545
/** Shared ownership of a native connection across pending operations. */
4646
using SQLiteConnectionPtr = std::shared_ptr<SQLiteConnection>;
4747

48-
/** Registry of default database names and opaque independent connection IDs. */
48+
/** Connections owned by one NitroSQLite root. Destruction closes only this owner's handles. */
4949
class DatabaseConnections final {
5050
public:
51+
DatabaseConnections();
52+
~DatabaseConnections();
53+
54+
DatabaseConnections(const DatabaseConnections&) = delete;
55+
DatabaseConnections& operator=(const DatabaseConnections&) = delete;
56+
5157
// Callers hold this while resolving or migrating a database path. It is recursive because
52-
// open, attach and drop take it again after path resolution.
53-
std::recursive_mutex lifecycleMutex;
58+
// open, attach and drop take it again after path resolution. Shared across roots to protect files.
59+
std::recursive_mutex& lifecycleMutex;
5460

5561
/** Open a name-based default connection. An existing key is an error. */
5662
void open(const std::string& key, const std::filesystem::path& path, bool readOnly,
@@ -68,23 +74,21 @@ class DatabaseConnections final {
6874
bool isOpen(const std::string& key);
6975
/** Resolve the file path for a registered or encoded independent key. */
7076
std::optional<std::filesystem::path> physicalPathForKey(const std::string& key);
71-
/** Find an open connection using either candidate path. */
77+
/** Find an open connection in any owner using either candidate path. */
7278
std::optional<std::filesystem::path> findLivePath(const std::filesystem::path& first, const std::filesystem::path& second);
73-
/** Run @p action while holding the lifecycle and every connection lock. */
79+
/** Run an internal file operation while holding the lifecycle and all owners' connection locks. */
7480
void withConnectionsLocked(const std::function<void()>& action);
7581
/** Delete a database after checking that no other connection or attachment uses it. */
7682
void drop(const std::string& dbName, const std::filesystem::path& path, const std::optional<std::string>& connectionId,
7783
const std::optional<std::filesystem::path>& otherPath = std::nullopt);
7884

7985
private:
8086
void openKey(const std::string& key, const std::filesystem::path& path, bool readOnly, const std::optional<std::string>& encryptionKey);
81-
bool isPathInUse(const std::filesystem::path& path, const std::string& excludedKey) const;
87+
bool isPathInUse(const std::filesystem::path& path, const SQLiteConnectionPtr& excludedConnection) const;
8288

8389
std::map<std::string, SQLiteConnectionPtr> connections;
84-
unsigned long long nextConnectionId = 0;
8590
};
8691

87-
DatabaseConnections& databaseConnections();
8892
std::filesystem::path canonicalDatabasePath(const std::filesystem::path& path);
8993
void validateDatabaseName(const std::string& dbName);
9094

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteExecuteBatch.cpp‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
2121
if (std::holds_alternative<NestedParamsVec>(*command.params)) {
2222
groupedCommand.parameterSets = std::get<NestedParamsVec>(*command.params);
2323
} else {
24-
groupedCommand.parameterSets.push_back(std::get<ParamsVec>(*command.params));
24+
const auto& params = std::get<ParamsVec>(*command.params);
25+
// An empty JavaScript array matches the flat variant first. It still
26+
// represents an empty group, so only omitted params execute once.
27+
if (!params.empty()) {
28+
groupedCommand.parameterSets.push_back(params);
29+
}
2530
}
2631
} else {
2732
groupedCommand.parameterSets.emplace_back();
@@ -34,10 +39,6 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
3439
return commands;
3540
}
3641

37-
SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands) {
38-
return sqliteExecuteBatch(sqliteGetOpenDatabase(dbName), commands);
39-
}
40-
4142
SQLiteOperationResult sqliteExecuteBatch(const SQLiteConnectionPtr& connection, const std::vector<BatchQuery>& commands) {
4243
std::lock_guard lock(connection->mutex);
4344
if (commands.empty()) {

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteExecuteBatch.hpp‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
2525
/**
2626
* Execute a batch of commands in a exclusive transaction
2727
*/
28-
SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands);
2928
SQLiteOperationResult sqliteExecuteBatch(const std::shared_ptr<SQLiteConnection>& connection, const std::vector<BatchQuery>& commands);
3029

3130
} // namespace margelo::nitro::rnnitrosqlite

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteImportSqlFile.cpp‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010

1111
namespace margelo::nitro::rnnitrosqlite {
1212

13-
SQLiteOperationResult importSqlFile(const std::string& dbName, const std::string& fileLocation) {
14-
return importSqlFile(sqliteGetOpenDatabase(dbName), fileLocation);
15-
}
16-
1713
SQLiteOperationResult importSqlFile(const SQLiteConnectionPtr& connection, const std::string& fileLocation) {
1814
std::lock_guard lock(connection->mutex);
1915
std::string line;

‎packages/react-native-nitro-sqlite/cpp/NitroSQLiteImportSqlFile.hpp‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ namespace margelo::nitro::rnnitrosqlite {
1313

1414
struct SQLiteConnection;
1515

16-
SQLiteOperationResult importSqlFile(const std::string& dbName, const std::string& fileLocation);
1716
SQLiteOperationResult importSqlFile(const std::shared_ptr<SQLiteConnection>& connection, const std::string& fileLocation);
1817

1918
} // namespace margelo::nitro::rnnitrosqlite

0 commit comments

Comments
 (0)