Skip to content
Open
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,23 @@ nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1"

To put the database in an app group (e.g. for extensions), set `RNNitroSQLite_AppGroup` in your `Info.plist` to the app group ID and add the App Groups capability in Xcode.

## Database location (iOS)

By default, databases are stored in the app's **Documents** directory. If your app enables file sharing (`UIFileSharingEnabled` + `LSSupportsOpeningDocumentsInPlace`), that directory — including your raw database and its `-wal`/`-shm` journal files — becomes visible to users in the Files app, where they can be shared, modified, or deleted from outside your app.

To store databases in `Library/Application Support` instead (persistent, backed up, and never user-visible), set `RNNitroSQLite_DatabaseLocation` in your `Info.plist`:

```xml
<key>RNNitroSQLite_DatabaseLocation</key>
<string>ApplicationSupport</string>
```

Supported values are `Documents` (the default) and `ApplicationSupport`.

Databases created while the app was still using the Documents directory are automatically moved to `Library/Application Support` the first time they are opened after enabling this option, so existing users keep their data. If you later remove the option, databases already moved to `Library/Application Support` are **not** moved back.

This option has no effect when `RNNitroSQLite_AppGroup` is set, since app group databases live in the shared container.

---

# Exports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "macros.hpp"
#include "operations.hpp"
#include "sqliteExecuteBatch.hpp"
#include <filesystem>
#include <iostream>
#include <map>
#include <optional>
Expand Down Expand Up @@ -65,8 +66,76 @@ const std::string getDocPath(const std::optional<std::string>& location) {
return tempDocPath;
}

// Moves a database (together with its -wal/-shm journal files) out of the directory a previous
// app version stored it in. Committed-but-uncheckpointed writes live in the -wal file and SQLite
// only replays it when it sits next to its database, so the set must never be separated: the
// whole set is copied before any original is deleted, and if anything fails the intact originals
// stay in place (the caller then keeps opening the database there) and the migration retries on
// the next open.
static void migrateDatabase(const std::string& dbName, const std::filesystem::path& fromDirectory,
const std::filesystem::path& toDirectory) {
namespace fs = std::filesystem;
const std::string files[] = {dbName, dbName + "-wal", dbName + "-shm"};
std::error_code ec;

if (!fs::exists(fromDirectory / dbName, ec)) {
// Nothing to migrate. A previous run may have been interrupted after copying the set but
// before removing the journal files, so sweep any leftovers out of the old directory.
fs::remove(fromDirectory / (dbName + "-wal"), ec);
fs::remove(fromDirectory / (dbName + "-shm"), ec);
return;
}

// A database in the old directory means an older app version was writing there, so it is the
// live copy. Remove whatever sits at the destination (e.g. after a downgrade and re-upgrade)
// so a -wal from one database generation is never replayed into a database from another.
for (const auto& file : files) {
fs::remove(toDirectory / file, ec);
}

fs::create_directories(toDirectory, ec);
for (const auto& file : files) {
if (!fs::exists(fromDirectory / file, ec)) {
continue;
}

if (!fs::copy_file(fromDirectory / file, toDirectory / file, ec) || ec) {
LOGW("Failed to migrate database file %s: %s", file.c_str(), ec.message().c_str());
return;
}
}

// The database file is deleted first, and the journals only once that succeeds: if the
// database cannot be removed, the caller keeps opening it from the old directory, so its -wal
// must stay next to it or committed writes would be lost. An interruption after the first
// delete can only leave journal files behind, which the sweep above removes on the next open.
if (!fs::remove(fromDirectory / dbName, ec) || ec) {
LOGW("Failed to remove migrated database %s from its old location: %s", dbName.c_str(), ec.message().c_str());
return;
}
fs::remove(fromDirectory / (dbName + "-wal"), ec);
fs::remove(fromDirectory / (dbName + "-shm"), ec);
}

void HybridNitroSQLite::open(const std::string& dbName, const std::optional<std::string>& location) {
const auto docPath = getDocPath(location);
auto docPath = getDocPath(location);

if (!migrationDocPath.empty()) {
std::string oldDocPath = migrationDocPath;
if (location) {
oldDocPath = oldDocPath + "/" + *location;
}

migrateDatabase(dbName, oldDocPath, docPath);

// If the database could not be moved out of its old directory, keep opening it there rather
// than creating a fresh empty one; the migration retries on the next open.
std::error_code ec;
if (std::filesystem::exists(std::filesystem::path(oldDocPath) / dbName, ec)) {
docPath = oldDocPath;
}
}

sqliteOpenDb(dbName, docPath);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class HybridNitroSQLite : public HybridNitroSQLiteSpec {

public:
static std::string docPath;
// Directory databases were stored in by previous app versions, when the platform layer has
// relocated docPath (e.g. iOS with RNNitroSQLite_DatabaseLocation set to "ApplicationSupport").
// When non-empty, each database found there is moved to docPath as it is opened.
static std::string migrationDocPath;

public:
// Methods
Expand Down Expand Up @@ -43,5 +47,6 @@ class HybridNitroSQLite : public HybridNitroSQLiteSpec {
};

inline std::string HybridNitroSQLite::docPath = "";
inline std::string HybridNitroSQLite::migrationDocPath = "";

} // namespace margelo::nitro::rnnitrosqlite
29 changes: 26 additions & 3 deletions packages/react-native-nitro-sqlite/ios/OnLoad.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,32 @@ + (void)load {

documentPath = [storeUrl path];
} else {
// Get iOS app's document directory (to safely store database .sqlite3 file)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true);
documentPath = [paths objectAtIndex:0];
NSString *databaseLocation = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RNNitroSQLite_DatabaseLocation"];

if ([databaseLocation isEqualToString:@"ApplicationSupport"]) {
// Library/Application Support is persistent, backed up, and never exposed to the user
// via the Files app (unlike the Documents directory, which becomes user-visible when
// the app enables file sharing).
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true);
documentPath = [paths objectAtIndex:0];

NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:documentPath]) {
[fileManager createDirectoryAtPath:documentPath withIntermediateDirectories:YES attributes:nil error:nil];
}

// Databases created before this option was enabled still live in Documents; each one is
// moved over when it is opened (see HybridNitroSQLite::open).
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) objectAtIndex:0];
HybridNitroSQLite::migrationDocPath = [documentsDirectory UTF8String];
} else {
if (databaseLocation != nil && ![databaseLocation isEqualToString:@"Documents"]) {
NSLog(@"Invalid RNNitroSQLite_DatabaseLocation value provided (%@). Supported values are \"Documents\" and \"ApplicationSupport\". Falling back to \"Documents\".", databaseLocation);
}
// Get iOS app's document directory (to safely store database .sqlite3 file)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true);
documentPath = [paths objectAtIndex:0];
}
}

HybridNitroSQLite::docPath = [documentPath UTF8String];
Expand Down