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
54 changes: 54 additions & 0 deletions .github/workflows/test-cpp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Test C++

on:
push:
branches:
- main
paths:
- ".github/workflows/test-cpp.yml"
- "packages/react-native-nitro-sqlite/cpp/databaseMigration.*"
- "packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp"
- "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*"
- "packages/react-native-nitro-sqlite/tests/cpp/**"
pull_request:
paths:
- ".github/workflows/test-cpp.yml"
- "packages/react-native-nitro-sqlite/cpp/databaseMigration.*"
- "packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp"
- "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*"
- "packages/react-native-nitro-sqlite/tests/cpp/**"

jobs:
test:
name: Database migration tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7

- name: Build bundled SQLite
run: |
clang \
-std=c11 \
-DSQLITE_THREADSAFE=2 \
-c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \
-o /tmp/sqlite3.o

- name: Build migration tests
run: |
clang++ \
-std=c++20 \
-Wall \
-Wextra \
-Werror \
-Ipackages/react-native-nitro-sqlite/cpp \
-Ipackages/react-native-nitro-sqlite/cpp/sqlite \
packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp \
packages/react-native-nitro-sqlite/tests/cpp/databaseMigration.test.cpp \
/tmp/sqlite3.o \
-ldl \
-lm \
-pthread \
-o /tmp/databaseMigrationTests

- name: Run migration tests
run: /tmp/databaseMigrationTests
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,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 or attached after enabling this option, so existing users keep their data. Deleting a database also removes any copy left in Documents by an interrupted migration. 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
123 changes: 123 additions & 0 deletions packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "databaseMigration.hpp"
#include "logs.hpp"
#include <array>
#include <system_error>

namespace margelo::nitro::rnnitrosqlite {

namespace fs = std::filesystem;

namespace {

constexpr std::size_t kDatabaseFileCount = 4;
using DatabaseFiles = std::array<std::string, kDatabaseFileCount>;

DatabaseFiles getDatabaseFiles(const std::string& dbName);
bool copyDatabaseFiles(const DatabaseFiles& files, const fs::path& fromDirectory, const fs::path& toDirectory);
void removeAuxiliaryDatabaseFiles(const DatabaseFiles& files, const fs::path& directory);

} // namespace

fs::path migrateDatabase(const std::string& dbName, const fs::path& fromDirectory, const fs::path& toDirectory) {
const auto files = getDatabaseFiles(dbName);
std::error_code ec;
const bool sourceExists = fs::exists(fromDirectory / dbName, ec);

if (ec) {
LOGW("Failed to inspect database %s in its old location: %s", dbName.c_str(), ec.message().c_str());
return fromDirectory;
}

if (!sourceExists) {
// A completed migration may have been interrupted after deleting the database but before
// deleting its journals. The destination is already authoritative in that state.
removeAuxiliaryDatabaseFiles(files, fromDirectory);
return toDirectory;
}

// A database in the old directory is the live copy. Clear every database generation file at
// the destination before copying so SQLite never pairs the source with a stale journal.
if (!removeDatabaseFiles(dbName, toDirectory)) {
return fromDirectory;
}

fs::create_directories(toDirectory, ec);
if (ec) {
LOGW("Failed to create database migration directory %s: %s", toDirectory.string().c_str(), ec.message().c_str());
return fromDirectory;
}

if (!copyDatabaseFiles(files, fromDirectory, toDirectory)) {
return fromDirectory;
}

// Delete the database first. If this fails, every source journal must remain beside it so the
// caller can safely keep using the old location. Leftover journals after a successful database
// deletion are harmless and are removed on the next migration attempt.
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 fromDirectory;
}

removeAuxiliaryDatabaseFiles(files, fromDirectory);
return toDirectory;
}

bool removeDatabaseFiles(const std::string& dbName, const fs::path& directory) {
const auto files = getDatabaseFiles(dbName);

for (const auto& file : files) {
std::error_code ec;
fs::remove(directory / file, ec);
if (ec) {
LOGW("Failed to remove database file %s: %s", file.c_str(), ec.message().c_str());
return false;
}
}

return true;
}

