From a8bd0f4eb5b2e9655bbb64ac252c68cbe06323e0 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 17 May 2026 11:14:12 -0400 Subject: [PATCH 1/6] Modify delete to not take any params --- cpp/DBHostObject.cpp | 25 +-- docs/docs/api.md | 27 +++ node/src/database.ts | 5 +- node/src/test.spec.ts | 374 +++++++++++++++++++++--------------------- node/src/types.ts | 2 +- src/index.ts | 52 +++--- src/index.web.ts | 44 ++--- src/types.ts | 4 +- 8 files changed, 269 insertions(+), 264 deletions(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index a31c48c5..c184b4bd 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -280,31 +280,10 @@ 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"); - } - - std::string location = args[1].asString(rt).utf8(rt); - - if (!location.empty()) { - if (location == ":memory:") { - path = ":memory:"; - } else if (location.rfind('/', 0) == 0) { - path = location; - } else { - path = path + "/" + location; - } - } - } - #ifdef OP_SQLITE_USE_LIBSQL - opsqlite_libsql_remove(db, db_name, path); + opsqlite_libsql_remove(db, db_name, base_path); #else - opsqlite_remove(db, db_name, path); + opsqlite_remove(db, db_name, base_path); #endif return {}; diff --git a/docs/docs/api.md b/docs/docs/api.md index 053568d1..d70d9374 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -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. diff --git a/node/src/database.ts b/node/src/database.ts index 789087ee..64f343dd 100644 --- a/node/src/database.ts +++ b/node/src/database.ts @@ -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); diff --git a/node/src/test.spec.ts b/node/src/test.spec.ts index cfcd9984..9998ab2b 100644 --- a/node/src/test.spec.ts +++ b/node/src/test.spec.ts @@ -1,188 +1,188 @@ -import { open, isSQLCipher, isLibsql, isIOSEmbedded } from './index'; - -describe('op-sqlite Node.js tests', () => { - let db: ReturnType; - - beforeAll(() => { - db = open({ name: 'test.sqlite', location: './' }); - }); - - afterAll(() => { - db.close(); - - const cleanupDb = open({ name: 'test.sqlite', location: './' }); - cleanupDb.delete('./'); - - const cleanupDb2 = open({ name: 'test2.sqlite', location: './' }); - cleanupDb2.delete('./'); - }); - - test('Database opens successfully', () => { - const path = db.getDbPath(); - expect(path).toContain('test.sqlite'); - }); - - test('Create table', () => { - db.executeSync( - 'CREATE TABLE IF NOT EXISTS test_users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)' - ); - }); - - test('Insert data', () => { - const result = db.executeSync( - 'INSERT INTO test_users (name, age) VALUES (?, ?)', - ['Alice', 30] - ); - expect(result.rowsAffected).toBe(1); - expect(result.insertId).toBeDefined(); - }); - - test('Query data', async () => { - const result = await db.execute('SELECT * FROM test_users WHERE name = ?', [ - 'Alice', - ]); - expect(result.rows.length).toBe(1); - expect(result.rows[0].name).toBe('Alice'); - expect(result.rows[0].age).toBe(30); - }); - - test('Query without parameters', () => { - const result = db.executeSync('SELECT COUNT(*) as count FROM test_users'); - const row: number = result.rows.length; - expect(row).toBeGreaterThanOrEqual(1); - }); - - test('Update data', () => { - const result = db.executeSync( - 'UPDATE test_users SET age = ? WHERE name = ?', - [31, 'Alice'] - ); - expect(result.rowsAffected).toBe(1); - }); - - test('Verify update', () => { - const result = db.executeSync('SELECT age FROM test_users WHERE name = ?', [ - 'Alice', - ]); - expect(result.rows[0].age).toBe(31); - }); - - test('Execute raw query', async () => { - const result = await db.executeRaw( - 'SELECT name, age FROM test_users WHERE name = ?', - ['Alice'] - ); - expect(Array.isArray(result)).toBe(true); - expect(Array.isArray(result[0])).toBe(true); - expect(result[0][0]).toBe('Alice'); - expect(result[0][1]).toBe(31); - }); - - test('Execute batch', async () => { - const result = await db.executeBatch([ - ['INSERT INTO test_users (name, age) VALUES (?, ?)', ['Bob', 25]], - ['INSERT INTO test_users (name, age) VALUES (?, ?)', ['Charlie', 35]], - ]); - expect(result.rowsAffected ?? 0).toBeGreaterThanOrEqual(2); - }); - - test('Transaction commit', async () => { - // Skipped: better-sqlite3's transaction function doesn't support async/await - await db.transaction(async (tx) => { - await tx.execute('INSERT INTO test_users (name, age) VALUES (?, ?)', [ - 'David', - 40, - ]); - await tx.execute('INSERT INTO test_users (name, age) VALUES (?, ?)', [ - 'Emma', - 28, - ]); - }); - - const result = db.executeSync('SELECT COUNT(*) as count FROM test_users'); - expect(result.rows[0].count).toBeGreaterThanOrEqual(5); - }); - - test.skip('Transaction rollback', async () => { - // Skipped: The transaction implementation doesn't properly return a promise, - // which causes issues with testing error handling - const beforeCount = db.executeSync( - 'SELECT COUNT(*) as count FROM test_users' - ).rows[0].count; - - try { - await db.transaction(async (tx) => { - await tx.execute('INSERT INTO test_users (name, age) VALUES (?, ?)', [ - 'Temporary', - 99, - ]); - throw new Error('Rollback test'); - }); - } catch (error: any) { - // Expected error - } - - const afterCount = db.executeSync( - 'SELECT COUNT(*) as count FROM test_users' - ).rows[0].count; - expect(beforeCount).toBe(afterCount); - }); - - test('Prepared statement', async () => { - const stmt = db.prepareStatement('SELECT * FROM test_users WHERE age > ?'); - - stmt.bindSync([30]); - const result1 = await stmt.execute(); - expect(result1.rows.length).toBeGreaterThanOrEqual(2); - - stmt.bindSync([25]); - const result2 = await stmt.execute(); - expect(result2.rows.length).toBeGreaterThanOrEqual(result1.rows.length); - }); - - test('Query metadata', () => { - const result = db.executeSync('SELECT * FROM test_users LIMIT 1'); - expect(result.metadata).toBeDefined(); - expect(result.metadata!.length).toBeGreaterThanOrEqual(3); - expect(result.columnNames).toBeDefined(); - expect(result.columnNames).toContain('name'); - }); - - test('Delete data', () => { - const result = db.executeSync('DELETE FROM test_users WHERE name = ?', [ - 'Bob', - ]); - expect(result.rowsAffected).toBe(1); - }); - - test('Attach database', () => { - const db2 = open({ name: 'test2.sqlite', location: './' }); - db2.executeSync( - 'CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT)' - ); - db2.executeSync('INSERT INTO products (name) VALUES (?)', ['Laptop']); - db2.close(); - - db.attach({ - secondaryDbFileName: 'test2.sqlite', - alias: 'secondary', - location: './', - }); - const result = db.executeSync('SELECT * FROM secondary.products'); - expect(result.rows.length).toBe(1); - expect(result.rows[0].name).toBe('Laptop'); - }); - - test('Detach database', () => { - db.detach('secondary'); - expect(() => { - db.executeSync('SELECT * FROM secondary.products'); - }).toThrow(/no such table|secondary/); - }); - - test('Feature checks', () => { - expect(isSQLCipher()).toBe(false); - expect(isLibsql()).toBe(false); - expect(isIOSEmbedded()).toBe(false); - }); +import { isIOSEmbedded, isLibsql, isSQLCipher, open } from "./index"; + +describe("op-sqlite Node.js tests", () => { + let db: ReturnType; + + beforeAll(() => { + db = open({ name: "test.sqlite", location: "./" }); + }); + + afterAll(() => { + db.close(); + + const cleanupDb = open({ name: "test.sqlite", location: "./" }); + cleanupDb.delete(); + + const cleanupDb2 = open({ name: "test2.sqlite", location: "./" }); + cleanupDb2.delete(); + }); + + test("Database opens successfully", () => { + const path = db.getDbPath(); + expect(path).toContain("test.sqlite"); + }); + + test("Create table", () => { + db.executeSync( + "CREATE TABLE IF NOT EXISTS test_users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", + ); + }); + + test("Insert data", () => { + const result = db.executeSync( + "INSERT INTO test_users (name, age) VALUES (?, ?)", + ["Alice", 30], + ); + expect(result.rowsAffected).toBe(1); + expect(result.insertId).toBeDefined(); + }); + + test("Query data", async () => { + const result = await db.execute("SELECT * FROM test_users WHERE name = ?", [ + "Alice", + ]); + expect(result.rows.length).toBe(1); + expect(result.rows[0].name).toBe("Alice"); + expect(result.rows[0].age).toBe(30); + }); + + test("Query without parameters", () => { + const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); + const row: number = result.rows.length; + expect(row).toBeGreaterThanOrEqual(1); + }); + + test("Update data", () => { + const result = db.executeSync( + "UPDATE test_users SET age = ? WHERE name = ?", + [31, "Alice"], + ); + expect(result.rowsAffected).toBe(1); + }); + + test("Verify update", () => { + const result = db.executeSync("SELECT age FROM test_users WHERE name = ?", [ + "Alice", + ]); + expect(result.rows[0].age).toBe(31); + }); + + test("Execute raw query", async () => { + const result = await db.executeRaw( + "SELECT name, age FROM test_users WHERE name = ?", + ["Alice"], + ); + expect(Array.isArray(result)).toBe(true); + expect(Array.isArray(result[0])).toBe(true); + expect(result[0][0]).toBe("Alice"); + expect(result[0][1]).toBe(31); + }); + + test("Execute batch", async () => { + const result = await db.executeBatch([ + ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Bob", 25]], + ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Charlie", 35]], + ]); + expect(result.rowsAffected ?? 0).toBeGreaterThanOrEqual(2); + }); + + test("Transaction commit", async () => { + // Skipped: better-sqlite3's transaction function doesn't support async/await + await db.transaction(async (tx) => { + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ + "David", + 40, + ]); + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ + "Emma", + 28, + ]); + }); + + const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); + expect(result.rows[0].count).toBeGreaterThanOrEqual(5); + }); + + test.skip("Transaction rollback", async () => { + // Skipped: The transaction implementation doesn't properly return a promise, + // which causes issues with testing error handling + const beforeCount = db.executeSync( + "SELECT COUNT(*) as count FROM test_users", + ).rows[0].count; + + try { + await db.transaction(async (tx) => { + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ + "Temporary", + 99, + ]); + throw new Error("Rollback test"); + }); + } catch (error: any) { + // Expected error + } + + const afterCount = db.executeSync( + "SELECT COUNT(*) as count FROM test_users", + ).rows[0].count; + expect(beforeCount).toBe(afterCount); + }); + + test("Prepared statement", async () => { + const stmt = db.prepareStatement("SELECT * FROM test_users WHERE age > ?"); + + stmt.bindSync([30]); + const result1 = await stmt.execute(); + expect(result1.rows.length).toBeGreaterThanOrEqual(2); + + stmt.bindSync([25]); + const result2 = await stmt.execute(); + expect(result2.rows.length).toBeGreaterThanOrEqual(result1.rows.length); + }); + + test("Query metadata", () => { + const result = db.executeSync("SELECT * FROM test_users LIMIT 1"); + expect(result.metadata).toBeDefined(); + expect(result.metadata!.length).toBeGreaterThanOrEqual(3); + expect(result.columnNames).toBeDefined(); + expect(result.columnNames).toContain("name"); + }); + + test("Delete data", () => { + const result = db.executeSync("DELETE FROM test_users WHERE name = ?", [ + "Bob", + ]); + expect(result.rowsAffected).toBe(1); + }); + + test("Attach database", () => { + const db2 = open({ name: "test2.sqlite", location: "./" }); + db2.executeSync( + "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT)", + ); + db2.executeSync("INSERT INTO products (name) VALUES (?)", ["Laptop"]); + db2.close(); + + db.attach({ + secondaryDbFileName: "test2.sqlite", + alias: "secondary", + location: "./", + }); + const result = db.executeSync("SELECT * FROM secondary.products"); + expect(result.rows.length).toBe(1); + expect(result.rows[0].name).toBe("Laptop"); + }); + + test("Detach database", () => { + db.detach("secondary"); + expect(() => { + db.executeSync("SELECT * FROM secondary.products"); + }).toThrow(/no such table|secondary/); + }); + + test("Feature checks", () => { + expect(isSQLCipher()).toBe(false); + expect(isLibsql()).toBe(false); + expect(isIOSEmbedded()).toBe(false); + }); }); diff --git a/node/src/types.ts b/node/src/types.ts index e5395152..a4975c9c 100644 --- a/node/src/types.ts +++ b/node/src/types.ts @@ -52,7 +52,7 @@ export type PreparedStatement = { export type DB = { close: () => void; - delete: (location?: string) => void; + delete: () => void; attach: (params: { secondaryDbFileName: string; alias: string; diff --git a/src/index.ts b/src/index.ts index 6fe3041e..eb98eba7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,30 @@ -import { NativeModules } from 'react-native'; +import { NativeModules } from "react-native"; -export * from './functions'; -export { Storage } from './Storage'; +export * from "./functions"; +export { Storage } from "./Storage"; export type { - Scalar, - QueryResult, - ColumnMetadata, - SQLBatchTuple, - UpdateHookOperation, - BatchQueryResult, - FileLoadResult, - Transaction, - _PendingTransaction, - PreparedStatement, - _InternalDB, - DB, - DBParams, - OPSQLiteProxy, -} from './types'; + _InternalDB, + _PendingTransaction, + BatchQueryResult, + ColumnMetadata, + DB, + DBParams, + FileLoadResult, + OPSQLiteProxy, + PreparedStatement, + QueryResult, + Scalar, + SQLBatchTuple, + Transaction, + UpdateHookOperation, +} from "./types"; export const { - IOS_DOCUMENT_PATH, - IOS_LIBRARY_PATH, - ANDROID_DATABASE_PATH, - ANDROID_FILES_PATH, - ANDROID_EXTERNAL_FILES_PATH, -} = !!NativeModules.OPSQLite.getConstants - ? NativeModules.OPSQLite.getConstants() - : NativeModules.OPSQLite; + IOS_DOCUMENT_PATH, + IOS_LIBRARY_PATH, + ANDROID_DATABASE_PATH, + ANDROID_FILES_PATH, + ANDROID_EXTERNAL_FILES_PATH, +} = NativeModules.OPSQLite.getConstants + ? NativeModules.OPSQLite.getConstants() + : NativeModules.OPSQLite; diff --git a/src/index.web.ts b/src/index.web.ts index 5b5b2a55..0a8f975c 100644 --- a/src/index.web.ts +++ b/src/index.web.ts @@ -1,24 +1,24 @@ -export * from './functions.web'; -export { Storage } from './Storage.web'; +export * from "./functions.web"; +export { Storage } from "./Storage.web"; export type { - Scalar, - QueryResult, - ColumnMetadata, - SQLBatchTuple, - UpdateHookOperation, - BatchQueryResult, - FileLoadResult, - Transaction, - _PendingTransaction, - PreparedStatement, - _InternalDB, - DB, - DBParams, - OPSQLiteProxy, -} from './types'; + _InternalDB, + _PendingTransaction, + BatchQueryResult, + ColumnMetadata, + DB, + DBParams, + FileLoadResult, + OPSQLiteProxy, + PreparedStatement, + QueryResult, + Scalar, + SQLBatchTuple, + Transaction, + UpdateHookOperation, +} from "./types"; -export const IOS_DOCUMENT_PATH = ''; -export const IOS_LIBRARY_PATH = ''; -export const ANDROID_DATABASE_PATH = ''; -export const ANDROID_FILES_PATH = ''; -export const ANDROID_EXTERNAL_FILES_PATH = ''; +export const IOS_DOCUMENT_PATH = ""; +export const IOS_LIBRARY_PATH = ""; +export const ANDROID_DATABASE_PATH = ""; +export const ANDROID_FILES_PATH = ""; +export const ANDROID_EXTERNAL_FILES_PATH = ""; diff --git a/src/types.ts b/src/types.ts index 8610bada..03abd56d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,7 +102,7 @@ export type PreparedStatement = { export type _InternalDB = { close: () => void; closeAsync?: () => Promise; - delete: (location?: string) => void; + delete: () => void; attach: (params: { secondaryDbFileName: string; alias: string; @@ -153,7 +153,7 @@ export type _InternalDB = { export type DB = { close: () => void; closeAsync: () => Promise; - delete: (location?: string) => void; + delete: () => void; attach: (params: { secondaryDbFileName: string; alias: string; From 5a06e5002b6e9f5c26e32773fa7c8c0ed543d819 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 17 May 2026 16:25:26 -0400 Subject: [PATCH 2/6] Various fixes --- cpp/DBHostObject.cpp | 33 +++++++++++++++++++++++++++------ cpp/DBHostObject.h | 1 + 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index c184b4bd..9cc96f16 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -6,6 +6,7 @@ #include "bridge.h" #endif #include "logs.h" +#include #include "macros.hpp" #include "utils.hpp" #include @@ -23,6 +24,13 @@ void DBHostObject::flush_pending_reactive_queries( resolve->asObject(rt).asFunction(rt).call(rt, {}); }); } +#elif OP_SQLITE_USE_TURSO + +std::string turso_remote_db_name(const std::string &url) { + return "turso_remote_" + std::to_string(std::hash{}(url)) + + ".sqlite"; +} + #else void DBHostObject::flush_pending_reactive_queries( const std::shared_ptr &resolve) { @@ -161,7 +169,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(); @@ -176,7 +184,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(); db = opsqlite_open_remote(url, auth_token, base_path); @@ -188,7 +197,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(); @@ -205,7 +214,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(); #ifdef OP_SQLITE_USE_SQLCIPHER @@ -278,12 +287,24 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["delete"] = HFN(this) { + if (count != 0) { + throw std::runtime_error("[op-sqlite] Delete no longer takes arguments"); + } + 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 (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, base_path); + opsqlite_libsql_remove(db, delete_db_name, base_path); #else - opsqlite_remove(db, db_name, base_path); + opsqlite_remove(db, delete_db_name, base_path); #endif return {}; diff --git a/cpp/DBHostObject.h b/cpp/DBHostObject.h index 3d6c8a93..7029a152 100644 --- a/cpp/DBHostObject.h +++ b/cpp/DBHostObject.h @@ -89,6 +89,7 @@ class JSI_EXPORT DBHostObject : public jsi::HostObject { std::string base_path; std::shared_ptr thread_pool; std::string db_name; + std::string delete_db_name; std::shared_ptr update_hook_callback; std::shared_ptr commit_hook_callback; std::shared_ptr rollback_hook_callback; From 5e3523ecfc9d4318151f3acb4dbbec49e63ce3e7 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 17 May 2026 16:30:26 -0400 Subject: [PATCH 3/6] Fix --- cpp/DBHostObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 9cc96f16..1b95a9c1 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -24,7 +24,7 @@ void DBHostObject::flush_pending_reactive_queries( resolve->asObject(rt).asFunction(rt).call(rt, {}); }); } -#elif OP_SQLITE_USE_TURSO +#elif defined(OP_SQLITE_USE_TURSO) std::string turso_remote_db_name(const std::string &url) { return "turso_remote_" + std::to_string(std::hash{}(url)) + From c1dafcc40ce0b1fbe61400da5c2b5f824e148e21 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 17 May 2026 16:38:29 -0400 Subject: [PATCH 4/6] Empty flush_pending_reactive_queries for turso --- cpp/DBHostObject.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 1b95a9c1..97b56225 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -31,6 +31,12 @@ std::string turso_remote_db_name(const std::string &url) { ".sqlite"; } +void DBHostObject::flush_pending_reactive_queries( + const std::shared_ptr &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 &resolve) { From 9115e76ebd8bad83d7a9d97763e68bc48099b668 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Mon, 18 May 2026 07:46:48 -0400 Subject: [PATCH 5/6] Make reactive queries tests deterministic --- example/package.json | 140 +++++++++++++++++----------------- example/src/tests/reactive.ts | 16 +++- 2 files changed, 83 insertions(+), 73 deletions(-) diff --git a/example/package.json b/example/package.json index 9666c964..667f0289 100644 --- a/example/package.json +++ b/example/package.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/example/src/tests/reactive.ts b/example/src/tests/reactive.ts index bea3e3a9..6436453a 100644 --- a/example/src/tests/reactive.ts +++ b/example/src/tests/reactive.ts @@ -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;", @@ -244,10 +245,15 @@ 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 (?, ?, ?, ?, ?);", @@ -255,13 +261,17 @@ describe("Reactive queries", () => { ], ]); - 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); @@ -274,7 +284,7 @@ describe("Reactive queries", () => { }); it("Update hook and reactive queries work at the same time", async () => { - let promiseResolve: any; + let promiseResolve: ((v: unknown) => void) | null = null; const promise = new Promise((resolve) => { promiseResolve = resolve; }); From 14f4d226b3f53f3bbe9d86af16e0f170f2054696 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Mon, 18 May 2026 08:04:40 -0400 Subject: [PATCH 6/6] Make other reactive test deterministic --- example/src/tests/reactive.ts | 21 ++++++++++++--------- node/tsconfig.json | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/example/src/tests/reactive.ts b/example/src/tests/reactive.ts index 6436453a..e44b10f6 100644 --- a/example/src/tests/reactive.ts +++ b/example/src/tests/reactive.ts @@ -284,15 +284,19 @@ describe("Reactive queries", () => { }); it("Update hook and reactive queries work at the same time", async () => { - let promiseResolve: ((v: unknown) => void) | null = null; - 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: [], @@ -302,7 +306,7 @@ describe("Reactive queries", () => { }, ], callback: (data) => { - emittedUser = data.rows[0]; + reactiveResolve?.(data.rows[0]); }, }); @@ -321,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({ diff --git a/node/tsconfig.json b/node/tsconfig.json index c9c09d62..3b4d879e 100644 --- a/node/tsconfig.json +++ b/node/tsconfig.json @@ -12,7 +12,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", + "moduleResolution": "nodenext", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "types": ["node", "jest"]