Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/docs/guides/batch-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const { rowsAffected } = await db.executeBatchAsync(commands)

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.

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`.

```ts
await db.executeBatchAsync([
{
Expand Down
10 changes: 9 additions & 1 deletion docs/content/docs/guides/database-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Open, close, delete, and place database files in an app directory.

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.

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).
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).

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

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.

## Runtime teardown

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.

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.

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.

## File location and prepopulated databases

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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ namespace {

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

// File migration and deletion must account for handles owned by other runtimes.
// Weak references observe those handles without extending their lifetime.
struct ProcessConnections {
std::recursive_mutex lifecycleMutex;
std::vector<std::weak_ptr<SQLiteConnection>> connections;
unsigned long long nextConnectionId = 0;
};

ProcessConnections& processConnections() {
static ProcessConnections state;
return state;
}

int readOnlyAuthorizer(void*, int action, const char*, const char*, const char*, const char*) {
return action == SQLITE_ATTACH ? SQLITE_DENY : SQLITE_OK;
}
Expand Down Expand Up @@ -152,6 +165,12 @@ void SQLiteConnection::close() noexcept {
database = nullptr;
}

DatabaseConnections::DatabaseConnections() : lifecycleMutex(processConnections().lifecycleMutex) {}

DatabaseConnections::~DatabaseConnections() {
closeAll();
}

void DatabaseConnections::open(const std::string& key, const fs::path& path, bool readOnly,
const std::optional<std::string>& encryptionKey) {
std::lock_guard lock(lifecycleMutex);
Expand All @@ -173,8 +192,8 @@ std::string DatabaseConnections::openIndependent(const fs::path& path, bool read
}
// NUL cannot occur in a filesystem name. The ID cannot collide with a legacy database key.
const auto physicalPath = canonicalDatabasePath(path);
const std::string key = std::string(1, '\0') + kIndependentPrefix + std::to_string(++nextConnectionId) + std::string(1, '\0') +
(readOnly ? "r" : "w") + physicalPath.string();
const std::string key = std::string(1, '\0') + kIndependentPrefix + std::to_string(++processConnections().nextConnectionId) +
std::string(1, '\0') + (readOnly ? "r" : "w") + physicalPath.string();
openKey(key, path, readOnly, encryptionKey);
return key;
}
Expand All @@ -201,6 +220,9 @@ void DatabaseConnections::openKey(const std::string& key, const fs::path& path,
}
auto connection = std::make_shared<SQLiteConnection>(connectionLabel(key), physicalPath, readOnly, database.get());
database.release();
auto& liveConnections = processConnections().connections;
std::erase_if(liveConnections, [](const auto& weak) { return weak.expired(); });
liveConnections.emplace_back(connection);
connections.emplace(key, std::move(connection));
}

Expand Down Expand Up @@ -250,8 +272,9 @@ std::optional<fs::path> DatabaseConnections::physicalPathForKey(const std::strin
std::optional<fs::path> DatabaseConnections::findLivePath(const fs::path& first, const fs::path& second) {
std::optional<fs::path> found;
withConnectionsLocked([&]() {
for (const auto& [_, connection] : connections) {
if (connection->database == nullptr) {
for (const auto& weak : processConnections().connections) {
const auto connection = weak.lock();
if (!connection || connection->database == nullptr) {
continue;
}
anyDatabasePath(connection->database, [&](const fs::path& candidate) {
Expand All @@ -275,9 +298,15 @@ std::optional<fs::path> DatabaseConnections::findLivePath(const fs::path& first,

void DatabaseConnections::withConnectionsLocked(const std::function<void()>& action) {
std::lock_guard lifecycleLock(lifecycleMutex);
std::vector<SQLiteConnectionPtr> liveConnections;
std::vector<std::unique_lock<std::recursive_mutex>> locks;
locks.reserve(connections.size());
for (const auto& [_, connection] : connections) {
for (const auto& weak : processConnections().connections) {
if (auto connection = weak.lock()) {
liveConnections.push_back(std::move(connection));
}
}
locks.reserve(liveConnections.size());
for (const auto& connection : liveConnections) {
locks.emplace_back(connection->mutex);
}
action();
Expand Down Expand Up @@ -313,7 +342,7 @@ void DatabaseConnections::drop(const std::string& dbName, const fs::path& path,

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

bool DatabaseConnections::isPathInUse(const fs::path& path, const std::string& excludedKey) const {
for (const auto& [key, connection] : connections) {
if (key == excludedKey) {
bool DatabaseConnections::isPathInUse(const fs::path& path, const SQLiteConnectionPtr& excludedConnection) const {
for (const auto& weak : processConnections().connections) {
const auto connection = weak.lock();
if (!connection || connection == excludedConnection) {
continue;
}
if (connection->database == nullptr) {
Expand All @@ -346,11 +376,6 @@ bool DatabaseConnections::isPathInUse(const fs::path& path, const std::string& e
return false;
}

DatabaseConnections& databaseConnections() {
static DatabaseConnections registry;
return registry;
}

void validateDatabaseName(const std::string& dbName) {
if (dbName.find('\0') != std::string::npos) {
throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, "Database name contains a NUL byte");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ struct SQLiteConnection final : std::enable_shared_from_this<SQLiteConnection> {
/** Shared ownership of a native connection across pending operations. */
using SQLiteConnectionPtr = std::shared_ptr<SQLiteConnection>;

/** Registry of default database names and opaque independent connection IDs. */
/** Connections owned by one NitroSQLite root. Destruction closes only this owner's handles. */
class DatabaseConnections final {
public:
DatabaseConnections();
~DatabaseConnections();

DatabaseConnections(const DatabaseConnections&) = delete;
DatabaseConnections& operator=(const DatabaseConnections&) = delete;

// Callers hold this while resolving or migrating a database path. It is recursive because
// open, attach and drop take it again after path resolution.
std::recursive_mutex lifecycleMutex;
// open, attach and drop take it again after path resolution. Shared across roots to protect files.
std::recursive_mutex& lifecycleMutex;

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

private:
void openKey(const std::string& key, const std::filesystem::path& path, bool readOnly, const std::optional<std::string>& encryptionKey);
bool isPathInUse(const std::filesystem::path& path, const std::string& excludedKey) const;
bool isPathInUse(const std::filesystem::path& path, const SQLiteConnectionPtr& excludedConnection) const;

std::map<std::string, SQLiteConnectionPtr> connections;
unsigned long long nextConnectionId = 0;
};

DatabaseConnections& databaseConnections();
std::filesystem::path canonicalDatabasePath(const std::filesystem::path& path);
void validateDatabaseName(const std::string& dbName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
if (std::holds_alternative<NestedParamsVec>(*command.params)) {
groupedCommand.parameterSets = std::get<NestedParamsVec>(*command.params);
} else {
groupedCommand.parameterSets.push_back(std::get<ParamsVec>(*command.params));
const auto& params = std::get<ParamsVec>(*command.params);
// An empty JavaScript array matches the flat variant first. It still
// represents an empty group, so only omitted params execute once.
if (!params.empty()) {
groupedCommand.parameterSets.push_back(params);
}
}
} else {
groupedCommand.parameterSets.emplace_back();
Expand All @@ -34,10 +39,6 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
return commands;
}

SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands) {
return sqliteExecuteBatch(sqliteGetOpenDatabase(dbName), commands);
}

SQLiteOperationResult sqliteExecuteBatch(const SQLiteConnectionPtr& connection, const std::vector<BatchQuery>& commands) {
std::lock_guard lock(connection->mutex);
if (commands.empty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
/**
* Execute a batch of commands in a exclusive transaction
*/
SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands);
SQLiteOperationResult sqliteExecuteBatch(const std::shared_ptr<SQLiteConnection>& connection, const std::vector<BatchQuery>& commands);

} // namespace margelo::nitro::rnnitrosqlite
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@

namespace margelo::nitro::rnnitrosqlite {

SQLiteOperationResult importSqlFile(const std::string& dbName, const std::string& fileLocation) {
return importSqlFile(sqliteGetOpenDatabase(dbName), fileLocation);
}

SQLiteOperationResult importSqlFile(const SQLiteConnectionPtr& connection, const std::string& fileLocation) {
std::lock_guard lock(connection->mutex);
std::string line;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ namespace margelo::nitro::rnnitrosqlite {

struct SQLiteConnection;

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

} // namespace margelo::nitro::rnnitrosqlite
Loading
Loading