namespace {

DatabaseFiles getDatabaseFiles(const std::string& dbName) {
return {dbName, dbName + "-journal", dbName + "-wal", dbName + "-shm"};
}

bool copyDatabaseFiles(const DatabaseFiles& files, const fs::path& fromDirectory, const fs::path& toDirectory) {
for (const auto& file : files) {
std::error_code ec;
const bool sourceExists = fs::exists(fromDirectory / file, ec);

if (ec) {
LOGW("Failed to inspect database file %s: %s", file.c_str(), ec.message().c_str());
return false;
}

if (!sourceExists) {
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 false;
}
}

return true;
}

void removeAuxiliaryDatabaseFiles(const DatabaseFiles& files, const fs::path& directory) {
for (std::size_t index = 1; index < files.size(); index++) {
const auto& file = files[index];
std::error_code ec;
fs::remove(directory / file, ec);
if (ec) {
LOGW("Failed to remove database file %s: %s", file.c_str(), ec.message().c_str());
}
}
}

} // namespace

} // namespace margelo::nitro::rnnitrosqlite
13 changes: 13 additions & 0 deletions packages/react-native-nitro-sqlite/cpp/databaseMigration.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

#include <filesystem>
#include <string>

namespace margelo::nitro::rnnitrosqlite {

std::filesystem::path migrateDatabase(const std::string& dbName, const std::filesystem::path& fromDirectory,
const std::filesystem::path& toDirectory);

bool removeDatabaseFiles(const std::string& dbName, const std::filesystem::path& directory);

} // namespace margelo::nitro::rnnitrosqlite
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#include "HybridNitroSQLite.hpp"
#include "HybridNitroSQLiteQueryResult.hpp"
#include "NitroSQLiteException.hpp"
#include "databaseMigration.hpp"
#include "importSqlFile.hpp"
#include "logs.hpp"
#include "macros.hpp"
#include "operations.hpp"
#include "sqliteExecuteBatch.hpp"
#include <exception>
#include <filesystem>
#include <iostream>
#include <map>
#include <optional>
Expand Down Expand Up @@ -66,8 +68,26 @@ const std::string getDocPath(const std::optional<std::string>& location) {
return tempDocPath;
}

const std::string getOldDocPath(const std::optional<std::string>& location) {
std::string oldDocPath = HybridNitroSQLite::migrationDocPath;
if (location) {
oldDocPath = oldDocPath + "/" + *location;
}

return oldDocPath;
}

const std::string getMigratedDocPath(const std::string& dbName, const std::optional<std::string>& location) {
const auto currentDocPath = getDocPath(location);
if (HybridNitroSQLite::migrationDocPath.empty()) {
return currentDocPath;
}

return migrateDatabase(dbName, getOldDocPath(location), currentDocPath).string();
}

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

Expand All @@ -76,18 +96,28 @@ void HybridNitroSQLite::close(const std::string& dbName) {
};

void HybridNitroSQLite::drop(const std::string& dbName, const std::optional<std::string>& location) {
const auto docPath = getDocPath(location);
sqliteRemoveDb(dbName, docPath);
const auto currentDocPath = getDocPath(location);
if (migrationDocPath.empty()) {
sqliteRemoveDb(dbName, currentDocPath);
return;
}

const auto oldDocPath = getOldDocPath(location);
std::error_code ec;
const bool oldDatabaseExists = std::filesystem::exists(std::filesystem::path(oldDocPath) / dbName, ec);
if (ec) {
LOGW("Failed to inspect database %s in its old location: %s", dbName.c_str(), ec.message().c_str());
}

sqliteRemoveDb(dbName, oldDatabaseExists || ec ? oldDocPath : currentDocPath);
removeDatabaseFiles(dbName, oldDocPath);
removeDatabaseFiles(dbName, currentDocPath);
};

void HybridNitroSQLite::attach(const std::string& mainDbName, const std::string& dbNameToAttach, const std::string& alias,
const std::optional<std::string>& location) {
std::string tempDocPath = std::string(docPath);
if (location) {
tempDocPath = tempDocPath + "/" + *location;
}

sqliteAttachDb(mainDbName, tempDocPath, dbNameToAttach, alias);
const auto attachedDocPath = getMigratedDocPath(dbNameToAttach, location);
sqliteAttachDb(mainDbName, attachedDocPath, dbNameToAttach, alias);
};

void HybridNitroSQLite::detach(const std::string& mainDbName, const std::string& alias) {
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, databases found there are resolved as they are opened, attached, or dropped.
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
Loading
Loading