diff --git a/README.md b/README.md index 80544f3e..cc0aa91d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ const db = open({ name: 'myDb.sqlite' }) | Method | Sync | Async | Description | |--------|------|-------|-------------| | **Execute** | `db.execute(query, params?)` | `db.executeAsync(query, params?)` | Run a single SQL statement. | +| **Prepared statement** | `db.prepare(query)` | Statement `executeAsync(params?)` | Prepare once and execute repeatedly with different parameters. | | **Batch** | `db.executeBatch(commands)` | `db.executeBatchAsync(commands)` | Run multiple statements in one transaction. | | **Load file** | `db.loadFile(path)` | `db.loadFileAsync(path)` | Execute SQL from a file. | | **Transaction** | — | `db.transaction(async (tx) => { ... })` | Run multiple statements in a transaction (async only). | @@ -136,6 +137,21 @@ const { rowsAffected } = db.executeBatch(commands) // Or: await db.executeBatchAsync(commands) ``` +## Prepared statements + +Use `db.prepare()` when the same SQL statement is executed repeatedly with different parameters. Call `finalize()` once the statement is no longer needed, and always finalize it before closing its database connection. + +```typescript +const insertUser = db.prepare( + 'INSERT INTO users (id, name) VALUES (?, ?)', +) + +insertUser.execute([1, 'Ada']) +await insertUser.executeAsync([2, 'Grace']) + +insertUser.finalize() +``` + # Column metadata When you need column types or names for the result set, use the `metadata` field on the query result. Keys are column names; values include `name`, `type` (e.g. from `ColumnType`), and `index`. diff --git a/bun.lock b/bun.lock index c94506eb..da8e5aa8 100644 --- a/bun.lock +++ b/bun.lock @@ -99,7 +99,7 @@ }, "packages/react-native-nitro-sqlite": { "name": "react-native-nitro-sqlite", - "version": "9.6.0", + "version": "9.7.0", "dependencies": { "typeorm": "0.3.27", }, @@ -119,9 +119,9 @@ }, "packages/react-native-nitro-sqlite-vec": { "name": "react-native-nitro-sqlite-vec", - "version": "9.6.0", + "version": "9.7.0", "devDependencies": { - "react-native-nitro-sqlite": "9.6.0", + "react-native-nitro-sqlite": "9.7.0", "typescript": "^5.8.3", }, "peerDependencies": { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d633e465..c3ce98a8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1908,7 +1908,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNNitroSQLite (9.6.0): + - RNNitroSQLite (9.7.0): - hermes-engine - NitroModules - RCTRequired @@ -1931,7 +1931,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNNitroSqliteVec (9.6.0): + - RNNitroSqliteVec (9.7.0): - RNNitroSQLite - RNScreens (4.18.0): - hermes-engine @@ -2314,8 +2314,8 @@ SPEC CHECKSUMS: ReactCommon: 7525e252c88d254545e3fdaea0000d1959dfbf20 ReactNativeDependencies: 0cdc5c6985700a9d8f654762fa702169ef9cef9b RNCClipboard: 7a7d4557bfd3370b35c99dfecd92ae7b9fc4948a - RNNitroSQLite: bad203a8435c5d843534398dc4822b3e80fbbd81 - RNNitroSqliteVec: cca5db2aec2a31f4e26a911bda7cac8fc2cdc2e2 + RNNitroSQLite: 6264d13622c7aa031831de174d2c7ec39c62f58b + RNNitroSqliteVec: bac14e5dad54e7dffdb5dd464d5e8853ae4d2540 RNScreens: dd5c879d56b543c7ff8e593739eeb66093d60263 Yoga: d68c8a4bde0af9c17b15540fffa330d26398d150 diff --git a/example/package.json b/example/package.json index a65a2689..8bdaa535 100644 --- a/example/package.json +++ b/example/package.json @@ -27,8 +27,8 @@ "react": "19.2.3", "react-native": "0.85.0-rc.0", "react-native-nitro-modules": "*", - "react-native-nitro-sqlite": "9.6.0", - "react-native-nitro-sqlite-vec": "9.6.0", + "react-native-nitro-sqlite": "9.7.0", + "react-native-nitro-sqlite-vec": "9.7.0", "react-native-quick-base64": "^3.0.0", "react-native-quick-crypto": "^1.1.5", "react-native-safe-area-context": "^5.5.2", diff --git a/example/tests/unit/index.ts b/example/tests/unit/index.ts index 5a51835b..2f424178 100644 --- a/example/tests/unit/index.ts +++ b/example/tests/unit/index.ts @@ -3,6 +3,7 @@ import { setupTestDb } from './common' import registerExecuteUnitTests from './specs/operations/execute.spec' import registerTransactionUnitTests from './specs/operations/transaction.spec' import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec' +import registerPreparedStatementUnitTests from './specs/operations/preparedStatement.spec' import registerTypeORMUnitTestsSpecs from './specs/typeorm.spec' import registerDatabaseQueueUnitTests from './specs/DatabaseQueue.spec' import registerSqliteVecUnitTestsSpecs from './specs/sqlite-vec.spec' @@ -14,6 +15,7 @@ export function registerUnitTests() { registerExecuteUnitTests() registerTransactionUnitTests() registerExecuteBatchUnitTests() + registerPreparedStatementUnitTests() }) registerDatabaseQueueUnitTests() diff --git a/example/tests/unit/specs/operations/preparedStatement.spec.ts b/example/tests/unit/specs/operations/preparedStatement.spec.ts new file mode 100644 index 00000000..b449a820 --- /dev/null +++ b/example/tests/unit/specs/operations/preparedStatement.spec.ts @@ -0,0 +1,85 @@ +import { chance, expect, isNitroSQLiteError } from '@tests/unit/common' +import { describe, it } from '@tests/TestApi' +import { testDb } from '@tests/db' + +export default function registerPreparedStatementUnitTests() { + describe('prepared statements', () => { + it('reuses one statement with different parameter values', () => { + const insert = testDb.prepare( + 'INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?)', + ) + const firstUser = { + id: chance.integer(), + name: chance.name(), + age: chance.integer(), + networth: chance.floating(), + } + const secondUser = { + id: chance.integer(), + name: chance.name(), + age: chance.integer(), + networth: chance.floating(), + } + + expect(insert.isFinalized).toBe(false) + expect( + insert.execute([ + firstUser.id, + firstUser.name, + firstUser.age, + firstUser.networth, + ]).rowsAffected, + ).toBe(1) + expect( + insert.execute([ + secondUser.id, + secondUser.name, + secondUser.age, + secondUser.networth, + ]).rowsAffected, + ).toBe(1) + + const select = testDb.prepare('SELECT * FROM User WHERE id = ?') + expect(select.execute([firstUser.id]).rows._array).toEqual([firstUser]) + expect(select.execute([secondUser.id]).rows._array).toEqual([secondUser]) + + insert.finalize() + select.finalize() + expect(insert.isFinalized).toBe(true) + expect(select.isFinalized).toBe(true) + }) + + it('executes asynchronously', async () => { + const id = chance.integer() + const statement = testDb.prepare( + 'INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?)', + ) + + const result = await statement.executeAsync([ + id, + chance.name(), + chance.integer(), + chance.floating(), + ]) + + expect(result.rowsAffected).toBe(1) + expect(testDb.execute('SELECT * FROM User WHERE id = ?', [id]).rows.length).toBe(1) + statement.finalize() + }) + + it('rejects execution after finalization', () => { + const statement = testDb.prepare('SELECT * FROM User') + statement.finalize() + + try { + statement.execute() + throw new Error('Expected execution to throw after finalization') + } catch (error) { + expect(isNitroSQLiteError(error)).toBe(true) + if (isNitroSQLiteError(error)) { + expect(error.message).toContain('Prepared statement has been finalized') + } + } + }) + }) +} diff --git a/package.json b/package.json index 974a3cd5..d15caee1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-sqlite-workspace", - "version": "9.6.0", + "version": "9.7.0", "packageManager": "bun@1.3.1", "private": "true", "workspaces": [ diff --git a/packages/react-native-nitro-sqlite-vec/package.json b/packages/react-native-nitro-sqlite-vec/package.json index d483f9d1..ef61a014 100644 --- a/packages/react-native-nitro-sqlite-vec/package.json +++ b/packages/react-native-nitro-sqlite-vec/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-sqlite-vec", - "version": "9.6.0", + "version": "9.7.0", "description": "Vector search (sqlite-vec) for react-native-nitro-sqlite. Native sqlite-vec is statically linked into the core's single sqlite3 — no second SQLite, no runtime extension loading.", "source": "./src/index", "react-native": "./src/index", @@ -35,13 +35,14 @@ }, "license": "MIT", "publishConfig": { - "registry": "https://registry.npmjs.org/" + "registry": "https://registry.npmjs.org/", + "access": "public" }, "peerDependencies": { "react-native-nitro-sqlite": ">=9.0.0" }, "devDependencies": { - "react-native-nitro-sqlite": "9.6.0", + "react-native-nitro-sqlite": "9.7.0", "typescript": "^5.8.3" }, "release-it": { diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp index 60a1d63f..f549d1ec 100644 --- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp +++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp @@ -1,4 +1,5 @@ #include "HybridNitroSQLite.hpp" +#include "HybridNitroSQLitePreparedStatement.hpp" #include "HybridNitroSQLiteQueryResult.hpp" #include "NitroSQLiteException.hpp" #include "importSqlFile.hpp" @@ -109,6 +110,10 @@ HybridNitroSQLite::executeAsync(const std::string& dbName, const std::string& qu }); }; +std::shared_ptr HybridNitroSQLite::prepare(const std::string& dbName, const std::string& query) { + return std::make_shared(sqlitePrepare(dbName, query)); +} + BatchQueryResult HybridNitroSQLite::executeBatch(const std::string& dbName, const std::vector& batchParams) { const auto commands = batchParamsToCommands(batchParams); diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp index a5331964..e6810ccc 100644 --- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp +++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp @@ -1,5 +1,6 @@ #pragma once +#include "HybridNitroSQLitePreparedStatementSpec.hpp" #include "HybridNitroSQLiteQueryResultSpec.hpp" #include "HybridNitroSQLiteSpec.hpp" #include "types.hpp" @@ -34,6 +35,8 @@ class HybridNitroSQLite : public HybridNitroSQLiteSpec { std::shared_ptr>> executeAsync(const std::string& dbName, const std::string& query, const std::optional& params) override; + std::shared_ptr prepare(const std::string& dbName, const std::string& query) override; + BatchQueryResult executeBatch(const std::string& dbName, const std::vector& commands) override; std::shared_ptr> executeBatchAsync(const std::string& dbName, const std::vector& commands) override; diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp new file mode 100644 index 00000000..fe5d7eaf --- /dev/null +++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp @@ -0,0 +1,61 @@ +#include "HybridNitroSQLitePreparedStatement.hpp" +#include "HybridNitroSQLiteQueryResult.hpp" +#include "operations.hpp" +#include + +namespace margelo::nitro::rnnitrosqlite { + +namespace { + + std::optional copyArrayBufferParamsForBackground(const std::optional& params) { + if (!params) { + return std::nullopt; + } + + SQLiteQueryParams copiedParams; + copiedParams.reserve(params->size()); + + for (const auto& value : *params) { + if (std::holds_alternative>(value)) { + copiedParams.push_back(ArrayBuffer::copy(std::get>(value))); + } else { + copiedParams.push_back(value); + } + } + + return copiedParams; + } + +} // namespace + +HybridNitroSQLitePreparedStatement::HybridNitroSQLitePreparedStatement( + std::shared_ptr<::margelo::rnnitrosqlite::SQLitePreparedStatement> statement) + : HybridObject(TAG), _statement(std::move(statement)) {} + +std::shared_ptr +HybridNitroSQLitePreparedStatement::execute(const std::optional& params) { + return _statement->execute(params); +} + +std::shared_ptr>> +HybridNitroSQLitePreparedStatement::executeAsync(const std::optional& params) { + const auto copiedParams = copyArrayBufferParamsForBackground(params); + const auto statement = _statement; + + return Promise>::async( + [statement, copiedParams]() -> std::shared_ptr { return statement->execute(copiedParams); }); +} + +void HybridNitroSQLitePreparedStatement::finalize() { + _statement->finalize(); +} + +bool HybridNitroSQLitePreparedStatement::getIsFinalized() { + return _statement->isFinalized(); +} + +size_t HybridNitroSQLitePreparedStatement::getExternalMemorySize() noexcept { + return sizeof(*this) + _statement->getExternalMemorySize(); +} + +} // namespace margelo::nitro::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp new file mode 100644 index 00000000..54c744a8 --- /dev/null +++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "HybridNitroSQLitePreparedStatementSpec.hpp" +#include "types.hpp" +#include + +using namespace margelo::rnnitrosqlite; + +namespace margelo::rnnitrosqlite { +class SQLitePreparedStatement; +} + +namespace margelo::nitro::rnnitrosqlite { + +class HybridNitroSQLitePreparedStatement : public HybridNitroSQLitePreparedStatementSpec { +public: + explicit HybridNitroSQLitePreparedStatement(std::shared_ptr<::margelo::rnnitrosqlite::SQLitePreparedStatement> statement); + + std::shared_ptr execute(const std::optional& params) override; + std::shared_ptr>> + executeAsync(const std::optional& params) override; + void finalize() override; + bool getIsFinalized() override; + + size_t getExternalMemorySize() noexcept override; + +private: + std::shared_ptr<::margelo::rnnitrosqlite::SQLitePreparedStatement> _statement; +}; + +} // namespace margelo::nitro::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/cpp/operations.cpp b/packages/react-native-nitro-sqlite/cpp/operations.cpp index ae61dc65..35901b65 100644 --- a/packages/react-native-nitro-sqlite/cpp/operations.cpp +++ b/packages/react-native-nitro-sqlite/cpp/operations.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -118,27 +119,33 @@ void sqliteRemoveDb(const std::string& dbName, const std::string& docPath) { } void bindStatement(sqlite3_stmt* statement, const SQLiteQueryParams& values) { - for (int valueIndex = 0; valueIndex < values.size(); valueIndex++) { + for (size_t valueIndex = 0; valueIndex < values.size(); valueIndex++) { int sqliteIndex = valueIndex + 1; - SQLiteValue value = values.at(valueIndex); + const SQLiteValue& value = values.at(valueIndex); + int bindStatus = SQLITE_OK; + if (std::holds_alternative(value)) { - sqlite3_bind_null(statement, sqliteIndex); + bindStatus = sqlite3_bind_null(statement, sqliteIndex); } else if (std::holds_alternative(value)) { - sqlite3_bind_int(statement, sqliteIndex, std::get(value)); + bindStatus = sqlite3_bind_int(statement, sqliteIndex, std::get(value)); } else if (std::holds_alternative(value)) { // Bind whole numbers as INTEGER so vec0 rowid/pk/partition (which reject REAL) work; SQLite still coerces to REAL for REAL columns. double doubleValue = std::get(value); if (std::trunc(doubleValue) == doubleValue && doubleValue >= kInt64MinAsDouble && doubleValue < kInt64UpperBoundAsDouble) { - sqlite3_bind_int64(statement, sqliteIndex, static_cast(doubleValue)); + bindStatus = sqlite3_bind_int64(statement, sqliteIndex, static_cast(doubleValue)); } else { - sqlite3_bind_double(statement, sqliteIndex, doubleValue); + bindStatus = sqlite3_bind_double(statement, sqliteIndex, doubleValue); } } else if (std::holds_alternative(value)) { - const auto stringValue = std::get(value); - sqlite3_bind_text(statement, sqliteIndex, stringValue.c_str(), stringValue.length(), SQLITE_TRANSIENT); + const auto& stringValue = std::get(value); + bindStatus = sqlite3_bind_text(statement, sqliteIndex, stringValue.c_str(), stringValue.length(), SQLITE_TRANSIENT); } else if (std::holds_alternative>(value)) { - const auto arrayBufferValue = std::get>(value); - sqlite3_bind_blob(statement, sqliteIndex, arrayBufferValue->data(), arrayBufferValue->size(), SQLITE_STATIC); + const auto& arrayBufferValue = std::get>(value); + bindStatus = sqlite3_bind_blob(statement, sqliteIndex, arrayBufferValue->data(), arrayBufferValue->size(), SQLITE_TRANSIENT); + } + + if (bindStatus != SQLITE_OK) { + throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(sqlite3_db_handle(statement))); } } } @@ -197,70 +204,74 @@ namespace { } } -} // namespace - -std::shared_ptr sqliteExecute(const std::string& dbName, const std::string& query, - const std::optional& params) { - auto db = getOpenDatabase(dbName); - auto statement = prepareStatement(db, query, params); - SQLiteQueryResults results; - - consumeStatement(db, statement.get(), [&](sqlite3_stmt* currentStatement) { - SQLiteQueryResultRow row; - int count = sqlite3_column_count(currentStatement); - - for (int i = 0; i < count; i++) { - int columnType = sqlite3_column_type(currentStatement, i); - std::string columnName = sqlite3_column_name(currentStatement, i); - - switch (columnType) { - case SQLITE_INTEGER: - case SQLITE_FLOAT: - row[columnName] = sqlite3_column_double(currentStatement, i); - break; - case SQLITE_TEXT: { - auto columnValue = reinterpret_cast(sqlite3_column_text(currentStatement, i)); - row[columnName] = columnValue; - break; - } - case SQLITE_BLOB: { - int blobSize = sqlite3_column_bytes(currentStatement, i); - const void* blob = sqlite3_column_blob(currentStatement, i); - if (blobSize > 0) { - const auto* blobData = reinterpret_cast(blob); - row[columnName] = ArrayBuffer::copy(blobData, static_cast(blobSize)); - } else { - row[columnName] = ArrayBuffer::allocate(0); + std::shared_ptr executeStatement(sqlite3* db, sqlite3_stmt* statement) { + SQLiteQueryResults results; + + consumeStatement(db, statement, [&](sqlite3_stmt* currentStatement) { + SQLiteQueryResultRow row; + int count = sqlite3_column_count(currentStatement); + + for (int i = 0; i < count; i++) { + int columnType = sqlite3_column_type(currentStatement, i); + std::string columnName = sqlite3_column_name(currentStatement, i); + + switch (columnType) { + case SQLITE_INTEGER: + case SQLITE_FLOAT: + row[columnName] = sqlite3_column_double(currentStatement, i); + break; + case SQLITE_TEXT: { + auto columnValue = reinterpret_cast(sqlite3_column_text(currentStatement, i)); + row[columnName] = columnValue; + break; } - break; + case SQLITE_BLOB: { + int blobSize = sqlite3_column_bytes(currentStatement, i); + const void* blob = sqlite3_column_blob(currentStatement, i); + if (blobSize > 0) { + const auto* blobData = reinterpret_cast(blob); + row[columnName] = ArrayBuffer::copy(blobData, static_cast(blobSize)); + } else { + row[columnName] = ArrayBuffer::allocate(0); + } + break; + } + case SQLITE_NULL: + default: + row[columnName] = NullType::null; + break; } - case SQLITE_NULL: - default: - row[columnName] = NullType::null; - break; } - } - results.push_back(std::move(row)); - }); + results.push_back(std::move(row)); + }); - std::optional metadata = std::nullopt; - int count = sqlite3_column_count(statement.get()); - for (int i = 0; i < count; i++) { - std::string columnName = sqlite3_column_name(statement.get(), i); - ColumnType columnDeclaredType = mapSQLiteTypeToColumnType(sqlite3_column_decltype(statement.get(), i)); - auto columnMeta = NitroSQLiteQueryColumnMetadata(columnName, std::move(columnDeclaredType), i); + std::optional metadata = std::nullopt; + int count = sqlite3_column_count(statement); + for (int i = 0; i < count; i++) { + std::string columnName = sqlite3_column_name(statement, i); + ColumnType columnDeclaredType = mapSQLiteTypeToColumnType(sqlite3_column_decltype(statement, i)); + auto columnMeta = NitroSQLiteQueryColumnMetadata(columnName, std::move(columnDeclaredType), i); - if (!metadata) { - metadata = std::make_optional(); + if (!metadata) { + metadata = std::make_optional(); + } + metadata->insert({columnName, std::move(columnMeta)}); } - metadata->insert({columnName, std::move(columnMeta)}); + + int rowsAffected = sqlite3_changes(db); + long long latestInsertRowId = sqlite3_last_insert_rowid(db); + return std::make_shared(std::move(results), static_cast(latestInsertRowId), rowsAffected, + std::move(metadata)); } - int rowsAffected = sqlite3_changes(db); - long long latestInsertRowId = sqlite3_last_insert_rowid(db); - return std::make_shared(std::move(results), static_cast(latestInsertRowId), rowsAffected, - std::move(metadata)); +} // namespace + +std::shared_ptr sqliteExecute(const std::string& dbName, const std::string& query, + const std::optional& params) { + auto db = getOpenDatabase(dbName); + auto statement = prepareStatement(db, query, params); + return executeStatement(db, statement.get()); } SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query, @@ -274,4 +285,70 @@ SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std: return {.rowsAffected = isReadOnly ? 0 : sqlite3_changes(db)}; } +struct SQLitePreparedStatement::State { + State(std::string dbName, sqlite3* database, SQLiteStatement statement) + : dbName(std::move(dbName)), database(database), statement(std::move(statement)) {} + + std::string dbName; + sqlite3* database; + SQLiteStatement statement; + mutable std::mutex mutex; +}; + +SQLitePreparedStatement::SQLitePreparedStatement(std::shared_ptr state) : _state(std::move(state)) {} + +SQLitePreparedStatement::~SQLitePreparedStatement() { + finalize(); +} + +std::shared_ptr SQLitePreparedStatement::execute(const std::optional& params) { + std::lock_guard lock(_state->mutex); + + if (!_state->statement) { + throw NitroSQLiteException("Prepared statement has been finalized"); + } + + auto database = getOpenDatabase(_state->dbName); + if (database != _state->database) { + throw NitroSQLiteException("Prepared statement belongs to a closed database connection"); + } + + int resetStatus = sqlite3_reset(_state->statement.get()); + if (resetStatus != SQLITE_OK) { + throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(database)); + } + + int clearBindingsStatus = sqlite3_clear_bindings(_state->statement.get()); + if (clearBindingsStatus != SQLITE_OK) { + throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(database)); + } + + if (params) { + bindStatement(_state->statement.get(), *params); + } + + return executeStatement(database, _state->statement.get()); +} + +void SQLitePreparedStatement::finalize() { + std::lock_guard lock(_state->mutex); + _state->statement.reset(); +} + +bool SQLitePreparedStatement::isFinalized() const { + std::lock_guard lock(_state->mutex); + return !_state->statement; +} + +size_t SQLitePreparedStatement::getExternalMemorySize() const noexcept { + return sizeof(*this) + sizeof(State); +} + +std::shared_ptr sqlitePrepare(const std::string& dbName, const std::string& query) { + auto database = getOpenDatabase(dbName); + auto statement = prepareStatement(database, query, std::nullopt); + auto state = std::make_shared(dbName, database, std::move(statement)); + return std::shared_ptr(new SQLitePreparedStatement(std::move(state))); +} + } // namespace margelo::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/cpp/operations.hpp b/packages/react-native-nitro-sqlite/cpp/operations.hpp index 49fce8ca..20574021 100644 --- a/packages/react-native-nitro-sqlite/cpp/operations.hpp +++ b/packages/react-native-nitro-sqlite/cpp/operations.hpp @@ -5,6 +5,25 @@ namespace margelo::rnnitrosqlite { +class SQLitePreparedStatement { +public: + ~SQLitePreparedStatement(); + + std::shared_ptr execute(const std::optional& params); + void finalize(); + bool isFinalized() const; + size_t getExternalMemorySize() const noexcept; + +private: + struct State; + + explicit SQLitePreparedStatement(std::shared_ptr state); + + std::shared_ptr _state; + + friend std::shared_ptr sqlitePrepare(const std::string& dbName, const std::string& query); +}; + void sqliteOpenDb(const std::string& dbName, const std::string& docPath); void sqliteCloseDb(const std::string& dbName); @@ -22,6 +41,8 @@ std::shared_ptr sqliteExecute(const std::string& d SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query, const std::optional& params = std::nullopt); +std::shared_ptr sqlitePrepare(const std::string& dbName, const std::string& query); + void sqliteCloseAll(); } // namespace margelo::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/android/RNNitroSQLite+autolinking.cmake b/packages/react-native-nitro-sqlite/nitrogen/generated/android/RNNitroSQLite+autolinking.cmake index 15e44d42..cad7718c 100644 --- a/packages/react-native-nitro-sqlite/nitrogen/generated/android/RNNitroSQLite+autolinking.cmake +++ b/packages/react-native-nitro-sqlite/nitrogen/generated/android/RNNitroSQLite+autolinking.cmake @@ -35,6 +35,7 @@ target_sources( # Shared Nitrogen C++ sources ../nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.cpp ../nitrogen/generated/shared/c++/HybridNitroSQLiteOnLoadSpec.cpp + ../nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp ../nitrogen/generated/shared/c++/HybridNitroSQLiteQueryResultSpec.cpp # Android-specific Nitrogen C++ sources ../nitrogen/generated/android/c++/JHybridNitroSQLiteOnLoadSpec.cpp diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp new file mode 100644 index 00000000..906479d3 --- /dev/null +++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp @@ -0,0 +1,24 @@ +/// +/// HybridNitroSQLitePreparedStatementSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridNitroSQLitePreparedStatementSpec.hpp" + +namespace margelo::nitro::rnnitrosqlite { + + void HybridNitroSQLitePreparedStatementSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridGetter("isFinalized", &HybridNitroSQLitePreparedStatementSpec::getIsFinalized); + prototype.registerHybridMethod("execute", &HybridNitroSQLitePreparedStatementSpec::execute); + prototype.registerHybridMethod("executeAsync", &HybridNitroSQLitePreparedStatementSpec::executeAsync); + prototype.registerHybridMethod("finalize", &HybridNitroSQLitePreparedStatementSpec::finalize); + }); + } + +} // namespace margelo::nitro::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.hpp b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.hpp new file mode 100644 index 00000000..fa97c166 --- /dev/null +++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.hpp @@ -0,0 +1,73 @@ +/// +/// HybridNitroSQLitePreparedStatementSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `HybridNitroSQLiteQueryResultSpec` to properly resolve imports. +namespace margelo::nitro::rnnitrosqlite { class HybridNitroSQLiteQueryResultSpec; } + +#include +#include "HybridNitroSQLiteQueryResultSpec.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace margelo::nitro::rnnitrosqlite { + + using namespace margelo::nitro; + + /** + * An abstract base class for `NitroSQLitePreparedStatement` + * Inherit this class to create instances of `HybridNitroSQLitePreparedStatementSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridNitroSQLitePreparedStatement: public HybridNitroSQLitePreparedStatementSpec { + * public: + * HybridNitroSQLitePreparedStatement(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridNitroSQLitePreparedStatementSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridNitroSQLitePreparedStatementSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridNitroSQLitePreparedStatementSpec() override = default; + + public: + // Properties + virtual bool getIsFinalized() = 0; + + public: + // Methods + virtual std::shared_ptr execute(const std::optional, std::string, double>>>& params) = 0; + virtual std::shared_ptr>> executeAsync(const std::optional, std::string, double>>>& params) = 0; + virtual void finalize() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "NitroSQLitePreparedStatement"; + }; + +} // namespace margelo::nitro::rnnitrosqlite diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.cpp b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.cpp index aea61f16..eb81b586 100644 --- a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.cpp +++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.cpp @@ -21,6 +21,7 @@ namespace margelo::nitro::rnnitrosqlite { prototype.registerHybridMethod("detach", &HybridNitroSQLiteSpec::detach); prototype.registerHybridMethod("execute", &HybridNitroSQLiteSpec::execute); prototype.registerHybridMethod("executeAsync", &HybridNitroSQLiteSpec::executeAsync); + prototype.registerHybridMethod("prepare", &HybridNitroSQLiteSpec::prepare); prototype.registerHybridMethod("executeBatch", &HybridNitroSQLiteSpec::executeBatch); prototype.registerHybridMethod("executeBatchAsync", &HybridNitroSQLiteSpec::executeBatchAsync); prototype.registerHybridMethod("loadFile", &HybridNitroSQLiteSpec::loadFile); diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.hpp b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.hpp index a773422e..8bbbd5f3 100644 --- a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.hpp +++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLiteSpec.hpp @@ -15,6 +15,8 @@ // Forward declaration of `HybridNitroSQLiteQueryResultSpec` to properly resolve imports. namespace margelo::nitro::rnnitrosqlite { class HybridNitroSQLiteQueryResultSpec; } +// Forward declaration of `HybridNitroSQLitePreparedStatementSpec` to properly resolve imports. +namespace margelo::nitro::rnnitrosqlite { class HybridNitroSQLitePreparedStatementSpec; } // Forward declaration of `BatchQueryResult` to properly resolve imports. namespace margelo::nitro::rnnitrosqlite { struct BatchQueryResult; } // Forward declaration of `BatchQueryCommand` to properly resolve imports. @@ -31,6 +33,7 @@ namespace margelo::nitro::rnnitrosqlite { struct FileLoadResult; } #include #include #include +#include "HybridNitroSQLitePreparedStatementSpec.hpp" #include "BatchQueryResult.hpp" #include "BatchQueryCommand.hpp" #include "FileLoadResult.hpp" @@ -73,6 +76,7 @@ namespace margelo::nitro::rnnitrosqlite { virtual void detach(const std::string& mainDbName, const std::string& alias) = 0; virtual std::shared_ptr execute(const std::string& dbName, const std::string& query, const std::optional, std::string, double>>>& params) = 0; virtual std::shared_ptr>> executeAsync(const std::string& dbName, const std::string& query, const std::optional, std::string, double>>>& params) = 0; + virtual std::shared_ptr prepare(const std::string& dbName, const std::string& query) = 0; virtual BatchQueryResult executeBatch(const std::string& dbName, const std::vector& commands) = 0; virtual std::shared_ptr> executeBatchAsync(const std::string& dbName, const std::vector& commands) = 0; virtual FileLoadResult loadFile(const std::string& dbName, const std::string& location) = 0; diff --git a/packages/react-native-nitro-sqlite/package.json b/packages/react-native-nitro-sqlite/package.json index a3fd5bda..1f884ebb 100644 --- a/packages/react-native-nitro-sqlite/package.json +++ b/packages/react-native-nitro-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-sqlite", - "version": "9.6.0", + "version": "9.7.0", "description": "Fast SQLite library for React Native built using Nitro Modules", "main": "./lib/module/index", "module": "./lib/module/index", @@ -71,7 +71,7 @@ "devDependencies": { "jest": "^30.2.0", "nitrogen": "0.36.1", - "react": "19.2.3", + "react": "19.2.8", "react-native": "0.85.0-rc.0", "react-native-builder-bob": "^0.31.0", "react-native-nitro-modules": "0.36.1" diff --git a/packages/react-native-nitro-sqlite/src/index.ts b/packages/react-native-nitro-sqlite/src/index.ts index 31c26718..280786bc 100644 --- a/packages/react-native-nitro-sqlite/src/index.ts +++ b/packages/react-native-nitro-sqlite/src/index.ts @@ -2,6 +2,7 @@ import { transaction } from './operations/transaction' import { HybridNitroSQLite } from './nitro' import { open } from './operations/session' import { execute, executeAsync } from './operations/execute' +import { prepare } from './operations/prepare' import { init } from './OnLoad' import { executeBatch, executeBatchAsync } from './operations/executeBatch' @@ -17,6 +18,7 @@ export const NitroSQLite = { transaction, execute, executeAsync, + prepare, executeBatch, executeBatchAsync, } diff --git a/packages/react-native-nitro-sqlite/src/operations/execute.ts b/packages/react-native-nitro-sqlite/src/operations/execute.ts index 0521f0e2..9511610e 100644 --- a/packages/react-native-nitro-sqlite/src/operations/execute.ts +++ b/packages/react-native-nitro-sqlite/src/operations/execute.ts @@ -33,7 +33,7 @@ export async function executeAsync( } } -function buildJSQueryResult( +export function buildJSQueryResult( result: NitroSQLiteQueryResult, ): QueryResult { const resultWithRows = result as QueryResult diff --git a/packages/react-native-nitro-sqlite/src/operations/prepare.ts b/packages/react-native-nitro-sqlite/src/operations/prepare.ts new file mode 100644 index 00000000..d1acf8ea --- /dev/null +++ b/packages/react-native-nitro-sqlite/src/operations/prepare.ts @@ -0,0 +1,48 @@ +import { HybridNitroSQLite } from '../nitro' +import NitroSQLiteError from '../NitroSQLiteError' +import type { + PreparedStatement, + QueryResult, + QueryResultRow, + SQLiteQueryParams, +} from '../types' +import { buildJSQueryResult } from './execute' + +export function prepare(dbName: string, query: string): PreparedStatement { + try { + const nativeStatement = HybridNitroSQLite.prepare(dbName, query) + + return { + get isFinalized() { + return nativeStatement.isFinalized + }, + execute: ( + params?: SQLiteQueryParams, + ): QueryResult => { + try { + return buildJSQueryResult(nativeStatement.execute(params)) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } + }, + executeAsync: async ( + params?: SQLiteQueryParams, + ): Promise> => { + try { + return buildJSQueryResult(await nativeStatement.executeAsync(params)) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } + }, + finalize: () => { + try { + nativeStatement.finalize() + } catch (error) { + throw NitroSQLiteError.fromError(error) + } + }, + } + } catch (error) { + throw NitroSQLiteError.fromError(error) + } +} diff --git a/packages/react-native-nitro-sqlite/src/operations/session.ts b/packages/react-native-nitro-sqlite/src/operations/session.ts index 391407f2..0955ecf0 100644 --- a/packages/react-native-nitro-sqlite/src/operations/session.ts +++ b/packages/react-native-nitro-sqlite/src/operations/session.ts @@ -11,6 +11,7 @@ import type { } from '../types' import { execute, executeAsync } from './execute' import { executeBatch, executeBatchAsync } from './executeBatch' +import { prepare } from './prepare' import NitroSQLiteError from '../NitroSQLiteError' import { closeDatabaseQueue, openDatabaseQueue } from '../DatabaseQueue' @@ -47,6 +48,7 @@ export function open( query: string, params?: SQLiteQueryParams, ): Promise> => executeAsync(options.name, query, params), + prepare: (query: string) => prepare(options.name, query), executeBatch: (commands: BatchQueryCommand[]) => executeBatch(options.name, commands), executeBatchAsync: (commands: BatchQueryCommand[]) => diff --git a/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts b/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts index 650493db..9dc4ce6b 100644 --- a/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts +++ b/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts @@ -6,6 +6,7 @@ import type { SQLiteQueryParams, } from '../types' import type { NitroSQLiteQueryResult } from './NitroSQLiteQueryResult.nitro' +import type { NitroSQLitePreparedStatement } from './NitroSQLitePreparedStatement.nitro' export interface NitroSQLite extends HybridObject<{ @@ -32,6 +33,7 @@ export interface NitroSQLite query: string, params?: SQLiteQueryParams, ): Promise + prepare(dbName: string, query: string): NitroSQLitePreparedStatement executeBatch(dbName: string, commands: BatchQueryCommand[]): BatchQueryResult executeBatchAsync( dbName: string, diff --git a/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts b/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts new file mode 100644 index 00000000..7c991dba --- /dev/null +++ b/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts @@ -0,0 +1,15 @@ +import type { HybridObject } from 'react-native-nitro-modules' +import type { SQLiteQueryParams } from '../types' +import type { NitroSQLiteQueryResult } from './NitroSQLiteQueryResult.nitro' + +export interface NitroSQLitePreparedStatement + extends HybridObject<{ + ios: 'c++' + android: 'c++' + }> { + readonly isFinalized: boolean + + execute(params?: SQLiteQueryParams): NitroSQLiteQueryResult + executeAsync(params?: SQLiteQueryParams): Promise + finalize(): void +} diff --git a/packages/react-native-nitro-sqlite/src/types.ts b/packages/react-native-nitro-sqlite/src/types.ts index 517bcca3..7a80ed5a 100644 --- a/packages/react-native-nitro-sqlite/src/types.ts +++ b/packages/react-native-nitro-sqlite/src/types.ts @@ -15,6 +15,7 @@ export interface NitroSQLiteConnection { ) => Promise execute: ExecuteQuery executeAsync: ExecuteAsyncQuery + prepare(query: string): PreparedStatement executeBatch(commands: BatchQueryCommand[]): BatchQueryResult executeBatchAsync(commands: BatchQueryCommand[]): Promise loadFile(location: string): FileLoadResult @@ -68,6 +69,25 @@ export type ExecuteAsyncQuery = ( params?: SQLiteQueryParams, ) => Promise> +export interface PreparedStatement { + readonly isFinalized: boolean + execute: ExecutePreparedStatement + executeAsync: ExecutePreparedStatementAsync + finalize(): void +} + +export type ExecutePreparedStatement = < + Row extends QueryResultRow = QueryResultRow, +>( + params?: SQLiteQueryParams, +) => QueryResult + +export type ExecutePreparedStatementAsync = < + Row extends QueryResultRow = QueryResultRow, +>( + params?: SQLiteQueryParams, +) => Promise> + export interface Transaction { commit(): NitroSQLiteQueryResult rollback(): NitroSQLiteQueryResult diff --git a/scripts/release.sh b/scripts/release.sh index a9f59e9b..edb1b4da 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -euo pipefail +set -eo pipefail echo "Starting the release process..." echo "Provided options: $*"