diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..55c15df3 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..1a581dd2 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,15 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "typescript", + "unicorn", + "oxc" + ], + "categories": { + "correctness": "warn" + }, + "rules": {}, + "env": { + "builtin": true + } +} \ No newline at end of file diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 7e11123e..ea0a6c5f 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -298,6 +298,8 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["close"] = HFN(this) { + reject_all_transaction_lock_waiters(rt, + "[op-sqlite] database is closed"); invalidated = true; // Abort pending native SQLite work before waiting on the thread pool. #if !defined(OP_SQLITE_USE_LIBSQL) && !defined(OP_SQLITE_USE_TURSO) @@ -346,6 +348,8 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { throw std::runtime_error("[op-sqlite] Delete no longer takes arguments"); } + reject_all_transaction_lock_waiters(rt, + "[op-sqlite] database was deleted"); invalidated = true; // Abort pending native SQLite work before waiting on the thread pool. #if !defined(OP_SQLITE_USE_LIBSQL) && !defined(OP_SQLITE_USE_TURSO) @@ -414,6 +418,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { return create_js_rows(rt, status); }); + function_map["executeRawSync"] = HFN(this) { const std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); std::vector params = count == 2 && args[1].isObject() @@ -523,6 +528,39 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); }); + function_map["acquireTransactionLock"] = HFN(this) { + if (invalidated) { + throw std::runtime_error( + "[op-sqlite][acquireTransactionLock] database is closed"); + } + + auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); + auto promise = promiseCtr.callAsConstructor(rt, HFN(this) { + auto resolve = std::make_shared(rt, args[0]); + auto reject = std::make_shared(rt, args[1]); + + if (transaction_lock_in_progress) { + transaction_lock_waiters.push_back({resolve, reject}); + } else { + transaction_lock_in_progress = true; + resolve->asObject(rt).asFunction(rt).call(rt, {}); + } + + return {}; + })); + + return promise; + }); + + function_map["releaseTransactionLock"] = HFN(this) { + if (!transaction_lock_in_progress) { + return {}; + } + + resolve_next_transaction_lock_waiter(rt); + return {}; + }); + #if defined(OP_SQLITE_USE_LIBSQL) || defined(OP_SQLITE_USE_TURSO) function_map["sync"] = HFN(this) { #ifdef OP_SQLITE_USE_LIBSQL @@ -742,6 +780,29 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); } +void DBHostObject::resolve_next_transaction_lock_waiter(jsi::Runtime &rt) { + if (!transaction_lock_waiters.empty()) { + auto waiter = transaction_lock_waiters.front(); + transaction_lock_waiters.pop_front(); + waiter.resolve->asObject(rt).asFunction(rt).call(rt, {}); + return; + } + + transaction_lock_in_progress = false; +} + +void DBHostObject::reject_all_transaction_lock_waiters( + jsi::Runtime &rt, const std::string &message) { + transaction_lock_in_progress = false; + while (!transaction_lock_waiters.empty()) { + auto waiter = transaction_lock_waiters.front(); + transaction_lock_waiters.pop_front(); + jsi::JSError js_error(rt, message); + const auto &error_value = js_error.value(); + waiter.reject->asObject(rt).asFunction(rt).call(rt, error_value); + } +} + std::vector DBHostObject::getPropertyNames(jsi::Runtime &_rt) { std::vector keys; keys.reserve(function_map.size()); diff --git a/cpp/DBHostObject.h b/cpp/DBHostObject.h index 7029a152..84fd7904 100644 --- a/cpp/DBHostObject.h +++ b/cpp/DBHostObject.h @@ -3,6 +3,7 @@ #include "OPThreadPool.h" #include "types.hpp" #include +#include #include #include #ifdef OP_SQLITE_USE_LIBSQL @@ -41,6 +42,11 @@ struct ReactiveQuery { std::shared_ptr callback; }; +struct PendingTransactionLockWaiter { + std::shared_ptr resolve; + std::shared_ptr reject; +}; + class JSI_EXPORT DBHostObject : public jsi::HostObject { public: // Normal constructor shared between all backends @@ -84,6 +90,9 @@ class JSI_EXPORT DBHostObject : public jsi::HostObject { void auto_register_update_hook(); void create_jsi_functions(jsi::Runtime &rt); void flush_pending_reactive_queries(const std::shared_ptr &resolve); + void resolve_next_transaction_lock_waiter(jsi::Runtime &rt); + void reject_all_transaction_lock_waiters(jsi::Runtime &rt, + const std::string &message); std::unordered_map function_map; std::string base_path; @@ -95,6 +104,8 @@ class JSI_EXPORT DBHostObject : public jsi::HostObject { std::shared_ptr rollback_hook_callback; std::vector> reactive_queries; std::vector pending_reactive_invocations; + std::deque transaction_lock_waiters; + bool transaction_lock_in_progress = false; bool is_update_hook_registered = false; bool invalidated = false; #ifdef OP_SQLITE_USE_LIBSQL diff --git a/cpp/bridge.cpp b/cpp/bridge.cpp index adafe298..c1af5192 100644 --- a/cpp/bridge.cpp +++ b/cpp/bridge.cpp @@ -880,15 +880,31 @@ opsqlite_execute_batch(sqlite3 *db, } int affectedRows = 0; - // opsqlite_execute(db, "BEGIN EXCLUSIVE TRANSACTION", nullptr); - for (int i = 0; i < commandCount; i++) { - const auto &command = commands->at(i); - // We do not provide a datastructure to receive query data because we - // don't need/want to handle this results in a batch execution - // There is also no need to commit/catch this transaction, this is done - // in the JS code - auto result = opsqlite_execute(db, command.sql, &command.params); - affectedRows += result.affectedRows; + const bool should_manage_transaction = sqlite3_get_autocommit(db) != 0; + if (should_manage_transaction) { + opsqlite_execute(db, "BEGIN TRANSACTION;", nullptr); + } + + try { + for (int i = 0; i < commandCount; i++) { + const auto &command = commands->at(i); + // We do not provide a datastructure to receive query data because we + // don't need/want to handle this results in a batch execution + auto result = opsqlite_execute(db, command.sql, &command.params); + affectedRows += result.affectedRows; + } + + if (should_manage_transaction) { + opsqlite_execute(db, "COMMIT;", nullptr); + } + } catch (...) { + if (should_manage_transaction) { + try { + opsqlite_execute(db, "ROLLBACK;", nullptr); + } catch (...) { + } + } + throw; } return BatchResult{ diff --git a/cpp/libsql/bridge.cpp b/cpp/libsql/bridge.cpp index 444dbe65..bd6d2de3 100644 --- a/cpp/libsql/bridge.cpp +++ b/cpp/libsql/bridge.cpp @@ -744,9 +744,10 @@ opsqlite_libsql_execute_batch(DB const &db, throw std::runtime_error("No SQL commands provided"); } + int affectedRows = 0; + opsqlite_libsql_execute(db, "BEGIN TRANSACTION;", nullptr); + try { - int affectedRows = 0; - // opsqlite_libsql_execute(db, "BEGIN EXCLUSIVE TRANSACTION", nullptr); for (int i = 0; i < commandCount; i++) { auto command = commands->at(i); // We do not provide a datastructure to receive query data because @@ -755,16 +756,19 @@ opsqlite_libsql_execute_batch(DB const &db, opsqlite_libsql_execute(db, command.sql, &command.params); affectedRows += result.affectedRows; } - // opsqlite_libsql_execute(db, "COMMIT", nullptr); + + opsqlite_libsql_execute(db, "COMMIT;", nullptr); + return BatchResult{ .affectedRows = affectedRows, .commands = static_cast(commandCount), }; - } catch (std::exception &exc) { - // opsqlite_libsql_execute(db, "ROLLBACK", nullptr); - return BatchResult{ - .message = exc.what(), - }; + } catch (...) { + try { + opsqlite_libsql_execute(db, "ROLLBACK;", nullptr); + } catch (...) { + } + throw; } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 0da908b2..e4e95e6a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -8,21 +8,14 @@ import "./tests"; // import all tests to register them import {performanceTest} from './performance_test'; import { StyleSheet, Text, View } from "react-native"; import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; -// import {open} from '@op-engineering/op-sqlite'; export default function App() { const [results, setResults] = useState(null); const [perfResult, setPerfResult] = useState(0); - const [openTime, setOpenTime] = useState(0); useEffect(() => { console.log("App has started 🟢"); const work = async () => { - // let start = performance.now(); - // open({ - // name: 'dummyDb.sqlite', - // }); - // setOpenTime(performance.now() - start); try { console.log("TESTS STARTED 🟠"); @@ -36,15 +29,15 @@ export default function App() { console.log("OPSQLITE_TEST_RESULT:FAIL"); } - setTimeout(() => { + setTimeout(async () => { try { global?.gc?.(); - let perfRes = performanceTest(); + let perfRes = await performanceTest(); setPerfResult(perfRes); } catch (e) { // intentionally left blank } - }, 4000); + }, 1000); }; work(); @@ -79,10 +72,7 @@ export default function App() { - Open DB time: {openTime.toFixed(0)} ms - - - 100_000 query time: {perfResult.toFixed(0)} ms + Performance Test: {perfResult.toFixed(0)} ms {displayResults(results)} diff --git a/example/src/performance_test.ts b/example/src/performance_test.ts index 67f7a9e6..2475dbb8 100644 --- a/example/src/performance_test.ts +++ b/example/src/performance_test.ts @@ -1,11 +1,10 @@ -import {open} from '@op-engineering/op-sqlite'; +import { open } from "@op-engineering/op-sqlite"; -export function performanceTest() { +export async function performanceTest() { const db = open({ - name: 'perfTest.sqlite', + name: "perfTest.sqlite", }); - // Create table with 14 columns db.executeSync( `CREATE TABLE IF NOT EXISTS perf_table ( id INTEGER PRIMARY KEY, @@ -13,29 +12,42 @@ export function performanceTest() { col8 TEXT, col9 TEXT, col10 TEXT, col11 TEXT, col12 TEXT, col13 TEXT, col14 TEXT )`, ); - // Clear table - db.executeSync('DELETE FROM perf_table'); - const testRow =Array(14).fill('test') ; - let start = performance.now(); + const testRow = Array(14).fill("test"); + const runDurations: number[] = []; - for (let i = 0; i < 1_000; i++) { - // Insert a single row for querying - db.executeSync( - `INSERT INTO perf_table ( + for (let run = 0; run < 10; run++) { + db.executeSync("DELETE FROM perf_table"); + + const start = performance.now(); + + for (let i = 0; i < 200; i++) { + const txPromises = Array.from({ length: 10 }, () => + db.transaction(async (tx) => { + await tx.execute( + `INSERT INTO perf_table ( col1, col2, col3, col4, col5, col6, col7, col8, col9, col10, col11, col12, col13, col14 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - testRow, - ); - } + testRow, + ); + }), + ); - for (let i = 0; i < 100000; i++) { - db.executeSync('SELECT * FROM perf_table WHERE id = 1'); + await Promise.all(txPromises); + } + + for (let i = 0; i < 100000; i++) { + db.executeSync("SELECT * FROM perf_table WHERE id = 1"); + } + + const end = performance.now(); + runDurations.push(end - start); } - const end = performance.now(); - // console.log(`Queried 100000 times in ${end - start} ms`); + + const total = runDurations.reduce((sum, duration) => sum + duration, 0); + const average = total / runDurations.length; // await db.close(); - return end - start; + return average; } diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 1b3db324..91405568 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -1,473 +1,471 @@ import { - // openRemote, - // openSync, - type DB, - isLibsql, - isTurso, - open, - type SQLBatchTuple, + // openRemote, + // openSync, + type DB, + isLibsql, + isTurso, + open, + type SQLBatchTuple, } from "@op-engineering/op-sqlite"; -import { - afterEach, - beforeEach, - describe, - expect, - it, -} from "@op-engineering/op-test"; +import { afterEach, beforeEach, describe, expect, it } from "@op-engineering/op-test"; import { chance, sleep } from "./utils"; // import pkg from '../../package.json' describe("Queries tests", () => { - let db: DB; - - beforeEach(async () => { - db = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - await db.execute("DROP TABLE IF EXISTS User;"); - await db.execute("DROP TABLE IF EXISTS T1;"); - await db.execute("DROP TABLE IF EXISTS T2;"); - await db.execute( - "CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", - ); - }); - - afterEach(() => { - if (db) { - db.delete(); - // @ts-expect-error - db = null; - } - }); - - if (isLibsql()) { - // itOnly('Remote open a turso database', async () => { - // const remoteDb = openRemote({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // }); - // console.log('Running select 1'); - // const res = await remoteDb.execute('SELECT 1'); - // console.log('after select 1;'); - // expect(res.rowsAffected).toEqual(0); - // }); - // it('Open a libsql database replicated to turso', async () => { - // const remoteDb = openSync({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // name: 'my replica', - // libsqlSyncInterval: 1000, - // encryptionKey: 'blah', - // }); - // const res = await remoteDb.execute('SELECT 1'); - // remoteDb.sync(); - // expect(res.rowsAffected).toEqual(0); - // }); - } - - it("Can create multiple connections to same db", async () => { - const db2 = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - const db3 = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - const promises = [ - db.execute("SELECT 1"), - db2.execute("SELECT 1"), - db3.execute("SELECT 1"), - ]; - - const res = await Promise.all(promises); - res.forEach((r) => { - expect(r.rowsAffected).toEqual(0); - expect(r.rows[0]?.["1"]).toEqual(1); - }); - }); - - it("Trying to pass object as param should throw", async () => { - try { - // @ts-expect-error - await db.execute("SELECT ?", [{ foo: "bar" }]); - } catch (e: any) { - expect( - e.message.includes( - "Object is not an ArrayBuffer or ArrayBuffer view", - ), - ).toEqual(true); - } - }); - - it("interrupt is safe to call with no in-flight query", () => { - if (isLibsql() || isTurso()) { - return; - } - - let threw = false; - try { - db.interrupt(); - } catch (_e) { - threw = true; - } - - expect(threw).toEqual(false); - }); - - it("interrupt aborts an in-flight query and rolls back the transaction", async () => { - if (isLibsql() || isTurso()) { - return; - } - - await db.execute("DROP TABLE IF EXISTS InterruptTest;"); - await db.execute("CREATE TABLE InterruptTest (n INTEGER);"); - - const longQuery = ` + let db: DB; + + beforeEach(async () => { + db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute("DROP TABLE IF EXISTS T1;"); + await db.execute("DROP TABLE IF EXISTS T2;"); + await db.execute( + "CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", + ); + }); + + afterEach(() => { + if (db) { + db.delete(); + // @ts-expect-error + db = null; + } + }); + + if (isLibsql()) { + // itOnly('Remote open a turso database', async () => { + // const remoteDb = openRemote({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // }); + // console.log('Running select 1'); + // const res = await remoteDb.execute('SELECT 1'); + // console.log('after select 1;'); + // expect(res.rowsAffected).toEqual(0); + // }); + // it('Open a libsql database replicated to turso', async () => { + // const remoteDb = openSync({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // name: 'my replica', + // libsqlSyncInterval: 1000, + // encryptionKey: 'blah', + // }); + // const res = await remoteDb.execute('SELECT 1'); + // remoteDb.sync(); + // expect(res.rowsAffected).toEqual(0); + // }); + } + + it("Can create multiple connections to same db", async () => { + const db2 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const db3 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const promises = [db.execute("SELECT 1"), db2.execute("SELECT 1"), db3.execute("SELECT 1")]; + + const res = await Promise.all(promises); + res.forEach((r) => { + expect(r.rowsAffected).toEqual(0); + expect(r.rows[0]?.["1"]).toEqual(1); + }); + }); + + it("Trying to pass object as param should throw", async () => { + try { + // @ts-expect-error + await db.execute("SELECT ?", [{ foo: "bar" }]); + } catch (e: any) { + expect(e.message.includes("Object is not an ArrayBuffer or ArrayBuffer view")).toEqual(true); + } + }); + + it("interrupt is safe to call with no in-flight query", () => { + if (isLibsql() || isTurso()) { + return; + } + + let threw = false; + try { + db.interrupt(); + } catch (_e) { + threw = true; + } + + expect(threw).toEqual(false); + }); + + it("interrupt aborts an in-flight query and rolls back the transaction", async () => { + if (isLibsql() || isTurso()) { + return; + } + + await db.execute("DROP TABLE IF EXISTS InterruptTest;"); + await db.execute("CREATE TABLE InterruptTest (n INTEGER);"); + + const longQuery = ` WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 100000000 ) INSERT INTO InterruptTest SELECT n FROM seq; `; - const queryPromise = db.execute(longQuery); + const queryPromise = db.execute(longQuery); - await sleep(50); - db.interrupt(); + await sleep(50); + db.interrupt(); - let interrupted = false; - try { - await queryPromise; - } catch (e: any) { - interrupted = /interrupt|interrupted|abort|code 9|SQLITE_INTERRUPT/i.test( - String(e?.message ?? e), - ); - } + let interrupted = false; + try { + await queryPromise; + } catch (e: any) { + interrupted = /interrupt|interrupted|abort|code 9|SQLITE_INTERRUPT/i.test( + String(e?.message ?? e), + ); + } - expect(interrupted).toEqual(true); + expect(interrupted).toEqual(true); - const count = await db.execute("SELECT COUNT(*) AS n FROM InterruptTest;"); - expect(count.rows[0]!.n).toEqual(0); - }); + const count = await db.execute("SELECT COUNT(*) AS n FROM InterruptTest;"); + expect(count.rows[0]!.n).toEqual(0); + }); - it("close interrupts an in-flight query before teardown", async () => { - if (isLibsql() || isTurso()) { - return; - } + it("close interrupts an in-flight query before teardown", async () => { + if (isLibsql() || isTurso()) { + return; + } - await db.execute("DROP TABLE IF EXISTS CloseInterruptTest;"); - await db.execute("CREATE TABLE CloseInterruptTest (n INTEGER);"); + await db.execute("DROP TABLE IF EXISTS CloseInterruptTest;"); + await db.execute("CREATE TABLE CloseInterruptTest (n INTEGER);"); - const longQuery = ` + const longQuery = ` WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 100000000 ) INSERT INTO CloseInterruptTest SELECT n FROM seq; `; - const queryPromise = db.execute(longQuery); - - await sleep(50); - const startedAt = Date.now(); - db.close(); - const elapsedMs = Date.now() - startedAt; - - await queryPromise.catch(() => undefined); - expect(elapsedMs < 2000).toEqual(true); - - const cleanupDb = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - cleanupDb.delete(); - - // @ts-expect-error Prevent afterEach from deleting a closed handle. - db = null; - }); - - it("executeSync", () => { - const res = db.executeSync("SELECT 1"); - expect(res.rowsAffected).toEqual(0); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res2 = db.executeSync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res2.rowsAffected).toEqual(1); - expect(res2.insertId).toEqual(1); - // expect(res2.rows).toBe([]); - expect(res2.rows?.length).toEqual(0); - }); - - it("Insert", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - it("Casts booleans to ints correctly", async () => { - await db.execute(`SELECT ?`, [1]); - await db.execute(`SELECT ?`, [true]); - }); - - it("Insert and query with host objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.executeWithHostObjects( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - const queryRes = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(queryRes.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query without params", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.execute("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query with params", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.execute("SELECT * FROM User WHERE id = ?", [id]); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query with sqlite functions", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - // COUNT(*) - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const countRes = await db.execute("SELECT COUNT(*) as count FROM User"); - - expect(countRes.rows?.length).toEqual(1); - expect(countRes.rows?.[0]?.count).toEqual(1); - - // SUM(age) - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id2, name2, age2, networth2], - ); - - const sumRes = await db.execute("SELECT SUM(age) as sum FROM User;"); - - expect(sumRes.rows[0]!.sum).toEqual(age + age2); - - const maxRes = await db.execute("SELECT MAX(networth) as `max` FROM User;"); - const minRes = await db.execute("SELECT MIN(networth) as `min` FROM User;"); - const maxNetworth = Math.max(networth, networth2); - const minNetworth = Math.min(networth, networth2); - - expect(maxRes.rows[0]!.max).toEqual(maxNetworth); - expect(minRes.rows[0]!.min).toEqual(minNetworth); - }); - - it("Executes all the statements in a single string", async () => { - if (isLibsql() || isTurso()) { - return; - } - await db.execute( - `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; + const queryPromise = db.execute(longQuery); + + await sleep(50); + const startedAt = Date.now(); + db.close(); + const elapsedMs = Date.now() - startedAt; + + await queryPromise.catch(() => undefined); + expect(elapsedMs < 2000).toEqual(true); + + const cleanupDb = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + cleanupDb.delete(); + + // @ts-expect-error Prevent afterEach from deleting a closed handle. + db = null; + }); + + it("executeSync", () => { + const res = db.executeSync("SELECT 1"); + expect(res.rowsAffected).toEqual(0); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res2 = db.executeSync('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + + expect(res2.rowsAffected).toEqual(1); + expect(res2.insertId).toEqual(1); + // expect(res2.rows).toBe([]); + expect(res2.rows?.length).toEqual(0); + }); + + it("Insert", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + it("Casts booleans to ints correctly", async () => { + await db.execute(`SELECT ?`, [1]); + await db.execute(`SELECT ?`, [true]); + }); + + it("Insert and query with host objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.executeWithHostObjects( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + const queryRes = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(queryRes.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query without params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query with params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.execute("SELECT * FROM User WHERE id = ?", [id]); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query with sqlite functions", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + // COUNT(*) + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const countRes = await db.execute("SELECT COUNT(*) as count FROM User"); + + expect(countRes.rows?.length).toEqual(1); + expect(countRes.rows?.[0]?.count).toEqual(1); + + // SUM(age) + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id2, + name2, + age2, + networth2, + ]); + + const sumRes = await db.execute("SELECT SUM(age) as sum FROM User;"); + + expect(sumRes.rows[0]!.sum).toEqual(age + age2); + + const maxRes = await db.execute("SELECT MAX(networth) as `max` FROM User;"); + const minRes = await db.execute("SELECT MIN(networth) as `min` FROM User;"); + const maxNetworth = Math.max(networth, networth2); + const minNetworth = Math.min(networth, networth2); + + expect(maxRes.rows[0]!.max).toEqual(maxNetworth); + expect(minRes.rows[0]!.min).toEqual(minNetworth); + }); + + it("Executes all the statements in a single string", async () => { + if (isLibsql() || isTurso()) { + return; + } + await db.execute( + `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; CREATE TABLE T2 ( id INT PRIMARY KEY) STRICT;`, - ); - - const t1name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", - ); - - expect(t1name.rows[0]!.name).toEqual("T1"); - - const t2name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", - ); - - expect(t2name.rows[0]!.name).toEqual("T2"); - }); - - it("Failed insert", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.string(); - const networth = chance.string(); - try { - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - } catch (e: any) { - expect(typeof e).toEqual("object"); - - expect(!!e.message).toEqual(true); - } - }); - - it("Transaction, auto commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Transaction, manual commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - await tx.commit(); - }); - - const res = await db.execute("SELECT * FROM User"); - // console.log(res); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Transaction, executed in order", async () => { - const xs = 10; - const actual: unknown[] = []; - - // ARRANGE: Generate expected data - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - - // ACT: Start multiple transactions to upsert and select the same record - const promises = []; - for (let i = 1; i <= xs; i++) { - const promised = db.transaction(async (tx) => { - // ACT: Upsert statement to create record / increment the value - await tx.execute( - ` + ); + + const t1name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", + ); + + expect(t1name.rows[0]!.name).toEqual("T1"); + + const t2name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", + ); + + expect(t2name.rows[0]!.name).toEqual("T2"); + }); + + it("Failed insert", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.string(); + const networth = chance.string(); + try { + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + } catch (e: any) { + expect(typeof e).toEqual("object"); + + expect(!!e.message).toEqual(true); + } + }); + + it("Transaction, auto commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, manual commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + await tx.commit(); + }); + + const res = await db.execute("SELECT * FROM User"); + // console.log(res); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, executed in order", async () => { + const xs = 10; + const actual: unknown[] = []; + + // ARRANGE: Generate expected data + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + + // ACT: Start multiple transactions to upsert and select the same record + const promises = []; + for (let i = 1; i <= xs; i++) { + const promised = db.transaction(async (tx) => { + // ACT: Upsert statement to create record / increment the value + await tx.execute( + ` INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth]) SELECT ?, ?, ?, IFNULL(( @@ -476,426 +474,439 @@ describe("Queries tests", () => { WHERE [id] = ? ), 0) `, - [id, name, age, id], - ); - - // ACT: Select statement to get incremented value and store it for checking later - const results = await tx.execute( - "SELECT [networth] FROM [User] WHERE [id] = ?", - [id], - ); - - actual.push(results.rows[0]!.networth); - }); - - promises.push(promised); - } - - // ACT: Wait for all transactions to complete - await Promise.all(promises); - - // ASSERT: That the expected values where returned - const expected = Array(xs) - .fill(0) - .map((_, index) => index * 1000); - - expect(actual).toDeepEqual(expected); - }); - - it("Transaction, cannot execute after commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows.length).toEqual(0); - - await tx.commit(); - - try { - await tx.execute('SELECT * FROM "User"'); - } catch (e) { - expect(!!e).toEqual(true); - } - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Incorrect transaction, manual rollback", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - try { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (_e) { - await tx.rollback(); - } - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([]); - }); - - it("Correctly throws", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - try { - await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (e: any) { - expect(!!e).toEqual(true); - } - }); - - it("Rollback", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - await tx.rollback(); - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([]); - }); - }); - - it("Execute raw sync should return just an array of objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it("Transaction, rejects on callback error", async () => { - const promised = db.transaction(() => { - throw new Error("Error from callback"); - }); - - // ASSERT: should return a promise that eventually rejects - expect(typeof promised === "object"); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect((e as Error)?.message).toEqual("Error from callback"); - } - }); - - it("Transaction, rejects on invalid query", async () => { - const promised = db.transaction(async (tx) => { - await tx.execute("SELECT * FROM [tableThatDoesNotExist];"); - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true); - } - }); - - it("Transaction, handle async callback", async () => { - let ranCallback = false; - const promised = db.transaction(async (tx) => { - await new Promise((done) => { - setTimeout(() => done(), 50); - }); - tx.execute("SELECT * FROM User;"); - ranCallback = true; - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - await promised; - expect(ranCallback).toEqual(true); - }); - - it("executeBatch", async () => { - const id1 = chance.integer(); - const name1 = chance.name(); - const age1 = chance.integer(); - const networth1 = chance.floating(); - - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - const commands: SQLBatchTuple[] = [ - ['SELECT * FROM "User"', []], - ['SELECT * FROM "User"'], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - await db.executeBatch(commands); - - const res = await db.execute("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { id: id1, name: name1, age: age1, networth: networth1, nickname: null }, - { - id: id2, - name: name2, - age: age2, - networth: networth2, - nickname: null, - }, - ]); - }); - - it("Batch execute with BLOB", async () => { - const db = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - await db.execute("DROP TABLE IF EXISTS User;"); - await db.execute( - "CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;", - ); - const id1 = "1"; - const name1 = "name1"; - const age1 = 12; - const networth1 = new Uint8Array([1, 2, 3]); - - const id2 = "2"; - const name2 = "name2"; - const age2 = 17; - const networth2 = new Uint8Array([3, 2, 1]); - - const commands: SQLBatchTuple[] = [ - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: - await db.executeBatch(commands); - }); - - it("DumbHostObject allows to write known props", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.name = "quack_changed"; - - expect(res.rows[0]!.name).toEqual("quack_changed"); - }); - - it("DumbHostObject allows to write new props", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(res.rows!).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.myWeirdProp = "quack_changed"; - - expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); - }); - - it("Execute raw should return just an array of objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it("Create fts5 virtual table", async () => { - if (isTurso()) { - return; - } - - await db.execute( - "CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);", - ); - await db.execute("INSERT INTO fts5_table (name, content) VALUES(?, ?)", [ - "test", - "test content", - ]); - - const res = await db.execute("SELECT * FROM fts5_table"); - expect(res.rows).toDeepEqual([{ name: "test", content: "test content" }]); - }); - - it("Various queries", async () => { - await db.execute("SELECT 1 "); - await db.execute("SELECT 1 "); - await db.execute("SELECT 1; ", []); - await db.execute("SELECT ?; ", [1]); - }); - - it("Handles concurrent transactions correctly", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - const transaction1 = db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - const transaction2 = db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id + 1, name, age, networth], - ); - }); - - await Promise.all([transaction1, transaction2]); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows.length).toEqual(2); - }); - - it("Pragma user_version", () => { - const res = db.executeSync("PRAGMA user_version"); - expect(res.rows).toDeepEqual([{ user_version: 0 }]); - }); - - // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; - // if (sqliteVecEnabled) { - // it('sqlite-vec extension: vector similarity search', async () => { - // // Create a virtual table for storing vectors - // await db.execute(` - // CREATE VIRTUAL TABLE vec_items USING vec0( - // embedding FLOAT[8] - // ) - // `); - - // // Insert some sample vectors - // await db.execute(` - // INSERT INTO vec_items(rowid, embedding) - // VALUES - // (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), - // (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), - // (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), - // (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') - // `); - - // // Perform KNN query to find the 2 nearest neighbors - // const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; - // const result = await db.execute(` - // SELECT rowid, distance - // FROM vec_items - // WHERE embedding MATCH ? - // ORDER BY distance - // LIMIT 2 - // `, [queryVector]); - - // // Verify results - // expect(result.rows.length).toEqual(2); - // expect(result.rows[0]!.rowid).toEqual(2); - // expect(result.rows[1]!.rowid).toEqual(1); - - // // Verify distances are positive numbers - // const distance0 = result.rows[0]!.distance as number; - // const distance1 = result.rows[1]!.distance as number; - // expect(typeof distance0).toEqual('number'); - // expect(distance0 > 0).toBeTruthy(); - // expect(distance1 > 0).toBeTruthy(); - // }); - // } + [id, name, age, id], + ); + + // ACT: Select statement to get incremented value and store it for checking later + const results = await tx.execute("SELECT [networth] FROM [User] WHERE [id] = ?", [id]); + + actual.push(results.rows[0]!.networth); + }); + + promises.push(promised); + } + + // ACT: Wait for all transactions to complete + await Promise.all(promises); + + // ASSERT: That the expected values where returned + const expected = Array(xs) + .fill(0) + .map((_, index) => index * 1000); + + expect(actual).toDeepEqual(expected); + }); + + it("Transaction, cannot execute after commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows.length).toEqual(0); + + await tx.commit(); + + try { + await tx.execute('SELECT * FROM "User"'); + } catch (e) { + expect(!!e).toEqual(true); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Incorrect transaction, manual rollback", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + try { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + } catch (_e) { + await tx.rollback(); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + + it("Correctly throws", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + try { + await db.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + } catch (e: any) { + expect(!!e).toEqual(true); + } + }); + + it("Rollback", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + await tx.rollback(); + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + }); + + it("Execute raw sync should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Transaction, rejects on callback error", async () => { + const promised = db.transaction(() => { + throw new Error("Error from callback"); + }); + + // ASSERT: should return a promise that eventually rejects + expect(typeof promised === "object"); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect((e as Error)?.message).toEqual("Error from callback"); + } + }); + + it("Transaction, rejects on invalid query", async () => { + const promised = db.transaction(async (tx) => { + await tx.execute("SELECT * FROM [tableThatDoesNotExist];"); + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true); + } + }); + + it("Transaction, handle async callback", async () => { + let ranCallback = false; + const promised = db.transaction(async (tx) => { + await new Promise((done) => { + setTimeout(() => done(), 50); + }); + tx.execute("SELECT * FROM User;"); + ranCallback = true; + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + await promised; + expect(ranCallback).toEqual(true); + }); + + it("executeBatch", async () => { + const id1 = chance.integer(); + const name1 = chance.name(); + const age1 = chance.integer(); + const networth1 = chance.floating(); + + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + const commands: SQLBatchTuple[] = [ + ['SELECT * FROM "User"', []], + ['SELECT * FROM "User"'], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + await db.executeBatch(commands); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { id: id1, name: name1, age: age1, networth: networth1, nickname: null }, + { + id: id2, + name: name2, + age: age2, + networth: networth2, + nickname: null, + }, + ]); + }); + + it("Batch execute with BLOB", async () => { + const db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;", + ); + const id1 = "1"; + const name1 = "name1"; + const age1 = 12; + const networth1 = new Uint8Array([1, 2, 3]); + + const id2 = "2"; + const name2 = "name2"; + const age2 = 17; + const networth2 = new Uint8Array([3, 2, 1]); + + const commands: SQLBatchTuple[] = [ + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: + await db.executeBatch(commands); + }); + + it("DumbHostObject allows to write known props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.name = "quack_changed"; + + expect(res.rows[0]!.name).toEqual("quack_changed"); + }); + + it("DumbHostObject allows to write new props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.rows!).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.myWeirdProp = "quack_changed"; + + expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); + }); + + it("Execute raw should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Create fts5 virtual table", async () => { + if (isTurso()) { + return; + } + + await db.execute("CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);"); + await db.execute("INSERT INTO fts5_table (name, content) VALUES(?, ?)", [ + "test", + "test content", + ]); + + const res = await db.execute("SELECT * FROM fts5_table"); + expect(res.rows).toDeepEqual([{ name: "test", content: "test content" }]); + }); + + it("Various queries", async () => { + await db.execute("SELECT 1 "); + await db.execute("SELECT 1 "); + await db.execute("SELECT 1; ", []); + await db.execute("SELECT ?; ", [1]); + }); + + it("Handles concurrent transactions correctly", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + const transaction1 = db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + }); + + const transaction2 = db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id + 1, + name, + age, + networth, + ]); + }); + + await Promise.all([transaction1, transaction2]); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows.length).toEqual(2); + }); + + it("Pragma user_version", () => { + const res = db.executeSync("PRAGMA user_version"); + expect(res.rows).toDeepEqual([{ user_version: 0 }]); + }); + + // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; + // if (sqliteVecEnabled) { + // it('sqlite-vec extension: vector similarity search', async () => { + // // Create a virtual table for storing vectors + // await db.execute(` + // CREATE VIRTUAL TABLE vec_items USING vec0( + // embedding FLOAT[8] + // ) + // `); + + // // Insert some sample vectors + // await db.execute(` + // INSERT INTO vec_items(rowid, embedding) + // VALUES + // (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), + // (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), + // (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), + // (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') + // `); + + // // Perform KNN query to find the 2 nearest neighbors + // const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; + // const result = await db.execute(` + // SELECT rowid, distance + // FROM vec_items + // WHERE embedding MATCH ? + // ORDER BY distance + // LIMIT 2 + // `, [queryVector]); + + // // Verify results + // expect(result.rows.length).toEqual(2); + // expect(result.rows[0]!.rowid).toEqual(2); + // expect(result.rows[1]!.rowid).toEqual(1); + + // // Verify distances are positive numbers + // const distance0 = result.rows[0]!.distance as number; + // const distance1 = result.rows[1]!.distance as number; + // expect(typeof distance0).toEqual('number'); + // expect(distance0 > 0).toBeTruthy(); + // expect(distance1 > 0).toBeTruthy(); + // }); + // } }); diff --git a/package.json b/package.json index bc4558a1..79852019 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,10 @@ "scripts": { "test:node": "yarn workspace node test", "example": "yarn workspace op_sqlite_example", + "lint": "oxlint .", + "lint:fix": "oxlint --fix .", + "format": "oxfmt .", + "format:check": "oxfmt --check .", "typecheck": "tsc", "prepare": "bob build && yarn build:node", "build:node": "yarn workspace node build", @@ -66,13 +70,14 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { - "@biomejs/biome": "^2.4.10", "@sqlite.org/sqlite-wasm": "^3.51.2-build8", "@types/better-sqlite3": "^7.6.13", "@types/jest": "^30.0.0", "better-sqlite3": "^12.5.0", "clang-format": "^1.8.0", "jest": "^29.5.0", + "oxfmt": "^0.54.0", + "oxlint": "^1.69.0", "react": "19.2.3", "react-native": "0.86.0", "react-native-builder-bob": "^0.40.15", diff --git a/src/functions.ts b/src/functions.ts index ca88b238..4153bdc4 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -1,319 +1,244 @@ import { NativeModules, Platform } from "react-native"; import type { - _InternalDB, - _PendingTransaction, - BatchQueryResult, - DB, - DBParams, - OPSQLiteProxy, - QueryResult, - Scalar, - SQLBatchTuple, - Transaction, + _InternalDB, + BatchQueryResult, + DB, + DBParams, + OPSQLiteProxy, + QueryResult, + Scalar, + SQLBatchTuple, + Transaction, } from "./types"; declare global { - var __OPSQLiteProxy: object | undefined; + var __OPSQLiteProxy: object | undefined; } if (global.__OPSQLiteProxy == null) { - if (NativeModules.OPSQLite == null) { - throw new Error( - "Base module not found. Did you do a pod install/clear the gradle cache?", - ); - } - - // Call the synchronous blocking install() function - const installed = NativeModules.OPSQLite.install(); - if (!installed) { - throw new Error( - `Failed to install op-sqlite: The native OPSQLite Module could not be installed! Looks like something went wrong when installing JSI bindings, check the native logs for more info`, - ); - } - - // Check again if the constructor now exists. If not, throw an error. - if (global.__OPSQLiteProxy == null) { - throw new Error( - "OPSqlite native object is not available. Something is wrong. Check the native logs for more information.", - ); - } + if (NativeModules.OPSQLite == null) { + throw new Error("Base module not found. Did you do a pod install/clear the gradle cache?"); + } + + // Call the synchronous blocking install() function + const installed = NativeModules.OPSQLite.install(); + if (!installed) { + throw new Error( + `Failed to install op-sqlite: The native OPSQLite Module could not be installed! Looks like something went wrong when installing JSI bindings, check the native logs for more info`, + ); + } + + // Check again if the constructor now exists. If not, throw an error. + if (global.__OPSQLiteProxy == null) { + throw new Error( + "OPSqlite native object is not available. Something is wrong. Check the native logs for more information.", + ); + } } const proxy = global.__OPSQLiteProxy; export const OPSQLite = proxy as OPSQLiteProxy; function enhanceDB(db: _InternalDB, options: DBParams): DB { - const lock = { - queue: [] as _PendingTransaction[], - inProgress: false, - }; - - const startNextTransaction = () => { - if (lock.inProgress) { - // Transaction is already in process bail out - return; - } - - if (lock.queue.length) { - lock.inProgress = true; - const tx = lock.queue.shift(); - - if (!tx) { - throw new Error("Could not get a operation on database"); - } - - setImmediate(() => { - tx.start(); - }); - } - }; - - // spreading the object does not work with HostObjects (db) - // We need to manually assign the fields - const enhancedDb = { - delete: db.delete, - attach: db.attach, - detach: db.detach, - loadFile: db.loadFile, - updateHook: db.updateHook, - commitHook: db.commitHook, - rollbackHook: db.rollbackHook, - loadExtension: db.loadExtension, - getDbPath: db.getDbPath, - reactiveExecute: db.reactiveExecute, - sync: db.sync, - setReservedBytes: db.setReservedBytes, - getReservedBytes: db.getReservedBytes, - close: db.close, - interrupt: db.interrupt, - closeAsync: async () => { - db.close(); - }, - flushPendingReactiveQueries: db.flushPendingReactiveQueries, - executeBatch: async ( - commands: SQLBatchTuple[], - ): Promise => { - async function run() { - try { - enhancedDb.executeSync("BEGIN TRANSACTION;"); - - const res = await db.executeBatch(commands as any[]); - - enhancedDb.executeSync("COMMIT;"); - - await db.flushPendingReactiveQueries(); - - return res; - } catch (executionError) { - try { - enhancedDb.executeSync("ROLLBACK;"); - } catch (rollbackError) { - throw rollbackError; - } - - throw executionError; - } finally { - lock.inProgress = false; - startNextTransaction(); - } - } - - return await new Promise((resolve, reject) => { - const tx: _PendingTransaction = { - start: () => { - run().then(resolve).catch(reject); - }, - }; - - lock.queue.push(tx); - startNextTransaction(); - }); - }, - executeWithHostObjects: async ( - query: string, - params?: Scalar[], - ): Promise => { - return params - ? await db.executeWithHostObjects(query, params) - : await db.executeWithHostObjects(query); - }, - executeRaw: async (query: string, params?: Scalar[]) => { - return db.executeRaw(query, params as Scalar[]); - }, - executeRawSync: (query: string, params?: Scalar[]) => { - return db.executeRawSync(query, params as Scalar[]); - }, - // Wrapper for executeRaw, drizzleORM uses this function - // at some point I changed the API but they did not pin their dependency to a specific version - // so re-inserting this so it starts working again - executeRawAsync: async (query: string, params?: Scalar[]) => { - return db.executeRaw(query, params as Scalar[]); - }, - executeSync: (query: string, params?: Scalar[]): QueryResult => { - let res = params ? db.executeSync(query, params) : db.executeSync(query); - - if (!res.rows) { - const rows: Record[] = []; - for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { - const row: Record = {}; - const rawRow = res.rawRows![i]!; - for (let j = 0; j < res.columnNames!.length; j++) { - const columnName = res.columnNames![j]!; - const value = rawRow[j]!; - - row[columnName] = value; - } - rows.push(row); - } - - delete res.rawRows; - - res = { - ...res, - rows, - }; - } - - return res; - }, - executeAsync: async ( - query: string, - params?: Scalar[] | undefined, - ): Promise => { - return db.execute(query, params); - }, - execute: async ( - query: string, - params?: Scalar[] | undefined, - ): Promise => { - let res = params ? await db.execute(query, params) : await db.execute(query); - - if (!res.rows) { - const rows: Record[] = []; - for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { - const row: Record = {}; - const rawRow = res.rawRows![i]!; - for (let j = 0; j < res.columnNames!.length; j++) { - const columnName = res.columnNames![j]!; - const value = rawRow[j]!; - - row[columnName] = value; - } - rows.push(row); - } - - delete res.rawRows; - - res = { - ...res, - rows, - }; - } - - return res; - }, - prepareStatement: (query: string) => { - const stmt = db.prepareStatement(query); - - return { - bindSync: (params: Scalar[]) => { - stmt.bindSync(params); - }, - bind: async (params: Scalar[]) => { - await stmt.bind(params); - }, - execute: stmt.execute, - }; - }, - transaction: async ( - fn: (tx: Transaction) => Promise, - ): Promise => { - let isFinalized = false; - - const execute = async (query: string, params?: Scalar[]) => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction`, - ); - } - return await enhancedDb.execute(query, params); - }; - - const commit = async (): Promise => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction`, - ); - } - const result = enhancedDb.executeSync("COMMIT;"); - - await db.flushPendingReactiveQueries(); - - isFinalized = true; - return result; - }; - - const rollback = (): QueryResult => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction`, - ); - } - const result = enhancedDb.executeSync("ROLLBACK;"); - isFinalized = true; - return result; - }; - - async function run() { - try { - enhancedDb.executeSync("BEGIN TRANSACTION;"); - - await fn({ - commit, - execute, - rollback, - }); - - if (!isFinalized) { - commit(); - } - } catch (executionError) { - if (!isFinalized) { - try { - rollback(); - } catch (rollbackError) { - throw rollbackError; - } - } - - throw executionError; - } finally { - lock.inProgress = false; - isFinalized = false; - startNextTransaction(); - } - } - - return await new Promise((resolve, reject) => { - const tx: _PendingTransaction = { - start: () => { - run().then(resolve).catch(reject); - }, - }; - - lock.queue.push(tx); - startNextTransaction(); - }); - }, - }; - - return enhancedDb; + let hasReactiveQueries = false; + + const flushReactiveIfNeeded = async () => { + if (!hasReactiveQueries) { + return; + } + + await db.flushPendingReactiveQueries(); + }; + + // spreading the object does not work with HostObjects (db) + // We need to manually assign the fields + const enhancedDb = { + delete: db.delete, + attach: db.attach, + detach: db.detach, + loadFile: db.loadFile, + updateHook: db.updateHook, + commitHook: db.commitHook, + rollbackHook: db.rollbackHook, + loadExtension: db.loadExtension, + getDbPath: db.getDbPath, + reactiveExecute: (params: Parameters<_InternalDB["reactiveExecute"]>[0]) => { + hasReactiveQueries = true; + return db.reactiveExecute(params); + }, + sync: db.sync, + setReservedBytes: db.setReservedBytes, + getReservedBytes: db.getReservedBytes, + close: db.close, + interrupt: db.interrupt, + closeAsync: async () => { + db.close(); + }, + flushPendingReactiveQueries: db.flushPendingReactiveQueries, + executeBatch: async (commands: SQLBatchTuple[]): Promise => { + const res = await db.executeBatch(commands as any[]); + await flushReactiveIfNeeded(); + return res; + }, + executeWithHostObjects: async (query: string, params?: Scalar[]): Promise => { + return params + ? await db.executeWithHostObjects(query, params) + : await db.executeWithHostObjects(query); + }, + executeRaw: async (query: string, params?: Scalar[]) => { + return db.executeRaw(query, params as Scalar[]); + }, + executeRawSync: (query: string, params?: Scalar[]) => { + return db.executeRawSync(query, params as Scalar[]); + }, + // Wrapper for executeRaw, drizzleORM uses this function + // at some point I changed the API but they did not pin their dependency to a specific version + // so re-inserting this so it starts working again + executeRawAsync: async (query: string, params?: Scalar[]) => { + return db.executeRaw(query, params as Scalar[]); + }, + executeSync: (query: string, params?: Scalar[]): QueryResult => { + let res = params ? db.executeSync(query, params) : db.executeSync(query); + + if (!res.rows) { + const rows: Record[] = []; + for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { + const row: Record = {}; + const rawRow = res.rawRows![i]!; + for (let j = 0; j < res.columnNames!.length; j++) { + const columnName = res.columnNames![j]!; + const value = rawRow[j]!; + + row[columnName] = value; + } + rows.push(row); + } + + delete res.rawRows; + + res = { + ...res, + rows, + }; + } + + return res; + }, + executeAsync: async (query: string, params?: Scalar[] | undefined): Promise => { + return db.execute(query, params); + }, + execute: async (query: string, params?: Scalar[] | undefined): Promise => { + let res = params ? await db.execute(query, params) : await db.execute(query); + + if (!res.rows) { + const rows: Record[] = []; + for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { + const row: Record = {}; + const rawRow = res.rawRows![i]!; + for (let j = 0; j < res.columnNames!.length; j++) { + const columnName = res.columnNames![j]!; + const value = rawRow[j]!; + + row[columnName] = value; + } + rows.push(row); + } + + delete res.rawRows; + + res = { + ...res, + rows, + }; + } + + return res; + }, + prepareStatement: (query: string) => { + const stmt = db.prepareStatement(query); + + return { + bindSync: (params: Scalar[]) => { + stmt.bindSync(params); + }, + bind: async (params: Scalar[]) => { + await stmt.bind(params); + }, + execute: stmt.execute, + }; + }, + transaction: async (fn: (tx: Transaction) => Promise): Promise => { + await db.acquireTransactionLock(); + + try { + let isFinalized = false; + + const execute = async (query: string, params?: Scalar[]) => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + return await enhancedDb.execute(query, params); + }; + + const commit = async (): Promise => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + const result = db.executeSync("COMMIT;"); + + await flushReactiveIfNeeded(); + + isFinalized = true; + return result; + }; + + const rollback = (): QueryResult => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + const result = db.executeSync("ROLLBACK"); + isFinalized = true; + return result; + }; + + try { + db.executeSync("BEGIN TRANSACTION"); + + await fn({ + commit, + execute, + rollback, + }); + + if (!isFinalized) { + await commit(); + } + } catch (executionError) { + if (!isFinalized) { + rollback(); + } + + throw executionError; + } + } finally { + db.releaseTransactionLock(); + } + }, + }; + + return enhancedDb; } /** @@ -321,25 +246,23 @@ function enhanceDB(db: _InternalDB, options: DBParams): DB { * Requires libsql or turso backend to be enabled in package.json. */ export const openSync = (params: { - url: string; - authToken: string; - name: string; - location?: string; - libsqlSyncInterval?: number; - libsqlOffline?: boolean; - encryptionKey?: string; - remoteEncryptionKey?: string; + url: string; + authToken: string; + name: string; + location?: string; + libsqlSyncInterval?: number; + libsqlOffline?: boolean; + encryptionKey?: string; + remoteEncryptionKey?: string; }): DB => { - if (!isLibsql() && !isTurso()) { - throw new Error( - "This function is only available for libsql or turso backends", - ); - } + if (!isLibsql() && !isTurso()) { + throw new Error("This function is only available for libsql or turso backends"); + } - const db = OPSQLite.openSync(params); - const enhancedDb = enhanceDB(db, params); + const db = OPSQLite.openSync(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; }; /** @@ -347,38 +270,32 @@ export const openSync = (params: { * Requires libsql or turso backend to be enabled in package.json. */ export const openRemote = (params: { url: string; authToken: string }): DB => { - if (!isLibsql() && !isTurso()) { - throw new Error( - "This function is only available for libsql or turso backends", - ); - } + if (!isLibsql() && !isTurso()) { + throw new Error("This function is only available for libsql or turso backends"); + } - const db = OPSQLite.openRemote(params); - const enhancedDb = enhanceDB(db, params); + const db = OPSQLite.openRemote(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; }; /** * Open a connection to a local sqlite or sqlcipher database * If you want libsql remote or sync connections, use openSync or openRemote */ -export const open = (params: { - name: string; - location?: string; - encryptionKey?: string; -}): DB => { - if (params.location?.startsWith("file://")) { - console.warn( - "[op-sqlite] You are passing a path with 'file://' prefix, it's automatically removed", - ); - params.location = params.location.substring(7); - } - - const db = OPSQLite.open(params); - const enhancedDb = enhanceDB(db, params); - - return enhancedDb; +export const open = (params: { name: string; location?: string; encryptionKey?: string }): DB => { + if (params.location?.startsWith("file://")) { + console.warn( + "[op-sqlite] You are passing a path with 'file://' prefix, it's automatically removed", + ); + params.location = params.location.substring(7); + } + + const db = OPSQLite.open(params); + const enhancedDb = enhanceDB(db, params); + + return enhancedDb; }; /** @@ -386,11 +303,11 @@ export const open = (params: { * Useful for cross-platform code that also targets web where openAsync() is required. */ export const openAsync = async (params: { - name: string; - location?: string; - encryptionKey?: string; + name: string; + location?: string; + encryptionKey?: string; }): Promise => { - return open(params); + return open(params); }; /** @@ -401,11 +318,11 @@ export const openAsync = async (params: { * @returns promise, rejects if failed to move the database, resolves if the operation was successful */ export const moveAssetsDatabase = async (args: { - filename: string; - path?: string; - overwrite?: boolean; + filename: string; + path?: string; + overwrite?: boolean; }): Promise => { - return NativeModules.OPSQLite.moveAssetsDatabase(args); + return NativeModules.OPSQLite.moveAssetsDatabase(args); }; /** @@ -417,27 +334,27 @@ export const moveAssetsDatabase = async (args: { * @returns */ export const getDylibPath = (bundle: string, name: string): string => { - return NativeModules.OPSQLite.getDylibPath(bundle, name); + return NativeModules.OPSQLite.getDylibPath(bundle, name); }; export const isSQLCipher = (): boolean => { - return OPSQLite.isSQLCipher(); + return OPSQLite.isSQLCipher(); }; export const isLibsql = (): boolean => { - return OPSQLite.isLibsql(); + return OPSQLite.isLibsql(); }; export const isTurso = (): boolean => { - return OPSQLite.isTurso(); + return OPSQLite.isTurso(); }; export const isIOSEmbedded = (): boolean => { - if (Platform.OS !== "ios") { - return false; - } + if (Platform.OS !== "ios") { + return false; + } - return OPSQLite.isIOSEmbedded(); + return OPSQLite.isIOSEmbedded(); }; /** diff --git a/src/types.ts b/src/types.ts index 2174e21c..a00f07b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,4 @@ -export type Scalar = - | string - | number - | boolean - | null - | ArrayBuffer - | ArrayBufferView; +export type Scalar = string | number | boolean | null | ArrayBuffer | ArrayBufferView; /** * Object returned by SQL Query executions { @@ -17,17 +11,17 @@ export type Scalar = * @interface QueryResult */ export type QueryResult = { - insertId?: number; - rowsAffected: number; - res?: any[]; - rows: Array>; - // An array of intermediate results, just values without column names - rawRows?: Scalar[][]; - columnNames?: string[]; - /** - * Query metadata, available only for select query results - */ - metadata?: ColumnMetadata[]; + insertId?: number; + rowsAffected: number; + res?: any[]; + rows: Array>; + // An array of intermediate results, just values without column names + rawRows?: Scalar[][]; + columnNames?: string[]; + /** + * Query metadata, available only for select query results + */ + metadata?: ColumnMetadata[]; }; /** @@ -35,13 +29,13 @@ export type QueryResult = { * Describes some information about columns fetched by the query */ export type ColumnMetadata = { - /** The name used for this column for this result set */ - name: string; - /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ - type: string; - /** - * The index for this column for this result set*/ - index: number; + /** The name used for this column for this result set */ + name: string; + /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ + type: string; + /** + * The index for this column for this result set*/ + index: number; }; /** @@ -50,10 +44,7 @@ export type ColumnMetadata = { * If a single query must be executed many times with different arguments, its preferred * to declare it a single time, and use an array of array parameters. */ -export type SQLBatchTuple = - | [string] - | [string, Scalar[]] - | [string, Scalar[][]]; +export type SQLBatchTuple = [string] | [string, Scalar[]] | [string, Scalar[][]]; export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; @@ -63,7 +54,7 @@ export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; * rowsAffected: Number of affected rows if status == 0 */ export type BatchQueryResult = { - rowsAffected?: number; + rowsAffected?: number; }; /** @@ -71,267 +62,254 @@ export type BatchQueryResult = { * Similar to BatchQueryResult */ export type FileLoadResult = BatchQueryResult & { - commands?: number; + commands?: number; }; export type Transaction = { - commit: () => Promise; - execute: (query: string, params?: Scalar[]) => Promise; - rollback: () => QueryResult; + commit: () => Promise; + execute: (query: string, params?: Scalar[]) => Promise; + rollback: () => QueryResult; }; export type _PendingTransaction = { - /* - * The start function should not throw or return a promise because the - * queue just calls it and does not monitor for failures or completions. - * - * It should catch any errors and call the resolve or reject of the wrapping - * promise when complete. - * - * It should also automatically commit or rollback the transaction if needed - */ - start: () => void; + /* + * The start function should not throw or return a promise because the + * queue just calls it and does not monitor for failures or completions. + * + * It should catch any errors and call the resolve or reject of the wrapping + * promise when complete. + * + * It should also automatically commit or rollback the transaction if needed + */ + start: () => void; }; export type PreparedStatement = { - bind: (params: any[]) => Promise; - bindSync: (params: any[]) => void; - execute: () => Promise; + bind: (params: any[]) => Promise; + bindSync: (params: any[]) => void; + execute: () => Promise; }; export type _InternalDB = { - close: () => void; - closeAsync?: () => Promise; - interrupt: () => void; - delete: () => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - transaction: (fn: (tx: Transaction) => Promise) => Promise; - executeSync: (query: string, params?: Scalar[]) => QueryResult; - execute: (query: string, params?: Scalar[]) => Promise; - executeWithHostObjects: ( - query: string, - params?: Scalar[], - ) => Promise; - executeBatch: (commands: SQLBatchTuple[]) => Promise; - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null, - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - prepareStatement: (query: string) => PreparedStatement; - loadExtension: (path: string, entryPoint?: string) => void; - executeRaw: (query: string, params?: Scalar[]) => Promise; - executeRawSync: (query: string, params?: Scalar[]) => any[]; - getDbPath: (location?: string) => string; - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync?: () => Promise; + interrupt: () => void; + delete: () => void; + attach: (params: { secondaryDbFileName: string; alias: string; location?: string }) => void; + detach: (alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise) => Promise; + acquireTransactionLock: () => Promise; + releaseTransactionLock: () => void; + beginTransaction?: () => QueryResult; + commitTransaction?: () => QueryResult; + rollbackTransaction?: () => QueryResult; + executeSync: (query: string, params?: Scalar[]) => QueryResult; + execute: (query: string, params?: Scalar[]) => Promise; + executeWithHostObjects: (query: string, params?: Scalar[]) => Promise; + executeBatch: (commands: SQLBatchTuple[]) => Promise; + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise; + executeRawSync: (query: string, params?: Scalar[]) => any[]; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + flushPendingReactiveQueries: () => Promise; }; export type DB = { - close: () => void; - closeAsync: () => Promise; - /** - * Aborts any pending database operation on this connection. - * - * Calls SQLite's native sqlite3_interrupt(). Safe to call from a thread - * different from the one running the operation. An interrupted operation - * returns SQLITE_INTERRUPT and any in-flight transaction is rolled back. - */ - interrupt: () => void; - delete: () => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - /** - * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes - * - * You need to use this if you are using reactive queries for the queries to fire after the transaction is done - */ - transaction: (fn: (tx: Transaction) => Promise) => Promise; - /** - * Sync version of the execute function - * It will block the JS thread and therefore your UI and should be used with caution - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * @param query - * @param params - * @returns QueryResult - */ - executeSync: (query: string, params?: Scalar[]) => QueryResult; - /** - * Basic query execution function, it is async don't forget to await it - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * await db.execute('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` - * - * @param query string of your SQL query - * @param params a list of parameters to bind to the query, if any - * @returns Promise with the result of the query - */ - execute: (query: string, params?: Scalar[]) => Promise; - /** - * Similar to the execute function but returns the response in HostObjects - * Read more about HostObjects in the documentation and their pitfalls - * - * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields - * as the conversion is done the moment you access any field - * @param query - * @param params - * @returns - */ - executeWithHostObjects: ( - query: string, - params?: Scalar[], - ) => Promise; - /** - * Executes all the queries in the params inside a single transaction - * - * It's faster than executing single queries as data is sent to the native side only once - * @param commands - * @returns Promise - */ - executeBatch: (commands: SQLBatchTuple[]) => Promise; - /** - * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved - */ - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null, - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - /** - * Constructs a prepared statement from the query string - * The statement can be re-bound with parameters and executed - * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) - * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound - * but the query itself is already optimized - * - * @param query string of your SQL query - * @returns Prepared statement object - */ - prepareStatement: (query: string) => PreparedStatement; - /** - * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions - */ - loadExtension: (path: string, entryPoint?: string) => void; - /** - * Same as `execute` except the results are not returned in objects but rather in arrays with just the values and not the keys - * It will be faster since a lot of repeated work is skipped and only the values you care about are returned - */ - executeRaw: (query: string, params?: Scalar[]) => Promise; - /** - * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution - * It will return an array of arrays with just the values and not the keys - */ - executeRawSync: (query: string, params?: Scalar[]) => any[]; - /** - * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices - */ - getDbPath: (location?: string) => string; - /** - * Reactive execution of queries when data is written to the database. Check the docs for how to use them. - */ - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - /** This function is only available for libsql. - * Allows to trigger a sync the database with it's remote replica - * In order for this function to work you need to use openSync or openRemote functions - * with libsql: true in the package.json - * - * The database is hosted in turso - **/ - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - /** - * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own - * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not - * use the db.transaction method internally - * @returns void - */ - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync: () => Promise; + /** + * Aborts any pending database operation on this connection. + * + * Calls SQLite's native sqlite3_interrupt(). Safe to call from a thread + * different from the one running the operation. An interrupted operation + * returns SQLITE_INTERRUPT and any in-flight transaction is rolled back. + */ + interrupt: () => void; + delete: () => void; + attach: (params: { secondaryDbFileName: string; alias: string; location?: string }) => void; + detach: (alias: string) => void; + /** + * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes + * + * You need to use this if you are using reactive queries for the queries to fire after the transaction is done + */ + transaction: (fn: (tx: Transaction) => Promise) => Promise; + /** + * Sync version of the execute function + * It will block the JS thread and therefore your UI and should be used with caution + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * @param query + * @param params + * @returns QueryResult + */ + executeSync: (query: string, params?: Scalar[]) => QueryResult; + /** + * Basic query execution function, it is async don't forget to await it + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * await db.execute('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` + * + * @param query string of your SQL query + * @param params a list of parameters to bind to the query, if any + * @returns Promise with the result of the query + */ + execute: (query: string, params?: Scalar[]) => Promise; + /** + * Similar to the execute function but returns the response in HostObjects + * Read more about HostObjects in the documentation and their pitfalls + * + * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields + * as the conversion is done the moment you access any field + * @param query + * @param params + * @returns + */ + executeWithHostObjects: (query: string, params?: Scalar[]) => Promise; + /** + * Executes all the queries in the params inside a single transaction + * + * It's faster than executing single queries as data is sent to the native side only once + * @param commands + * @returns Promise + */ + executeBatch: (commands: SQLBatchTuple[]) => Promise; + /** + * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved + */ + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + /** + * Constructs a prepared statement from the query string + * The statement can be re-bound with parameters and executed + * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) + * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound + * but the query itself is already optimized + * + * @param query string of your SQL query + * @returns Prepared statement object + */ + prepareStatement: (query: string) => PreparedStatement; + /** + * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions + */ + loadExtension: (path: string, entryPoint?: string) => void; + /** + * Same as `execute` except the results are not returned in objects but rather in arrays with just the values and not the keys + * It will be faster since a lot of repeated work is skipped and only the values you care about are returned + */ + executeRaw: (query: string, params?: Scalar[]) => Promise; + /** + * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution + * It will return an array of arrays with just the values and not the keys + */ + executeRawSync: (query: string, params?: Scalar[]) => any[]; + /** + * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices + */ + getDbPath: (location?: string) => string; + /** + * Reactive execution of queries when data is written to the database. Check the docs for how to use them. + */ + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + /** This function is only available for libsql. + * Allows to trigger a sync the database with it's remote replica + * In order for this function to work you need to use openSync or openRemote functions + * with libsql: true in the package.json + * + * The database is hosted in turso + **/ + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + /** + * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own + * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not + * use the db.transaction method internally + * @returns void + */ + flushPendingReactiveQueries: () => Promise; }; export type DBParams = { - url?: string; - authToken?: string; - name?: string; - location?: string; - syncInterval?: number; + url?: string; + authToken?: string; + name?: string; + location?: string; + syncInterval?: number; }; export type OPSQLiteProxy = { - open: (options: { - name: string; - location?: string; - encryptionKey?: string; - }) => _InternalDB; - openRemote: (options: { url: string; authToken: string }) => _InternalDB; - openSync: (options: DBParams) => _InternalDB; - isSQLCipher: () => boolean; - isLibsql: () => boolean; - isTurso: () => boolean; - isIOSEmbedded: () => boolean; + open: (options: { name: string; location?: string; encryptionKey?: string }) => _InternalDB; + openRemote: (options: { url: string; authToken: string }) => _InternalDB; + openSync: (options: DBParams) => _InternalDB; + isSQLCipher: () => boolean; + isLibsql: () => boolean; + isTurso: () => boolean; + isIOSEmbedded: () => boolean; }; diff --git a/yarn.lock b/yarn.lock index f4e6b0df..9e8706c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,97 +1523,6 @@ __metadata: languageName: node linkType: hard -"@biomejs/biome@npm:^2.4.10": - version: 2.4.10 - resolution: "@biomejs/biome@npm:2.4.10" - dependencies: - "@biomejs/cli-darwin-arm64": "npm:2.4.10" - "@biomejs/cli-darwin-x64": "npm:2.4.10" - "@biomejs/cli-linux-arm64": "npm:2.4.10" - "@biomejs/cli-linux-arm64-musl": "npm:2.4.10" - "@biomejs/cli-linux-x64": "npm:2.4.10" - "@biomejs/cli-linux-x64-musl": "npm:2.4.10" - "@biomejs/cli-win32-arm64": "npm:2.4.10" - "@biomejs/cli-win32-x64": "npm:2.4.10" - dependenciesMeta: - "@biomejs/cli-darwin-arm64": - optional: true - "@biomejs/cli-darwin-x64": - optional: true - "@biomejs/cli-linux-arm64": - optional: true - "@biomejs/cli-linux-arm64-musl": - optional: true - "@biomejs/cli-linux-x64": - optional: true - "@biomejs/cli-linux-x64-musl": - optional: true - "@biomejs/cli-win32-arm64": - optional: true - "@biomejs/cli-win32-x64": - optional: true - bin: - biome: bin/biome - checksum: 10c0/80d10d5e6fa41a24efb9020ee73b79b0aca46942b55ea96e880c3bb45ea14c71e49fb1be9f134bee23b2d940bb8cad51a70351ca051e09a43613018dba693bd6 - languageName: node - linkType: hard - -"@biomejs/cli-darwin-arm64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-darwin-arm64@npm:2.4.10" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@biomejs/cli-darwin-x64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-darwin-x64@npm:2.4.10" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@biomejs/cli-linux-arm64-musl@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-linux-arm64-musl@npm:2.4.10" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@biomejs/cli-linux-arm64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-linux-arm64@npm:2.4.10" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@biomejs/cli-linux-x64-musl@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-linux-x64-musl@npm:2.4.10" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@biomejs/cli-linux-x64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-linux-x64@npm:2.4.10" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@biomejs/cli-win32-arm64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-win32-arm64@npm:2.4.10" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@biomejs/cli-win32-x64@npm:2.4.10": - version: 2.4.10 - resolution: "@biomejs/cli-win32-x64@npm:2.4.10" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.27.7": version: 0.27.7 resolution: "@esbuild/aix-ppc64@npm:0.27.7" @@ -2265,13 +2174,14 @@ __metadata: version: 0.0.0-use.local resolution: "@op-engineering/op-sqlite@workspace:." dependencies: - "@biomejs/biome": "npm:^2.4.10" "@sqlite.org/sqlite-wasm": "npm:^3.51.2-build8" "@types/better-sqlite3": "npm:^7.6.13" "@types/jest": "npm:^30.0.0" better-sqlite3: "npm:^12.5.0" clang-format: "npm:^1.8.0" jest: "npm:^29.5.0" + oxfmt: "npm:^0.54.0" + oxlint: "npm:^1.69.0" react: "npm:19.2.3" react-native: "npm:0.86.0" react-native-builder-bob: "npm:^0.40.15" @@ -2296,6 +2206,272 @@ __metadata: languageName: node linkType: hard +"@oxfmt/binding-android-arm-eabi@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-android-arm-eabi@npm:0.54.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@oxfmt/binding-android-arm64@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-android-arm64@npm:0.54.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@oxfmt/binding-darwin-arm64@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-darwin-arm64@npm:0.54.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@oxfmt/binding-darwin-x64@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-darwin-x64@npm:0.54.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oxfmt/binding-freebsd-x64@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-freebsd-x64@npm:0.54.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@oxfmt/binding-linux-arm-gnueabihf@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.54.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxfmt/binding-linux-arm-musleabihf@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.54.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxfmt/binding-linux-arm64-gnu@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.54.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@oxfmt/binding-linux-arm64-musl@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.54.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@oxfmt/binding-linux-ppc64-gnu@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.54.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@oxfmt/binding-linux-riscv64-gnu@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.54.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@oxfmt/binding-linux-riscv64-musl@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.54.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@oxfmt/binding-linux-s390x-gnu@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.54.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@oxfmt/binding-linux-x64-gnu@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.54.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@oxfmt/binding-linux-x64-musl@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-linux-x64-musl@npm:0.54.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@oxfmt/binding-openharmony-arm64@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-openharmony-arm64@npm:0.54.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@oxfmt/binding-win32-arm64-msvc@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.54.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@oxfmt/binding-win32-ia32-msvc@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.54.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@oxfmt/binding-win32-x64-msvc@npm:0.54.0": + version: 0.54.0 + resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.54.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@oxlint/binding-android-arm-eabi@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-android-arm-eabi@npm:1.69.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@oxlint/binding-android-arm64@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-android-arm64@npm:1.69.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@oxlint/binding-darwin-arm64@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-darwin-arm64@npm:1.69.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@oxlint/binding-darwin-x64@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-darwin-x64@npm:1.69.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oxlint/binding-freebsd-x64@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-freebsd-x64@npm:1.69.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@oxlint/binding-linux-arm-gnueabihf@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-arm-gnueabihf@npm:1.69.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxlint/binding-linux-arm-musleabihf@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-arm-musleabihf@npm:1.69.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxlint/binding-linux-arm64-gnu@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-arm64-gnu@npm:1.69.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@oxlint/binding-linux-arm64-musl@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-arm64-musl@npm:1.69.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@oxlint/binding-linux-ppc64-gnu@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-ppc64-gnu@npm:1.69.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@oxlint/binding-linux-riscv64-gnu@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-riscv64-gnu@npm:1.69.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@oxlint/binding-linux-riscv64-musl@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-riscv64-musl@npm:1.69.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@oxlint/binding-linux-s390x-gnu@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-s390x-gnu@npm:1.69.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@oxlint/binding-linux-x64-gnu@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-x64-gnu@npm:1.69.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@oxlint/binding-linux-x64-musl@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-linux-x64-musl@npm:1.69.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@oxlint/binding-openharmony-arm64@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-openharmony-arm64@npm:1.69.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@oxlint/binding-win32-arm64-msvc@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-win32-arm64-msvc@npm:1.69.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@oxlint/binding-win32-ia32-msvc@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-win32-ia32-msvc@npm:1.69.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@oxlint/binding-win32-x64-msvc@npm:1.69.0": + version: 1.69.0 + resolution: "@oxlint/binding-win32-x64-msvc@npm:1.69.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -7454,6 +7630,159 @@ __metadata: languageName: node linkType: hard +"oxfmt@npm:^0.54.0": + version: 0.54.0 + resolution: "oxfmt@npm:0.54.0" + dependencies: + "@oxfmt/binding-android-arm-eabi": "npm:0.54.0" + "@oxfmt/binding-android-arm64": "npm:0.54.0" + "@oxfmt/binding-darwin-arm64": "npm:0.54.0" + "@oxfmt/binding-darwin-x64": "npm:0.54.0" + "@oxfmt/binding-freebsd-x64": "npm:0.54.0" + "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.54.0" + "@oxfmt/binding-linux-arm-musleabihf": "npm:0.54.0" + "@oxfmt/binding-linux-arm64-gnu": "npm:0.54.0" + "@oxfmt/binding-linux-arm64-musl": "npm:0.54.0" + "@oxfmt/binding-linux-ppc64-gnu": "npm:0.54.0" + "@oxfmt/binding-linux-riscv64-gnu": "npm:0.54.0" + "@oxfmt/binding-linux-riscv64-musl": "npm:0.54.0" + "@oxfmt/binding-linux-s390x-gnu": "npm:0.54.0" + "@oxfmt/binding-linux-x64-gnu": "npm:0.54.0" + "@oxfmt/binding-linux-x64-musl": "npm:0.54.0" + "@oxfmt/binding-openharmony-arm64": "npm:0.54.0" + "@oxfmt/binding-win32-arm64-msvc": "npm:0.54.0" + "@oxfmt/binding-win32-ia32-msvc": "npm:0.54.0" + "@oxfmt/binding-win32-x64-msvc": "npm:0.54.0" + tinypool: "npm:2.1.0" + peerDependencies: + svelte: ^5.0.0 + vite-plus: "*" + dependenciesMeta: + "@oxfmt/binding-android-arm-eabi": + optional: true + "@oxfmt/binding-android-arm64": + optional: true + "@oxfmt/binding-darwin-arm64": + optional: true + "@oxfmt/binding-darwin-x64": + optional: true + "@oxfmt/binding-freebsd-x64": + optional: true + "@oxfmt/binding-linux-arm-gnueabihf": + optional: true + "@oxfmt/binding-linux-arm-musleabihf": + optional: true + "@oxfmt/binding-linux-arm64-gnu": + optional: true + "@oxfmt/binding-linux-arm64-musl": + optional: true + "@oxfmt/binding-linux-ppc64-gnu": + optional: true + "@oxfmt/binding-linux-riscv64-gnu": + optional: true + "@oxfmt/binding-linux-riscv64-musl": + optional: true + "@oxfmt/binding-linux-s390x-gnu": + optional: true + "@oxfmt/binding-linux-x64-gnu": + optional: true + "@oxfmt/binding-linux-x64-musl": + optional: true + "@oxfmt/binding-openharmony-arm64": + optional: true + "@oxfmt/binding-win32-arm64-msvc": + optional: true + "@oxfmt/binding-win32-ia32-msvc": + optional: true + "@oxfmt/binding-win32-x64-msvc": + optional: true + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + bin: + oxfmt: bin/oxfmt + checksum: 10c0/e8e321c206ab952db58519a164f98cfec21fa14742e4e81046dc389a25c550285c7f998403f8b0a03f335070a477fccc281f3fc500f37883473a41ade2df0635 + languageName: node + linkType: hard + +"oxlint@npm:^1.69.0": + version: 1.69.0 + resolution: "oxlint@npm:1.69.0" + dependencies: + "@oxlint/binding-android-arm-eabi": "npm:1.69.0" + "@oxlint/binding-android-arm64": "npm:1.69.0" + "@oxlint/binding-darwin-arm64": "npm:1.69.0" + "@oxlint/binding-darwin-x64": "npm:1.69.0" + "@oxlint/binding-freebsd-x64": "npm:1.69.0" + "@oxlint/binding-linux-arm-gnueabihf": "npm:1.69.0" + "@oxlint/binding-linux-arm-musleabihf": "npm:1.69.0" + "@oxlint/binding-linux-arm64-gnu": "npm:1.69.0" + "@oxlint/binding-linux-arm64-musl": "npm:1.69.0" + "@oxlint/binding-linux-ppc64-gnu": "npm:1.69.0" + "@oxlint/binding-linux-riscv64-gnu": "npm:1.69.0" + "@oxlint/binding-linux-riscv64-musl": "npm:1.69.0" + "@oxlint/binding-linux-s390x-gnu": "npm:1.69.0" + "@oxlint/binding-linux-x64-gnu": "npm:1.69.0" + "@oxlint/binding-linux-x64-musl": "npm:1.69.0" + "@oxlint/binding-openharmony-arm64": "npm:1.69.0" + "@oxlint/binding-win32-arm64-msvc": "npm:1.69.0" + "@oxlint/binding-win32-ia32-msvc": "npm:1.69.0" + "@oxlint/binding-win32-x64-msvc": "npm:1.69.0" + peerDependencies: + oxlint-tsgolint: ">=0.22.1" + vite-plus: "*" + dependenciesMeta: + "@oxlint/binding-android-arm-eabi": + optional: true + "@oxlint/binding-android-arm64": + optional: true + "@oxlint/binding-darwin-arm64": + optional: true + "@oxlint/binding-darwin-x64": + optional: true + "@oxlint/binding-freebsd-x64": + optional: true + "@oxlint/binding-linux-arm-gnueabihf": + optional: true + "@oxlint/binding-linux-arm-musleabihf": + optional: true + "@oxlint/binding-linux-arm64-gnu": + optional: true + "@oxlint/binding-linux-arm64-musl": + optional: true + "@oxlint/binding-linux-ppc64-gnu": + optional: true + "@oxlint/binding-linux-riscv64-gnu": + optional: true + "@oxlint/binding-linux-riscv64-musl": + optional: true + "@oxlint/binding-linux-s390x-gnu": + optional: true + "@oxlint/binding-linux-x64-gnu": + optional: true + "@oxlint/binding-linux-x64-musl": + optional: true + "@oxlint/binding-openharmony-arm64": + optional: true + "@oxlint/binding-win32-arm64-msvc": + optional: true + "@oxlint/binding-win32-ia32-msvc": + optional: true + "@oxlint/binding-win32-x64-msvc": + optional: true + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + bin: + oxlint: bin/oxlint + checksum: 10c0/148d4bb03c8858454588f2f754eb7e6e13c7d0912da4b144b93f926799409e6bd884f39cf7a7232aed1c187167a897d5ccc56bb8ba80b83ed083787d7f0562bc + languageName: node + linkType: hard + "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -9043,6 +9372,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:2.1.0": + version: 2.1.0 + resolution: "tinypool@npm:2.1.0" + checksum: 10c0/9fb1c760558c6264e0f4cfde96a63b12450b43f1730fbe6274aa24ddbdf488745c08924d0dea7a1303b47d555416a6415f2113898c69b6ecf731e75ac95238a5 + languageName: node + linkType: hard + "tmp@npm:^0.2.4": version: 0.2.5 resolution: "tmp@npm:0.2.5"