diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd5eadc5..7caecab2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,13 @@ jobs: run: | TAG_NAME=${GITHUB_REF#refs/tags/} jq --arg version "$TAG_NAME" '.version = $version' package.json > tmp.$$.json && mv tmp.$$.json package.json - - run: npm publish + - name: Publish to npm + run: | + if echo "$GITHUB_REF_NAME" | grep -q -- '-beta\|-alpha\|-rc'; then + npm publish --tag beta + else + npm publish + fi - name: Create Github Release uses: elgohr/Github-Release-Action@v5 env: diff --git a/README.md b/README.md index 0bd6fac8..b482fcfe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![benchmark](benchmark2.jpg) +![benchmark](benchmark3.jpg) Created by [@ospfranco](https://twitter.com/ospfranco). **Please consider sponsoring!**. diff --git a/benchmark2.jpg b/benchmark2.jpg deleted file mode 100644 index bccec632..00000000 Binary files a/benchmark2.jpg and /dev/null differ diff --git a/benchmark3.jpg b/benchmark3.jpg new file mode 100644 index 00000000..00158a37 Binary files /dev/null and b/benchmark3.jpg differ diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 0b5031b1..5dfa3e28 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -400,7 +400,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { std::string query = args[0].asString(rt).utf8(rt); std::vector params; - if (count == 2) { + if (count == 2 && !args[1].isNull() && !args[1].isUndefined()) { params = to_variant_vec(rt, args[1]); } #ifdef OP_SQLITE_USE_LIBSQL diff --git a/cpp/bridge.cpp b/cpp/bridge.cpp index d05b213b..a5adaec8 100644 --- a/cpp/bridge.cpp +++ b/cpp/bridge.cpp @@ -10,6 +10,7 @@ #include "utils.hpp" #include #include +#include #include #include #include @@ -376,8 +377,9 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, std::string column_name, column_declared_type; std::vector column_names; std::vector> rows; - rows.reserve(20); std::vector row; + int changedRowCount = 0; + long long latestInsertRowId = 0; do { const char *query_str = @@ -402,22 +404,33 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, opsqlite_bind_statement(statement, params); } + // sqlite3_column_count is the correct signal: it's non-zero for any + // statement that can return rows (SELECT, and write statements with + // RETURNING), regardless of sqlite3_stmt_readonly. + column_names.clear(); column_count = sqlite3_column_count(statement); - column_names.reserve(column_count); + if (column_count > 0) { + column_names.reserve(column_count); + for (int i = 0; i < column_count; i++) { + column_name = sqlite3_column_name(statement, i); + column_names.emplace_back(column_name); + } + } + bool is_consuming_rows = true; double double_value; const char *string_value; - // Do a first pass to get the column names - for (int i = 0; i < column_count; i++) { - column_name = sqlite3_column_name(statement, i); - column_names.emplace_back(column_name); - } - while (is_consuming_rows) { status = sqlite3_step(statement); switch (status) { + case SQLITE_DONE: + changedRowCount = sqlite3_changes(db); + latestInsertRowId = sqlite3_last_insert_rowid(db); + is_consuming_rows = false; + break; + case SQLITE_ROW: current_column = 0; row = std::vector(); @@ -470,10 +483,6 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, rows.emplace_back(std::move(row)); break; - case SQLITE_DONE: - is_consuming_rows = false; - break; - default: has_failed = true; is_consuming_rows = false; @@ -481,6 +490,7 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, } sqlite3_finalize(statement); + } while (remainingStatement != nullptr && strcmp(remainingStatement, "") != 0 && !has_failed); @@ -490,8 +500,6 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, std::string(message)); } - int changedRowCount = sqlite3_changes(db); - long long latestInsertRowId = sqlite3_last_insert_rowid(db); return {.affectedRows = changedRowCount, .insertId = static_cast(latestInsertRowId), .rows = std::move(rows), diff --git a/cpp/utils.cpp b/cpp/utils.cpp index 990dd9e9..62a3e4fe 100644 --- a/cpp/utils.cpp +++ b/cpp/utils.cpp @@ -1,3 +1,4 @@ + #include "utils.hpp" #include "SmartHostObject.h" #include "types.hpp" @@ -15,7 +16,7 @@ namespace opsqlite { namespace jsi = facebook::jsi; namespace react = facebook::react; -inline jsi::Value to_jsi(jsi::Runtime &rt, const JSVariant &value) { +jsi::Value to_jsi(jsi::Runtime &rt, const JSVariant &value) { if (std::holds_alternative(value)) { return std::get(value); } else if (std::holds_alternative(value)) { @@ -76,7 +77,7 @@ inline jsi::Value to_jsi(jsi::Runtime &rt, const JSVariant &value) { // value); } -inline JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { +JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { if (value.isNull() || value.isUndefined()) { return JSVariant(nullptr); } else if (value.isBool()) { @@ -94,7 +95,7 @@ inline JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { } } else if (value.isString()) { std::string strVal = value.asString(rt).utf8(rt); - return JSVariant(strVal); + return JSVariant(std::move(strVal)); } else if (value.isObject()) { auto obj = value.asObject(rt); size_t byteOffset = 0; @@ -142,7 +143,7 @@ inline JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { } std::vector to_string_vec(jsi::Runtime &rt, jsi::Value const &xs) { - jsi::Array values = xs.asObject(rt).asArray(rt); + jsi::Array const values = xs.asObject(rt).asArray(rt); std::vector res; for (int ii = 0; ii < values.length(rt); ii++) { std::string value = values.getValueAtIndex(rt, ii).asString(rt).utf8(rt); @@ -152,7 +153,7 @@ std::vector to_string_vec(jsi::Runtime &rt, jsi::Value const &xs) { } std::vector to_int_vec(jsi::Runtime &rt, jsi::Value const &xs) { - jsi::Array values = xs.asObject(rt).asArray(rt); + jsi::Array const values = xs.asObject(rt).asArray(rt); std::vector res; for (int ii = 0; ii < values.length(rt); ii++) { int value = static_cast(values.getValueAtIndex(rt, ii).asNumber()); @@ -162,13 +163,15 @@ std::vector to_int_vec(jsi::Runtime &rt, jsi::Value const &xs) { } std::vector to_variant_vec(jsi::Runtime &rt, jsi::Value const &xs) { + jsi::Array const values = xs.asObject(rt).asArray(rt); + size_t arg_length = values.length(rt); + std::vector res; - jsi::Array values = xs.asObject(rt).asArray(rt); + res.reserve(arg_length); - for (int ii = 0; ii < values.length(rt); ii++) { - jsi::Value value = values.getValueAtIndex(rt, ii); - res.emplace_back(to_variant(rt, value)); - } + for (size_t ii = 0; ii < arg_length; ii++) { + res.emplace_back(to_variant(rt, values.getValueAtIndex(rt, ii))); + } return res; } @@ -184,6 +187,11 @@ jsi::Value create_js_rows(jsi::Runtime &rt, const BridgeResult &status) { size_t row_count = status.rows.size(); size_t column_count = status.column_names.size(); + if (row_count == 0) { + res.setProperty(rt, "rows", jsi::Array(rt, 0)); + return res; + } + std::vector column_prop_ids; column_prop_ids.reserve(column_count); for (size_t i = 0; i < column_count; i++) { diff --git a/example/src/App.tsx b/example/src/App.tsx index 0da908b2..db519711 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -5,7 +5,7 @@ import { } from "@op-engineering/op-test"; import { useEffect, useState } from "react"; import "./tests"; // import all tests to register them -import {performanceTest} from './performance_test'; +import {performanceTest, insertTest} 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'; @@ -36,15 +36,16 @@ export default function App() { console.log("OPSQLITE_TEST_RESULT:FAIL"); } - setTimeout(() => { - try { - global?.gc?.(); - let perfRes = performanceTest(); - setPerfResult(perfRes); - } catch (e) { - // intentionally left blank - } - }, 4000); + // setTimeout(() => { + // try { + // global?.gc?.(); + // // let perfRes = performanceTest(); + // let perfRes = insertTest(); + // setPerfResult(perfRes); + // } catch (e) { + // // intentionally left blank + // } + // }, 4000); }; work(); diff --git a/example/src/performance_test.ts b/example/src/performance_test.ts index 67f7a9e6..673720b9 100644 --- a/example/src/performance_test.ts +++ b/example/src/performance_test.ts @@ -1,5 +1,7 @@ import {open} from '@op-engineering/op-sqlite'; +const ITERATIONS = 1000; + export function performanceTest() { const db = open({ name: 'perfTest.sqlite', @@ -39,3 +41,28 @@ export function performanceTest() { // await db.close(); return end - start; } + +export function insertTest() { + const db = open({ + name: 'insertTest.sqlite' + }); + + + db.executeSync("DROP TABLE IF EXISTS bench"); + db.executeSync( + "CREATE TABLE bench (id INTEGER PRIMARY KEY, name TEXT, value REAL)", + ); + + // sync inserts + for (let i = 0; i < ITERATIONS; i++) { + db.executeSync("INSERT INTO bench VALUES (?,?,?)", [i, `n${i}`, i * 1.5]); + } + + // select all + let t = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + db.executeSync("SELECT * FROM bench"); + } + + return performance.now() - t; +} diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 040083d3..cca42c20 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -195,8 +195,7 @@ describe("Queries tests", () => { expect(res2.rowsAffected).toEqual(1); expect(res2.insertId).toEqual(1); - // expect(res2.rows).toBe([]); - expect(res2.rows?.length).toEqual(0); + expect(res2.rows.length).toEqual(0); }); it("Insert", async () => { @@ -891,6 +890,53 @@ describe("Queries tests", () => { expect(res.rows).toDeepEqual([{ user_version: 0 }]); }); + it("INSERT RETURNING yields rows with correct columns", async () => { + if (isLibsql() || isTurso()) { + return; + } + const res = await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?) RETURNING id, name", + [42, "Alice", 30, 1234.56], + ); + expect(res.rows.length).toBe(1); + expect(res.rows[0]!.id).toBe(42); + expect(res.rows[0]!.name).toBe("Alice"); + }); + + it("UPDATE RETURNING yields updated rows", async () => { + if (isLibsql() || isTurso()) { + return; + } + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?)", + [1, "Bob", 25, 500.0], + ); + const res = await db.execute( + "UPDATE User SET name = ? WHERE id = ? RETURNING id, name", + ["Robert", 1], + ); + expect(res.rows.length).toBe(1); + expect(res.rows[0]!.id).toBe(1); + expect(res.rows[0]!.name).toBe("Robert"); + }); + + it("DELETE RETURNING yields deleted rows", async () => { + if (isLibsql() || isTurso()) { + return; + } + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?)", + [99, "Eve", 40, 999.0], + ); + const res = await db.execute( + "DELETE FROM User WHERE id = ? RETURNING id, name", + [99], + ); + expect(res.rows.length).toBe(1); + expect(res.rows[0]!.id).toBe(99); + expect(res.rows[0]!.name).toBe("Eve"); + }); + // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; // if (sqliteVecEnabled) { // it('sqlite-vec extension: vector similarity search', async () => { diff --git a/example/src/tests/storage.ts b/example/src/tests/storage.ts index 4b1f9185..dfde6751 100644 --- a/example/src/tests/storage.ts +++ b/example/src/tests/storage.ts @@ -29,7 +29,7 @@ describe("Storage", () => { expect(res).toEqual("bark"); }); - it("can remove item sync", async () => { + it("Can remove item sync", async () => { storage.setItemSync("foo", "bar"); storage.removeItemSync("foo"); const res = storage.getItemSync("foo"); diff --git a/package.json b/package.json index bc4558a1..6cb2d9ea 100644 --- a/package.json +++ b/package.json @@ -1,134 +1,137 @@ { - "name": "@op-engineering/op-sqlite", - "version": "0.0.0", - "description": "Fastest SQLite for React Native (with node.js support)", - "main": "./lib/module/index.js", - "types": "./lib/typescript/src/index.d.ts", - "react-native": "src/index", - "exports": { - ".": { - "source": "./src/index.ts", - "node": "./node/dist/index.js", - "types": "./lib/typescript/src/index.d.ts", - "default": "./lib/module/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "src", - "lib", - "android", - "ios", - "cpp", - "node/dist", - "node/package.json", - "*.podspec", - "*.rb", - "react-native.config.js", - "ios/**.xcframework", - "!ios/build", - "!android/build", - "!android/gradle", - "!android/gradlew", - "!android/gradlew.bat", - "!android/local.properties", - "!**/__tests__", - "!**/__fixtures__", - "!**/__mocks__", - "!**/.*" - ], - "scripts": { - "test:node": "yarn workspace node test", - "example": "yarn workspace op_sqlite_example", - "typecheck": "tsc", - "prepare": "bob build && yarn build:node", - "build:node": "yarn workspace node build", - "build:turso": "./scripts/build-turso-binaries.sh", - "pods": "cd example && yarn pods", - "clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h" - }, - "keywords": [ - "react-native", - "ios", - "android", - "node", - "sqlite", - "database" - ], - "repository": "https://github.com/OP-Engineering/op-sqlite", - "author": "Oscar Franco (https://github.com/ospfranco)", - "license": "MIT", - "bugs": { - "url": "https://github.com/OP-Engineering/op-sqlite/issues" - }, - "homepage": "https://github.com/OP-Engineering/op-sqlite#readme", - "publishConfig": { - "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", - "react": "19.2.3", - "react-native": "0.86.0", - "react-native-builder-bob": "^0.40.15", - "typescript": "^5.9.2" - }, - "peerDependencies": { - "@sqlite.org/sqlite-wasm": "*", - "react": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "@sqlite.org/sqlite-wasm": { - "optional": true - } - }, - "workspaces": [ - "example", - "node" - ], - "prettier": { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - }, - "react-native-builder-bob": { - "source": "src", - "output": "lib", - "targets": [ - [ - "module", - { - "esm": true - } - ], - [ - "typescript", - { - "project": "tsconfig.build.json" - } - ] - ] - }, - "codegenConfig": { - "name": "OPSQLiteSpec", - "type": "modules", - "jsSrcsDir": "src", - "android": { - "javaPackageName": "com.op.sqlite" - } - }, - "create-react-native-library": { - "languages": "kotlin-objc", - "type": "turbo-module", - "version": "0.52.1" - }, - "packageManager": "yarn@4.11.0" + "name": "@op-engineering/op-sqlite", + "version": "0.0.0", + "description": "Fastest SQLite for React Native (with node.js support)", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "react-native": "src/index", + "exports": { + ".": { + "source": "./src/index.ts", + "node": "./node/dist/index.js", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "node/dist", + "node/package.json", + "*.podspec", + "*.rb", + "react-native.config.js", + "ios/**.xcframework", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "test:node": "yarn workspace node test", + "example": "yarn workspace op_sqlite_example", + "typecheck": "tsc", + "prepare": "bob build && yarn build:node", + "build:node": "yarn workspace node build", + "build:turso": "./scripts/build-turso-binaries.sh", + "pods": "cd example && yarn pods", + "clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h" + }, + "keywords": [ + "react-native", + "ios", + "android", + "node", + "sqlite", + "database" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OP-Engineering/op-sqlite.git" + }, + "author": "Oscar Franco (https://github.com/ospfranco)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OP-Engineering/op-sqlite/issues" + }, + "homepage": "https://github.com/OP-Engineering/op-sqlite#readme", + "publishConfig": { + "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", + "react": "19.2.3", + "react-native": "0.86.0", + "react-native-builder-bob": "^0.40.15", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "@sqlite.org/sqlite-wasm": "*", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@sqlite.org/sqlite-wasm": { + "optional": true + } + }, + "workspaces": [ + "example", + "node" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "codegenConfig": { + "name": "OPSQLiteSpec", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.op.sqlite" + } + }, + "create-react-native-library": { + "languages": "kotlin-objc", + "type": "turbo-module", + "version": "0.52.1" + }, + "packageManager": "yarn@4.11.0" } diff --git a/src/Storage.ts b/src/Storage.ts index f38e2b4c..6725cab5 100644 --- a/src/Storage.ts +++ b/src/Storage.ts @@ -1,9 +1,9 @@ -import { isTurso, open } from "./functions"; -import type { DB } from "./types"; +import { isTurso, open } from './functions'; +import type { DB } from './types'; type StorageOptions = { - location?: string; - encryptionKey?: string; + location?: string; + encryptionKey?: string; }; /** @@ -11,94 +11,94 @@ type StorageOptions = { * The encryption key is only used when compiled against the SQLCipher version of op-sqlite. */ export class Storage { - private db: DB; - - constructor(options: StorageOptions) { - this.db = open({ ...options, name: "__opsqlite_storage.sqlite" }); - if (!isTurso()) { - this.db.executeSync("PRAGMA mmap_size=268435456"); - } - const createStorageTable = isTurso() - ? "CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT)" - : "CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT) WITHOUT ROWID"; - this.db.executeSync(createStorageTable); - } - - async getItem(key: string): Promise { - const result = await this.db.execute( - "SELECT value FROM storage WHERE key = ?", - [key], - ); - - const value = result.rows[0]?.value; - if (typeof value !== "undefined" && typeof value !== "string") { - throw new Error("Value must be a string or undefined"); - } - return value; - } - - getItemSync(key: string): string | undefined { - const result = this.db.executeSync( - "SELECT value FROM storage WHERE key = ?", - [key], - ); - - const value = result.rows[0]?.value; - if (typeof value !== "undefined" && typeof value !== "string") { - throw new Error("Value must be a string or undefined"); - } - - return value; - } - - async setItem(key: string, value: string) { - await this.db.execute( - "INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)", - [key, value.toString()], - ); - } - - setItemSync(key: string, value: string) { - this.db.executeSync( - "INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)", - [key, value.toString()], - ); - } - - async removeItem(key: string) { - await this.db.execute("DELETE FROM storage WHERE key = ?", [key]); - } - - removeItemSync(key: string) { - this.db.executeSync("DELETE FROM storage WHERE key = ?", [key]); - } - - async clear() { - await this.db.execute("DELETE FROM storage"); - } - - clearSync() { - this.db.executeSync("DELETE FROM storage"); - } - - getAllKeys() { - return this.db - .executeSync("SELECT key FROM storage") - .rows.map((row: any) => row.key); - } - - /** - * Deletes the underlying database file. - */ - delete() { - this.db.delete(); - } - - async close() { - await this.db.closeAsync(); - } - - closeSync() { - this.db.close(); - } + private db: DB; + + constructor(options: StorageOptions) { + this.db = open({ ...options, name: '__opsqlite_storage.sqlite' }); + if (!isTurso()) { + this.db.executeSync('PRAGMA mmap_size=268435456'); + } + const createStorageTable = isTurso() + ? 'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT)' + : 'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT) WITHOUT ROWID'; + this.db.executeSync(createStorageTable); + } + + async getItem(key: string): Promise { + const result = await this.db.execute( + 'SELECT value FROM storage WHERE key = ?', + [key] + ); + + const value = result.rows[0]?.value; + if (typeof value !== 'undefined' && typeof value !== 'string') { + throw new Error('Value must be a string or undefined'); + } + return value; + } + + getItemSync(key: string): string | undefined { + const result = this.db.executeSync( + 'SELECT value FROM storage WHERE key = ?', + [key] + ); + + const value = result.rows?.[0]?.value; + if (typeof value !== 'undefined' && typeof value !== 'string') { + throw new Error('Value must be a string or undefined'); + } + + return value; + } + + async setItem(key: string, value: string) { + await this.db.execute( + 'INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)', + [key, value.toString()] + ); + } + + setItemSync(key: string, value: string) { + this.db.executeSync( + 'INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)', + [key, value.toString()] + ); + } + + async removeItem(key: string) { + await this.db.execute('DELETE FROM storage WHERE key = ?', [key]); + } + + removeItemSync(key: string) { + this.db.executeSync('DELETE FROM storage WHERE key = ?', [key]); + } + + async clear() { + await this.db.execute('DELETE FROM storage'); + } + + clearSync() { + this.db.executeSync('DELETE FROM storage'); + } + + getAllKeys() { + return this.db + .executeSync('SELECT key FROM storage') + .rows.map((row: any) => row.key); + } + + /** + * Deletes the underlying database file. + */ + delete() { + this.db.delete(); + } + + async close() { + await this.db.closeAsync(); + } + + closeSync() { + this.db.close(); + } } diff --git a/src/functions.ts b/src/functions.ts index aa815fdb..8bc73661 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -1,320 +1,296 @@ -import { NativeModules, Platform } from "react-native"; +import { NativeModules, Platform } from 'react-native'; import type { - _InternalDB, - _PendingTransaction, - BatchQueryResult, - DB, - DBParams, - OpenOptions, - OPSQLiteProxy, - QueryResult, - Scalar, - SQLBatchTuple, - Transaction, -} from "./types"; + _InternalDB, + _PendingTransaction, + BatchQueryResult, + DB, + DBParams, + OpenOptions, + 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; + 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, + executeSync: db.executeSync, + 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[]); + }, + 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; } /** @@ -322,25 +298,25 @@ 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; }; /** @@ -348,16 +324,16 @@ 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; }; /** @@ -365,17 +341,17 @@ export const openRemote = (params: { url: string; authToken: string }): DB => { * If you want libsql remote or sync connections, use openSync or openRemote */ export const open = (params: OpenOptions): 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); - } + 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); + const db = OPSQLite.open(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; }; /** @@ -383,7 +359,7 @@ export const open = (params: OpenOptions): DB => { * Useful for cross-platform code that also targets web where openAsync() is required. */ export const openAsync = async (params: OpenOptions): Promise => { - return open(params); + return open(params); }; /** @@ -394,11 +370,11 @@ export const openAsync = async (params: OpenOptions): Promise => { * @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); }; /** @@ -410,27 +386,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(); }; /**