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
56 changes: 31 additions & 25 deletions cpp/DBHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "bridge.h"
#endif
#include "logs.h"
#include <functional>
#include "macros.hpp"
#include "utils.hpp"
#include <iostream>
Expand All @@ -23,6 +24,19 @@ void DBHostObject::flush_pending_reactive_queries(
resolve->asObject(rt).asFunction(rt).call(rt, {});
});
}
#elif defined(OP_SQLITE_USE_TURSO)

std::string turso_remote_db_name(const std::string &url) {
return "turso_remote_" + std::to_string(std::hash<std::string>{}(url)) +
".sqlite";
}

void DBHostObject::flush_pending_reactive_queries(
const std::shared_ptr<jsi::Value> &resolve) {
invoker->invokeAsync([resolve](jsi::Runtime &rt) {
resolve->asObject(rt).asFunction(rt).call(rt, {});
});
}
#else
void DBHostObject::flush_pending_reactive_queries(
const std::shared_ptr<jsi::Value> &resolve) {
Expand Down Expand Up @@ -161,7 +175,7 @@ DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &db_name,
std::string &auth_token, int sync_interval,
bool offline, std::string &encryption_key,
std::string &remote_encryption_key)
: db_name(db_name) {
: base_path(path), db_name(db_name), delete_db_name(db_name) {

thread_pool = std::make_shared<ThreadPool>();

Expand All @@ -176,7 +190,8 @@ DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &db_name,
// Remote connection constructor
DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &url,
std::string &auth_token, std::string &base_path)
: db_name(url) {
: base_path(base_path), db_name(url),
delete_db_name(turso_remote_db_name(url)) {
thread_pool = std::make_shared<ThreadPool>();
db = opsqlite_open_remote(url, auth_token, base_path);

Expand All @@ -188,7 +203,7 @@ DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &db_name,
std::string &path, std::string &url,
std::string &auth_token,
std::string &remote_encryption_key)
: db_name(db_name) {
: base_path(path), db_name(db_name), delete_db_name(db_name) {

thread_pool = std::make_shared<ThreadPool>();

Expand All @@ -205,7 +220,7 @@ DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &base_path,
std::string &crsqlite_path,
std::string &sqlite_vec_path,
std::string &encryption_key)
: base_path(base_path), db_name(db_name) {
: base_path(base_path), db_name(db_name), delete_db_name(db_name) {
thread_pool = std::make_shared<ThreadPool>();

#ifdef OP_SQLITE_USE_SQLCIPHER
Expand Down Expand Up @@ -278,33 +293,24 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) {
});

function_map["delete"] = HFN(this) {
invalidated = true;

std::string path = std::string(base_path);

if (count == 1) {
if (!args[1].isString()) {
throw std::runtime_error(
"[op-sqlite][open] database location must be a string");
}
if (count != 0) {
throw std::runtime_error("[op-sqlite] Delete no longer takes arguments");
}

std::string location = args[1].asString(rt).utf8(rt);
invalidated = true;
// Drain any in-flight async queries before closing/removing the db handle.
// Without this, queued/running work may dereference a freed sqlite handle.
thread_pool->waitFinished();

if (!location.empty()) {
if (location == ":memory:") {
path = ":memory:";
} else if (location.rfind('/', 0) == 0) {
path = location;
} else {
path = path + "/" + location;
}
}
if (delete_db_name.empty()) {
throw std::runtime_error(
"[op-sqlite][delete] delete() is not supported for remote-only databases");
}

#ifdef OP_SQLITE_USE_LIBSQL
opsqlite_libsql_remove(db, db_name, path);
opsqlite_libsql_remove(db, delete_db_name, base_path);
#else
opsqlite_remove(db, db_name, path);
opsqlite_remove(db, delete_db_name, base_path);
#endif
Comment thread
ospfranco marked this conversation as resolved.
Comment thread
ospfranco marked this conversation as resolved.

return {};
Expand Down
1 change: 1 addition & 0 deletions cpp/DBHostObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class JSI_EXPORT DBHostObject : public jsi::HostObject {
std::string base_path;
std::shared_ptr<ThreadPool> thread_pool;
std::string db_name;
std::string delete_db_name;
std::shared_ptr<jsi::Value> update_hook_callback;
std::shared_ptr<jsi::Value> commit_hook_callback;
std::shared_ptr<jsi::Value> rollback_hook_callback;
Expand Down
27 changes: 27 additions & 0 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ const syncDb = openSync({
syncDb.sync();
```

## Close

Closes the current database connection. This is mainly useful before tearing down the runtime, replacing the file on disk, or deleting the database.

```tsx
const db = open({
name: 'myDb.sqlite',
});

db.close();
```

On web, use `await db.closeAsync()` instead.

## Delete

Deletes the database file represented by the current connection.

```tsx
const db = open({
name: 'myDb.sqlite',
});

db.close();
db.delete();
```

## Execute

Base async query operation. All execute calls run on a (**single**) separate and dedicated thread, so the JS thread is not blocked. It’s recommended to ALWAYS use transactions since even read calls can corrupt a sqlite database.
Expand Down
140 changes: 70 additions & 70 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -1,71 +1,71 @@
{
"name": "op_sqlite_example",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios --scheme='debug' --simulator='iPhone 16 Pro'",
"run:ios:unused": "xcodebuild -workspace ios/OPSQLiteExample.xcworkspace -scheme release -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16 Pro' clean build",
"run:ios:release": "react-native run-ios --scheme='release' --no-packager",
"postinstall": "patch-package",
"start": "react-native start",
"pods": "cd ios && bundle exec pod install && rm -f .xcode.env.local",
"pods:nuke": "cd ios && rm -rf Pods && rm -rf Podfile.lock && bundle exec pod install",
"run:android:release": "cd android && ./gradlew assembleRelease && adb install -r app/build/outputs/apk/release/app-release.apk && adb shell am start -n com.op.sqlite.example/.MainActivity",
"build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
"build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO",
"web": "vite",
"web:build": "vite build",
"web:preview": "vite preview"
},
"dependencies": {
"@op-engineering/op-test": "0.2.7",
"@sqlite.org/sqlite-wasm": "^3.51.2-build8",
"chance": "^1.1.9",
"clsx": "^2.0.0",
"events": "^3.3.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-native": "0.82.1",
"react-native-safe-area-context": "^5.6.2",
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "^18.0.0",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.82.1",
"@react-native/metro-config": "0.82.1",
"@react-native/typescript-config": "0.81.5",
"@types/chance": "^1.1.7",
"@types/react": "^19.1.1",
"@vitejs/plugin-react": "^5.1.0",
"patch-package": "^8.0.1",
"react-native-builder-bob": "^0.40.13",
"react-native-monorepo-config": "^0.1.9",
"react-native-restart": "^0.0.27",
"tailwindcss": "3.3.2",
"vite": "^7.1.9"
},
"engines": {
"node": ">=18"
},
"op-sqlite": {
"libsql": false,
"turso": false,
"sqlcipher": false,
"iosSqlite": false,
"fts5": true,
"rtree": true,
"crsqlite": false,
"sqliteVec": false,
"performanceMode": true,
"tokenizers": [
"wordtokenizer",
"porter"
]
}
}
"name": "op_sqlite_example",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios --scheme='debug' --simulator='iPhone 16 Pro'",
"run:ios:unused": "xcodebuild -workspace ios/OPSQLiteExample.xcworkspace -scheme release -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16 Pro' clean build",
"run:ios:release": "react-native run-ios --scheme='release' --no-packager",
"postinstall": "patch-package",
"start": "react-native start",
"pods": "cd ios && bundle exec pod install && rm -f .xcode.env.local",
"pods:nuke": "cd ios && rm -rf Pods && rm -rf Podfile.lock && bundle exec pod install",
"run:android:release": "cd android && ./gradlew assembleRelease && adb install -r app/build/outputs/apk/release/app-release.apk && adb shell am start -n com.op.sqlite.example/.MainActivity",
"build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
"build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO",
"web": "vite",
"web:build": "vite build",
"web:preview": "vite preview"
},
"dependencies": {
"@op-engineering/op-test": "0.2.7",
"@sqlite.org/sqlite-wasm": "^3.51.2-build8",
"chance": "^1.1.9",
"clsx": "^2.0.0",
"events": "^3.3.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-native": "0.82.1",
"react-native-safe-area-context": "^5.6.2",
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "^18.0.0",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.82.1",
"@react-native/metro-config": "0.82.1",
"@react-native/typescript-config": "0.81.5",
"@types/chance": "^1.1.7",
"@types/react": "^19.1.1",
"@vitejs/plugin-react": "^5.1.0",
"patch-package": "^8.0.1",
"react-native-builder-bob": "^0.40.13",
"react-native-monorepo-config": "^0.1.9",
"react-native-restart": "^0.0.27",
"tailwindcss": "3.3.2",
"vite": "^7.1.9"
},
"engines": {
"node": ">=18"
},
"op-sqlite": {
"libsql": false,
"turso": false,
"sqlcipher": false,
"iosSqlite": false,
"fts5": true,
"rtree": true,
"crsqlite": false,
"sqliteVec": false,
"performanceMode": true,
"tokenizers": [
"wordtokenizer",
"porter"
]
}
}
35 changes: 24 additions & 11 deletions example/src/tests/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ describe("Reactive queries", () => {
let firstReactiveRan = false;
let secondReactiveRan = false;
let emittedUser = null;
let promiseResolve: ((v: unknown) => void) | null = null;

const unsubscribe = db.reactiveExecute({
query: "SELECT * FROM User;",
Expand Down Expand Up @@ -244,24 +245,33 @@ describe("Reactive queries", () => {
},
],
callback: (data) => {
promiseResolve?.(null);
emittedUser = data.rows[0];
},
});

let promise = new Promise(
(resolve, _) => (promiseResolve = resolve),
);

await db.executeBatch([
[
"INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);",
[1, "John", 30, 1000, "Johnny"],
],
]);

await sleep(0);
await promise;

promise = new Promise(
(resolve, _) => (promiseResolve = resolve),
);

await db.transaction(async (tx) => {
await tx.execute("UPDATE User SET name = ? WHERE id = ?;", ["Foo", 1]);
});

await sleep(0);
await promise;

expect(!!firstReactiveRan).toBe(false);
expect(!!secondReactiveRan).toBe(false);
Expand All @@ -274,15 +284,19 @@ describe("Reactive queries", () => {
});

it("Update hook and reactive queries work at the same time", async () => {
let promiseResolve: any;
const promise = new Promise((resolve) => {
promiseResolve = resolve;
let hookResolve: ((v: unknown) => void) | null = null;
let reactiveResolve: ((v: unknown) => void) | null = null;
const hookPromise = new Promise((resolve) => {
hookResolve = resolve;
});
const reactivePromise = new Promise((resolve) => {
reactiveResolve = resolve;
});

db.updateHook(({ operation }) => {
promiseResolve?.(operation);
hookResolve?.(operation);
});

let emittedUser = null;
const unsubscribe = db.reactiveExecute({
query: "SELECT * FROM User;",
arguments: [],
Expand All @@ -292,7 +306,7 @@ describe("Reactive queries", () => {
},
],
callback: (data) => {
emittedUser = data.rows[0];
reactiveResolve?.(data.rows[0]);
},
});

Expand All @@ -311,9 +325,8 @@ describe("Reactive queries", () => {
);
});

const operation = await promise;

await sleep(0);
const operation = await hookPromise;
const emittedUser = await reactivePromise;

expect(operation).toEqual("INSERT");
expect(emittedUser).toDeepEqual({
Expand Down
5 changes: 2 additions & 3 deletions node/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,9 @@ export class NodeDatabase implements DB {
}
}

delete(location?: string): void {
delete(): void {
this.close();
const dbLocation = location || './';
const dbPath = path.join(dbLocation, path.basename(this.dbPath));
const dbPath = this.dbPath;

if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath);
Expand Down
Loading
Loading