diff --git a/docs/content/docs/guides/node-test-mock.mdx b/docs/content/docs/guides/node-test-mock.mdx
index 41a2acbb..cfd1140f 100644
--- a/docs/content/docs/guides/node-test-mock.mdx
+++ b/docs/content/docs/guides/node-test-mock.mdx
@@ -38,7 +38,7 @@ test('stores a note', () => {
})
```
-The mock returns query rows through both `results` and `rows`, and supports [atomic batches](/docs/guides/batch-operations). Bound `undefined` becomes SQL NULL. A query that fails throws from `execute()` or rejects from `executeAsync()`.
+The mock returns query rows through both `results` and `rows`, supports [rows as arrays](/docs/guides/parameters-and-results#rows-as-arrays) through `executeRaw()` and `executeRawAsync()`, and supports [atomic batches](/docs/guides/batch-operations). Bound `undefined` becomes SQL NULL. A query that fails throws from `execute()` or rejects from `executeAsync()`.
## Share a database between connections
diff --git a/docs/content/docs/guides/parameters-and-results.mdx b/docs/content/docs/guides/parameters-and-results.mdx
index 9bf62830..a95bafd3 100644
--- a/docs/content/docs/guides/parameters-and-results.mdx
+++ b/docs/content/docs/guides/parameters-and-results.mdx
@@ -31,6 +31,21 @@ for (const person of result.rows._array) {
SQLite INTEGER and REAL result values both arrive as JavaScript numbers. BLOB values arrive as `ArrayBuffer`; NULL values arrive as `null`, even if the parameter was `undefined`. A bound boolean is stored through SQLite's integer binding, so read it as a number and convert it in application code if needed. JavaScript numbers cannot represent every 64-bit SQLite integer exactly; choose a storage representation appropriate for identifiers that exceed the safe integer range.
+## Rows as arrays
+
+A row object holds one value per column name, so when two result columns share a name, the later value replaces the earlier one. This happens in joins such as `SELECT users.id, pets.id ...`. `db.executeRaw()` and `db.executeRawAsync()` take the same arguments as `execute()` and `executeAsync()` but return each row as an array of values in column order, so every column is kept.
+
+```ts
+const { columnNames, rows } = db.executeRaw(
+ 'SELECT users.id, users.name, pets.id, pets.name FROM users LEFT JOIN pets ON pets.owner_id = users.id',
+)
+
+// columnNames: ['id', 'name', 'id', 'name']
+// rows: [[1, 'Ada', 7, 'Rex'], [2, 'Grace', null, null]]
+```
+
+The result has `columnNames`, `rows`, `rowsAffected`, and `insertId`. It has no `rows._array` container and no `metadata`. `columnNames` is filled even when the query returns no rows. Transactions and prepared statements have the same two methods. The exact shape is in [`NitroSQLiteRawQueryResult`](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteRawQueryResult).
+
## Column metadata
For a query with result columns, `metadata` maps column names to `{ name, type, index }`. `index` is the zero-based result column position. The [`ColumnType` values](/api/react-native-nitro-sqlite/enumerations/ColumnType) are `BOOLEAN = 0`, `NUMBER = 1`, `INT64 = 2`, `TEXT = 3`, `ARRAY_BUFFER = 4`, and `NULL_VALUE = 5`. The package exports `ColumnType` as a TypeScript type, not a runtime enum object. In the current native implementation, `metadata.type` is unreliable for detecting a column's declared SQL type. Use your schema for that decision. An expression without a declared type maps to `NULL_VALUE`, which does not mean the result itself is null. The exact shape is in [`NitroSQLiteQueryColumnMetadata`](/api/react-native-nitro-sqlite/type-aliases/NitroSQLiteQueryColumnMetadata).
diff --git a/docs/content/docs/guides/prepared-statements.mdx b/docs/content/docs/guides/prepared-statements.mdx
index 28b20b8d..ed1e8fe0 100644
--- a/docs/content/docs/guides/prepared-statements.mdx
+++ b/docs/content/docs/guides/prepared-statements.mdx
@@ -3,7 +3,7 @@ title: Prepared statements
description: Reuse a compiled SQL statement with different bound values.
---
-Call `db.prepare(query)` to reuse SQL with different parameter values. It returns a statement with `execute(params)`, `executeAsync(params)`, `isFinalized`, and `finalize()`. Preparation is synchronous and throws if the connection has work pending or running. For a query that runs only once, use `db.execute()` or `db.executeAsync()`. See [parameters and prepared statements](/docs/concepts/queries-and-indexes#parameters-and-prepared-statements) for the SQLite concept.
+Call `db.prepare(query)` to reuse SQL with different parameter values. It returns a statement with `execute(params)`, `executeAsync(params)`, `isFinalized`, and `finalize()`, plus `executeRaw(params)` and `executeRawAsync(params)` for [rows as arrays](/docs/guides/parameters-and-results#rows-as-arrays). Preparation is synchronous and throws if the connection has work pending or running. For a query that runs only once, use `db.execute()` or `db.executeAsync()`. See [parameters and prepared statements](/docs/concepts/queries-and-indexes#parameters-and-prepared-statements) for the SQLite concept.
```ts
import { open } from 'react-native-nitro-sqlite'
diff --git a/docs/content/docs/guides/sync-and-async.mdx b/docs/content/docs/guides/sync-and-async.mdx
index 46e96b6e..02428332 100644
--- a/docs/content/docs/guides/sync-and-async.mdx
+++ b/docs/content/docs/guides/sync-and-async.mdx
@@ -5,7 +5,7 @@ description: Pick the query form that fits your workload and understand connecti
Use `db.execute()` for a small query that needs an immediate result, and `await db.executeAsync()` for work that may take longer. A synchronous call blocks its JavaScript caller until SQLite finishes. An asynchronous call returns a promise and lets JavaScript continue while the database work runs in the background.
-`executeBatch` and `loadFile` also have matching `Async` methods. A [prepared statement](/docs/guides/prepared-statements) has `execute()` and `executeAsync()` for repeated SQL.
+`executeRaw`, `executeBatch`, and `loadFile` also have matching `Async` methods. A [prepared statement](/docs/guides/prepared-statements) has `execute()` and `executeAsync()` for repeated SQL.
```ts
import { open } from 'react-native-nitro-sqlite'
@@ -45,7 +45,11 @@ See [prepared statements](/docs/guides/prepared-statements#connection-ordering-a
## Global helpers and native access
-The [`NitroSQLite` export](/api/react-native-nitro-sqlite/variables/NitroSQLite) also has `execute`, `executeAsync`, `prepare`, `executeBatch`, `executeBatchAsync`, and `transaction` helpers that take a database name. `NitroSQLite.open(options)` is the same helper as the named [`open()` export](/api/react-native-nitro-sqlite/functions/open). Global queries join the default connection's JavaScript queue when the name was opened through `open()`. They do not address independent connections; use the object returned by `open()` for those. Without a default managed connection, global `execute` and `executeAsync` call the native methods directly and still need an open native database handle. Global `prepare`, batches, and transactions need a default connection opened through `open()`.
+The [`NitroSQLite` export](/api/react-native-nitro-sqlite/variables/NitroSQLite) also has `execute`, `executeAsync`, `executeRaw`, `executeRawAsync`, `prepare`, `executeBatch`, `executeBatchAsync`, and `transaction` helpers that take a database name. `NitroSQLite.open(options)` is the same helper as the named [`open()` export](/api/react-native-nitro-sqlite/functions/open).
+
+Global queries join the default connection's JavaScript queue when the name was opened through `open()`. This includes `NitroSQLite.executeRaw(dbName, query, params)` and `NitroSQLite.executeRawAsync(dbName, query, params)`. A synchronous query throws while that connection is busy; an asynchronous query waits for an active transaction to finish. Global helpers do not address independent connections; use the object returned by `open()` for those.
+
+Without a default managed connection, global `execute`, `executeAsync`, `executeRaw`, and `executeRawAsync` call the native methods directly and still need an open native database handle. Global `prepare`, batches, and transactions need a default connection opened through `open()`.
`NitroSQLite.native` exposes the underlying [`NitroSQLiteNative` hybrid object](/api/react-native-nitro-sqlite/hybrid-objects/NitroSQLiteNative). Its methods take a database name and return raw results without the connection helper's `rows` container. They also throw native errors without converting them to `NitroSQLiteError`. Its `executeAsync()` accepts an optional `onRows` callback that receives leading rows in batches, in order; the resolved `results` then holds only the rows after the last batch.
diff --git a/docs/content/docs/guides/transactions.mdx b/docs/content/docs/guides/transactions.mdx
index a2395503..efd3a9d5 100644
--- a/docs/content/docs/guides/transactions.mdx
+++ b/docs/content/docs/guides/transactions.mdx
@@ -29,7 +29,7 @@ Throw from the callback when an application check fails. Use `tx` for those chec
## Commit and rollback behavior
-When the callback resolves, the wrapper commits unless you already called `tx.commit()` or `tx.rollback()`. When it rejects or throws, the wrapper rolls back unless the transaction has already been finalized. An explicit `tx.rollback()` does not itself reject the callback: it ends the transaction, and the callback can still resolve. Calls to `tx.execute`, `tx.executeAsync`, `tx.commit`, or `tx.rollback` after finalization throw.
+When the callback resolves, the wrapper commits unless you already called `tx.commit()` or `tx.rollback()`. When it rejects or throws, the wrapper rolls back unless the transaction has already been finalized. An explicit `tx.rollback()` does not itself reject the callback: it ends the transaction, and the callback can still resolve. Calls to `tx.execute`, `tx.executeAsync`, `tx.executeRaw`, `tx.executeRawAsync`, `tx.commit`, or `tx.rollback` after finalization throw.
A failed COMMIT can leave SQLite's transaction active, for example when a deferred foreign-key constraint fails. NitroSQLite marks a commit complete only after SQLite accepts it. A failed manual or automatic commit triggers rollback and rejects with the commit error. This also happens if the callback catches a failed `tx.commit()` and returns without explicitly rolling back. After the failure, only `tx.rollback()` is allowed on that transaction handle.
@@ -39,7 +39,7 @@ If rollback also fails, the rejection preserves the original message and appends
-The connection queue is occupied for the whole callback. Use `tx.execute` or `tx.executeAsync` for every statement on this database during that callback, including statements in helpers.
+The connection queue is occupied for the whole callback. Use the `tx` methods (`execute`, `executeAsync`, `executeRaw`, or `executeRawAsync`) for every statement on this database during that callback, including statements in helpers.
> [!TIP]
> Pass `tx` to any helper that runs statements during the transaction.
@@ -57,7 +57,7 @@ await db.transaction(async (tx) => {
## Await pending statements
-Await every `tx.executeAsync()` before the callback returns or before manually finalizing. While an async statement is pending, `tx.execute()`, `tx.commit()`, and `tx.rollback()` throw instead of running out of order. If the callback throws, the wrapper waits for pending async statements before rolling back. A thrown error after an explicit commit or rollback cannot undo that completed transaction.
+Await every `tx.executeAsync()` and `tx.executeRawAsync()` before the callback returns or before manually finalizing. While an async statement is pending, `tx.execute()`, `tx.executeRaw()`, `tx.commit()`, and `tx.rollback()` throw instead of running out of order. If the callback throws, the wrapper waits for pending async statements before rolling back. A thrown error after an explicit commit or rollback cannot undo that completed transaction.
`tx.commit()` and `tx.rollback()` are synchronous. Their declared return type exposes the native query result fields. The JavaScript helper also adds `rows` at runtime, but that field is not in their declared type. Use them to finalize the transaction, not to retrieve rows. The transaction starts with `BEGIN TRANSACTION`; the global [`NitroSQLite.transaction`](/api/react-native-nitro-sqlite/variables/NitroSQLite) helper also accepts `isExclusive` to use `BEGIN EXCLUSIVE TRANSACTION`. See [transaction modes](/docs/concepts/transactions#transaction-modes) for the SQLite differences.
diff --git a/docs/scripts/check-api.mjs b/docs/scripts/check-api.mjs
index 96fc4c3e..d094e1d7 100644
--- a/docs/scripts/check-api.mjs
+++ b/docs/scripts/check-api.mjs
@@ -22,6 +22,7 @@ const requiredExports = {
'NitroSQLiteNative',
'NitroSQLiteQueryResult',
'NitroSQLiteQueryColumnMetadata',
+ 'NitroSQLiteRawQueryResult',
'TypeOrmNitroSQLiteConnection',
'typeORMDriver',
],
@@ -66,6 +67,8 @@ const requiredMembers = {
'transaction',
'execute',
'executeAsync',
+ 'executeRaw',
+ 'executeRawAsync',
'executeBatch',
'executeBatchAsync',
'close',
diff --git a/example/tests/unit/index.ts b/example/tests/unit/index.ts
index 30d0a01d..977ca3cf 100644
--- a/example/tests/unit/index.ts
+++ b/example/tests/unit/index.ts
@@ -2,6 +2,7 @@ import { beforeEach, describe } from '../TestApi'
import { setupTestDb } from './common'
import registerLoadFileUnitTests from './specs/operations/loadFile.spec'
import registerExecuteUnitTests from './specs/operations/execute.spec'
+import registerExecuteRawUnitTests from './specs/operations/executeRaw.spec'
import registerTransactionUnitTests from './specs/operations/transaction.spec'
import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec'
import registerPreparedStatementUnitTests from './specs/operations/preparedStatement.spec'
@@ -15,6 +16,7 @@ export function registerUnitTests() {
beforeEach(setupTestDb)
describe('operations/execute.spec.ts', registerExecuteUnitTests)
+ describe('operations/executeRaw.spec.ts', registerExecuteRawUnitTests)
describe('operations/transaction.spec.ts', registerTransactionUnitTests)
describe('operations/executeBatch.spec.ts', registerExecuteBatchUnitTests)
describe('operations/loadFile.spec.ts', registerLoadFileUnitTests)
diff --git a/example/tests/unit/specs/operations/executeRaw.spec.ts b/example/tests/unit/specs/operations/executeRaw.spec.ts
new file mode 100644
index 00000000..715feb9f
--- /dev/null
+++ b/example/tests/unit/specs/operations/executeRaw.spec.ts
@@ -0,0 +1,145 @@
+import {
+ expect,
+ isNitroSQLiteError,
+ TEST_ERROR_CODES,
+} from '@tests/unit/common'
+import { describe, it } from '@tests/TestApi'
+import { testDb } from '@tests/db'
+
+const valuesQuery =
+ 'SELECT 1 AS duplicate, 2 AS duplicate, 3.5 AS "café", NULL AS nullable, zeroblob(2) AS payload'
+
+const joinQuery = `
+ SELECT owners.id, owners.name, pets.id, pets.name
+ FROM owners LEFT JOIN pets ON pets.owner_id = owners.id
+ ORDER BY owners.id
+`
+
+function createJoinTables() {
+ testDb.execute('CREATE TABLE owners (id INTEGER PRIMARY KEY, name TEXT)')
+ testDb.execute(
+ 'CREATE TABLE pets (id INTEGER PRIMARY KEY, name TEXT, owner_id INTEGER)',
+ )
+ testDb.execute("INSERT INTO owners VALUES (1, 'Dave'), (2, 'Ritesh')")
+ testDb.execute("INSERT INTO pets VALUES (7, 'Rex', 1)")
+}
+
+function expectValuesRow(row: unknown[] | undefined) {
+ expect(row?.slice(0, 4)).toEqual([1, 2, 3.5, null])
+ expect(Array.from(new Uint8Array(row?.[4] as ArrayBuffer))).toEqual([0, 0])
+}
+
+export default function registerExecuteRawUnitTests() {
+ describe('executeRaw', () => {
+ it('returns every value type and keeps repeated column names', async () => {
+ const result = testDb.executeRaw(valuesQuery)
+ expect(result.columnNames).toEqual([
+ 'duplicate',
+ 'duplicate',
+ 'café',
+ 'nullable',
+ 'payload',
+ ])
+ expect(result.rows).toHaveLength(1)
+ expectValuesRow(result.rows[0])
+
+ const asyncResult = await testDb.executeRawAsync(valuesQuery)
+ expect(asyncResult.columnNames).toEqual(result.columnNames)
+ expectValuesRow(asyncResult.rows[0])
+ })
+
+ it('keeps both sides of a join with matching column names', async () => {
+ createJoinTables()
+ const expected = [
+ [1, 'Dave', 7, 'Rex'],
+ [2, 'Ritesh', null, null],
+ ]
+
+ const result = testDb.executeRaw(joinQuery)
+ expect(result.columnNames).toEqual(['id', 'name', 'id', 'name'])
+ expect(result.rows).toEqual(expected)
+ expect((await testDb.executeRawAsync(joinQuery)).rows).toEqual(expected)
+ })
+
+ it('binds parameters and reports changes for writes', async () => {
+ const inserted = testDb.executeRaw(
+ 'INSERT INTO User (id, name, age, networth) VALUES (?, ?, ?, ?)',
+ [1, 'Ada', 36, 1.5],
+ )
+ expect(inserted.rowsAffected).toBe(1)
+ expect(inserted.insertId).toBe(1)
+ expect(inserted.columnNames).toEqual([])
+ expect(inserted.rows).toEqual([])
+
+ const selected = await testDb.executeRawAsync(
+ 'SELECT name, age FROM User WHERE id = ?',
+ [1],
+ )
+ expect(selected.rows).toEqual([['Ada', 36]])
+ })
+
+ it('returns column names for an empty result', () => {
+ const result = testDb.executeRaw('SELECT id, name FROM User WHERE 0')
+ expect(result.columnNames).toEqual(['id', 'name'])
+ expect(result.rows).toEqual([])
+ })
+
+ it('returns large results in order', async () => {
+ const query = `
+ WITH RECURSIVE counter(value) AS (
+ SELECT 1 UNION ALL SELECT value + 1 FROM counter WHERE value < 10000
+ )
+ SELECT value, 'row-' || value FROM counter
+ `
+ for (const result of [
+ testDb.executeRaw(query),
+ await testDb.executeRawAsync(query),
+ ]) {
+ expect(result.rows).toHaveLength(10_000)
+ expect(result.rows[0]).toEqual([1, 'row-1'])
+ expect(result.rows[9_999]).toEqual([10_000, 'row-10000'])
+ }
+ })
+
+ it('runs inside a transaction', async () => {
+ createJoinTables()
+ await testDb.transaction(async (tx) => {
+ tx.executeRaw("INSERT INTO pets VALUES (8, 'Tom', 2)")
+ expect((await tx.executeRawAsync(joinQuery)).rows).toEqual([
+ [1, 'Dave', 7, 'Rex'],
+ [2, 'Ritesh', 8, 'Tom'],
+ ])
+ })
+ })
+
+ it('runs prepared statements and switches between raw and object rows', async () => {
+ createJoinTables()
+ const statement = testDb.prepare(`${joinQuery} LIMIT ?`)
+ try {
+ expect(statement.executeRaw([1]).rows).toEqual([[1, 'Dave', 7, 'Rex']])
+ expect(statement.execute([1]).results).toEqual([{ id: 7, name: 'Rex' }])
+ const asyncResult = await statement.executeRawAsync([2])
+ expect(asyncResult.columnNames).toEqual(['id', 'name', 'id', 'name'])
+ expect(asyncResult.rows).toHaveLength(2)
+ } finally {
+ statement.finalize()
+ }
+ })
+
+ it('throws NitroSQLiteError for invalid SQL', async () => {
+ try {
+ testDb.executeRaw('SELECT * FROM missing_table')
+ throw new Error(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR)
+ } catch (error) {
+ expect(isNitroSQLiteError(error)).toBe(true)
+ }
+
+ try {
+ await testDb.executeRawAsync('SELECT * FROM missing_table')
+ throw new Error(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION)
+ } catch (error) {
+ expect(isNitroSQLiteError(error)).toBe(true)
+ }
+ })
+ })
+}
diff --git a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.cpp b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.cpp
index 2c8c49fd..798bce81 100644
--- a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.cpp
+++ b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.cpp
@@ -217,7 +217,9 @@ namespace {
// and a smaller final batch shortens the conversion left after the statement completes.
constexpr size_t kRowBatchSize = 256;
- NitroSQLiteQueryResult executeStatement(sqlite3* db, sqlite3_stmt* statement, const SQLiteRowBatchHandler& onRows = nullptr) {
+ // Reads every row of a bound statement. With onRows, full batches go to it and only the rows
+ // after the last batch are returned.
+ SQLiteQueryResultData readStatementRows(sqlite3* db, sqlite3_stmt* statement, const SQLiteRowBatchHandler& onRows) {
int columnCount = 0;
std::vector columnNames;
std::vector rows;
@@ -292,8 +294,14 @@ namespace {
captureColumns();
}
+ return {std::move(columnNames), std::move(rows)};
+ }
+
+ NitroSQLiteQueryResult executeStatement(sqlite3* db, sqlite3_stmt* statement, const SQLiteRowBatchHandler& onRows = nullptr) {
+ auto [columnNames, rows] = readStatementRows(db, statement, onRows);
+
std::optional metadata = std::nullopt;
- for (int i = 0; i < columnCount; i++) {
+ for (int i = 0; i < static_cast(columnNames.size()); i++) {
const std::string& columnName = columnNames[i];
ColumnType columnDeclaredType = mapSQLiteTypeToColumnType(sqlite3_column_decltype(statement, i));
auto columnMeta = NitroSQLiteQueryColumnMetadata(columnName, std::move(columnDeclaredType), i);
@@ -312,21 +320,45 @@ namespace {
return result;
}
+ NitroSQLiteRawQueryResult executeStatementRaw(sqlite3* db, sqlite3_stmt* statement) {
+ auto [columnNames, rows] = readStatementRows(db, statement, nullptr);
+
+ NitroSQLiteRawQueryResult result;
+ result.rowsAffected = sqlite3_changes(db);
+ result.insertId = static_cast(sqlite3_last_insert_rowid(db));
+ result.columnNames = columnNames;
+ result.rows = SQLiteRawQueryResults(SQLiteQueryResults(std::move(columnNames), std::move(rows)));
+ return result;
+ }
+
+ // Runs a query on the connection's cached statement for it, bound to params.
+ template
+ auto executeCached(const SQLiteConnectionPtr& connection, const std::string& query, const std::optional& params,
+ Execute&& execute) {
+ std::lock_guard lock(connection->mutex);
+ sqlite3* db = connection->database;
+ if (db == nullptr) {
+ throw NitroSQLiteException::DatabaseNotOpen(connection->name);
+ }
+
+ CachedStatement statement(*connection, query);
+ if (params) {
+ bindStatement(statement.get(), *params);
+ }
+ return execute(db, statement.get());
+ }
+
} // namespace
NitroSQLiteQueryResult sqliteExecute(const SQLiteConnectionPtr& connection, const std::string& query,
const std::optional& params, const SQLiteRowBatchHandler& onRows) {
- std::lock_guard lock(connection->mutex);
- sqlite3* db = connection->database;
- if (db == nullptr) {
- throw NitroSQLiteException::DatabaseNotOpen(connection->name);
- }
+ return executeCached(connection, query, params,
+ [&](sqlite3* db, sqlite3_stmt* statement) { return executeStatement(db, statement, onRows); });
+}
- CachedStatement statement(*connection, query);
- if (params) {
- bindStatement(statement.get(), *params);
- }
- return executeStatement(db, statement.get(), onRows);
+NitroSQLiteRawQueryResult sqliteExecuteRaw(const SQLiteConnectionPtr& connection, const std::string& query,
+ const std::optional& params) {
+ return executeCached(connection, query, params, executeStatementRaw);
}
SQLiteOperationResult sqliteExecuteCommand(const SQLiteConnectionPtr& connection, const std::string& query,
@@ -379,7 +411,8 @@ SQLitePreparedStatement::~SQLitePreparedStatement() {
finalize();
}
-NitroSQLiteQueryResult SQLitePreparedStatement::execute(const std::optional& params) {
+template
+auto SQLitePreparedStatement::executeBound(const std::optional& params, Execute&& execute) {
std::lock_guard lock(_state->mutex);
std::lock_guard connectionLock(_state->connection->mutex);
@@ -407,13 +440,21 @@ NitroSQLiteQueryResult SQLitePreparedStatement::execute(const std::optionalstatement.get());
+ return execute(database, _state->statement.get());
} catch (...) {
sqlite3_reset(_state->statement.get());
throw;
}
}
+NitroSQLiteQueryResult SQLitePreparedStatement::execute(const std::optional& params) {
+ return executeBound(params, [](sqlite3* database, sqlite3_stmt* statement) { return executeStatement(database, statement); });
+}
+
+NitroSQLiteRawQueryResult SQLitePreparedStatement::executeRaw(const std::optional& params) {
+ return executeBound(params, executeStatementRaw);
+}
+
void SQLitePreparedStatement::finalize() {
std::lock_guard lock(_state->mutex);
std::lock_guard connectionLock(_state->connection->mutex);
diff --git a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.hpp b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.hpp
index 1bbd578a..9631c34e 100644
--- a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.hpp
+++ b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.hpp
@@ -2,6 +2,7 @@
#include "NitroSQLiteDatabaseConnections.hpp"
#include "NitroSQLiteQueryResult.hpp"
+#include "NitroSQLiteRawQueryResult.hpp"
#include "sqlite/sqlite3.h"
#include
#include
@@ -26,6 +27,8 @@ class SQLitePreparedStatement {
/** Reset and execute with new bindings. Throws after finalization or connection closure. */
NitroSQLiteQueryResult execute(const std::optional& params);
+ /** Reset and execute with new bindings, returning rows as arrays of column values. */
+ NitroSQLiteRawQueryResult executeRaw(const std::optional& params);
/** Release the native statement. Repeated calls are safe. */
void finalize();
/** Check whether the native statement has been released. */
@@ -38,6 +41,9 @@ class SQLitePreparedStatement {
explicit SQLitePreparedStatement(std::shared_ptr state);
+ template
+ auto executeBound(const std::optional& params, Execute&& execute);
+
std::shared_ptr _state;
friend std::shared_ptr sqlitePrepare(const SQLiteConnectionPtr& connection, const std::string& query);
@@ -59,6 +65,10 @@ void sqliteDetachDb(const SQLiteConnectionPtr& connection, const std::string& al
NitroSQLiteQueryResult sqliteExecute(const SQLiteConnectionPtr& connection, const std::string& query,
const std::optional& params, const SQLiteRowBatchHandler& onRows = nullptr);
+/** Execute one statement, returning rows as arrays of column values. */
+NitroSQLiteRawQueryResult sqliteExecuteRaw(const SQLiteConnectionPtr& connection, const std::string& query,
+ const std::optional& params);
+
SQLiteOperationResult sqliteExecuteCommand(const SQLiteConnectionPtr& connection, const std::string& query,
const std::optional& params = std::nullopt);
diff --git a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteQueryResults.hpp b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteQueryResults.hpp
index e12712c9..9d1a0342 100644
--- a/packages/react-native-nitro-sqlite/cpp/NitroSQLiteQueryResults.hpp
+++ b/packages/react-native-nitro-sqlite/cpp/NitroSQLiteQueryResults.hpp
@@ -28,6 +28,14 @@ struct SQLiteQueryResults {
std::shared_ptr data;
};
+// Rows converted to JavaScript arrays of column values, which keep repeated column names apart.
+struct SQLiteRawQueryResults {
+ SQLiteRawQueryResults() : data(std::make_shared()) {}
+ explicit SQLiteRawQueryResults(SQLiteQueryResults results) : data(std::move(results.data)) {}
+
+ std::shared_ptr data;
+};
+
/** Receives leading result rows in batches while the statement still runs. */
using SQLiteRowBatchHandler = std::function;
@@ -68,4 +76,30 @@ struct JSIConverter final {
}
};
+template <>
+struct JSIConverter final {
+ static jsi::Value toJSI(jsi::Runtime& runtime, const rnnitrosqlite::SQLiteRawQueryResults& results) {
+ const auto& data = *results.data;
+ jsi::Array array(runtime, data.rows.size());
+ for (size_t rowIndex = 0; rowIndex < data.rows.size(); rowIndex++) {
+ const auto& row = data.rows[rowIndex];
+ jsi::Array values(runtime, row.size());
+ for (size_t columnIndex = 0; columnIndex < row.size(); columnIndex++) {
+ values.setValueAtIndex(runtime, columnIndex, JSIConverter::toJSI(runtime, row[columnIndex]));
+ }
+ array.setValueAtIndex(runtime, rowIndex, std::move(values));
+ }
+ return array;
+ }
+
+ // Query results only travel from native code to JavaScript.
+ static rnnitrosqlite::SQLiteRawQueryResults fromJSI(jsi::Runtime&, const jsi::Value&) {
+ throw std::logic_error("SQLite query results cannot be passed from JavaScript");
+ }
+
+ static bool canConvert(jsi::Runtime&, const jsi::Value&) {
+ return false;
+ }
+};
+
} // namespace margelo::nitro
diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
index df6f21d9..fd2c3c5d 100644
--- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
+++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
@@ -229,6 +229,26 @@ std::shared_ptr> HybridNitroSQLite::executeAsync
});
};
+NitroSQLiteRawQueryResult HybridNitroSQLite::executeRaw(const std::string& dbName, const std::string& query,
+ const std::optional& params) {
+ return sqliteExecuteRaw(_connections.get(dbName), query, params);
+}
+
+std::shared_ptr> HybridNitroSQLite::executeRawAsync(const std::string& dbName, const std::string& query,
+ const std::optional& params) {
+ const auto copiedParams = copyArrayBufferParamsForBackground(params);
+ SQLiteConnectionPtr connection;
+ try {
+ connection = _connections.get(dbName);
+ } catch (...) {
+ return Promise::rejected(std::current_exception());
+ }
+
+ return enqueueConnectionOperation(
+ connection,
+ [connection, query, copiedParams]() -> NitroSQLiteRawQueryResult { return sqliteExecuteRaw(connection, query, copiedParams); });
+}
+
std::shared_ptr HybridNitroSQLite::prepare(const std::string& dbName, const std::string& query) {
return std::make_shared(sqlitePrepare(_connections.get(dbName), query));
}
diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp
index aca8e47d..72c9f6f2 100644
--- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp
+++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.hpp
@@ -73,6 +73,14 @@ class HybridNitroSQLite : public HybridNitroSQLiteSpec {
const std::optional& params,
const std::optional& onRows) override;
+ /** Execute one SQL statement on the calling thread, returning rows as arrays of column values. */
+ NitroSQLiteRawQueryResult executeRaw(const std::string& dbName, const std::string& query,
+ const std::optional& params) override;
+
+ /** Execute one SQL statement on a background thread, returning rows as arrays of column values. */
+ std::shared_ptr> executeRawAsync(const std::string& dbName, const std::string& query,
+ const std::optional& params) override;
+
/** Prepare one SQL statement for repeated execution on an open connection. */
std::shared_ptr prepare(const std::string& dbName, const std::string& query) override;
diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp
index 11caae01..70003b71 100644
--- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp
+++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.cpp
@@ -42,6 +42,19 @@ HybridNitroSQLitePreparedStatement::executeAsync(const std::optional NitroSQLiteQueryResult { return statement->execute(copiedParams); });
}
+NitroSQLiteRawQueryResult HybridNitroSQLitePreparedStatement::executeRaw(const std::optional& params) {
+ return _statement->executeRaw(params);
+}
+
+std::shared_ptr>
+HybridNitroSQLitePreparedStatement::executeRawAsync(const std::optional& params) {
+ const auto copiedParams = copyArrayBufferParamsForBackground(params);
+ const auto statement = _statement;
+
+ return Promise::async(
+ [statement, copiedParams]() -> NitroSQLiteRawQueryResult { return statement->executeRaw(copiedParams); });
+}
+
void HybridNitroSQLitePreparedStatement::finalize() {
_statement->finalize();
}
diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp
index 966acfab..c5e65822 100644
--- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp
+++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLitePreparedStatement.hpp
@@ -17,6 +17,10 @@ class HybridNitroSQLitePreparedStatement : public HybridNitroSQLitePreparedState
NitroSQLiteQueryResult execute(const std::optional& params) override;
/** Execute with new bindings on a background thread. */
std::shared_ptr> executeAsync(const std::optional& params) override;
+ /** Execute with new bindings on the calling thread, returning rows as arrays of column values. */
+ NitroSQLiteRawQueryResult executeRaw(const std::optional& params) override;
+ /** Execute with new bindings on a background thread, returning rows as arrays of column values. */
+ std::shared_ptr> executeRawAsync(const std::optional& params) override;
/** Release the native statement. */
void finalize() override;
/** Check whether the native statement has been released. */
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
index 906479d3..fefa6ac9 100644
--- a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp
+++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.cpp
@@ -17,6 +17,8 @@ namespace margelo::nitro::rnnitrosqlite {
prototype.registerHybridGetter("isFinalized", &HybridNitroSQLitePreparedStatementSpec::getIsFinalized);
prototype.registerHybridMethod("execute", &HybridNitroSQLitePreparedStatementSpec::execute);
prototype.registerHybridMethod("executeAsync", &HybridNitroSQLitePreparedStatementSpec::executeAsync);
+ prototype.registerHybridMethod("executeRaw", &HybridNitroSQLitePreparedStatementSpec::executeRaw);
+ prototype.registerHybridMethod("executeRawAsync", &HybridNitroSQLitePreparedStatementSpec::executeRawAsync);
prototype.registerHybridMethod("finalize", &HybridNitroSQLitePreparedStatementSpec::finalize);
});
}
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
index 5ede22ad..9fde1d9f 100644
--- a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.hpp
+++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/HybridNitroSQLitePreparedStatementSpec.hpp
@@ -15,6 +15,8 @@
// Forward declaration of `NitroSQLiteQueryResult` to properly resolve imports.
namespace margelo::nitro::rnnitrosqlite { struct NitroSQLiteQueryResult; }
+// Forward declaration of `NitroSQLiteRawQueryResult` to properly resolve imports.
+namespace margelo::nitro::rnnitrosqlite { struct NitroSQLiteRawQueryResult; }
#include "NitroSQLiteQueryResult.hpp"
#include
@@ -24,6 +26,7 @@ namespace margelo::nitro::rnnitrosqlite { struct NitroSQLiteQueryResult; }
#include
#include
#include
+#include "NitroSQLiteRawQueryResult.hpp"
namespace margelo::nitro::rnnitrosqlite {
@@ -58,6 +61,8 @@ namespace margelo::nitro::rnnitrosqlite {
// Methods
virtual NitroSQLiteQueryResult execute(const std::optional, std::string, double>>>>& params) = 0;
virtual std::shared_ptr> executeAsync(const std::optional, std::string, double>>>>& params) = 0;
+ virtual NitroSQLiteRawQueryResult executeRaw(const std::optional, std::string, double>>>>& params) = 0;
+ virtual std::shared_ptr> executeRawAsync(const std::optional, std::string, double>>>>& params) = 0;
virtual void finalize() = 0;
protected:
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 1297822c..a5cf0e27 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
@@ -23,6 +23,8 @@ namespace margelo::nitro::rnnitrosqlite {
prototype.registerHybridMethod("detach", &HybridNitroSQLiteSpec::detach);
prototype.registerHybridMethod("execute", &HybridNitroSQLiteSpec::execute);
prototype.registerHybridMethod("executeAsync", &HybridNitroSQLiteSpec::executeAsync);
+ prototype.registerHybridMethod("executeRaw", &HybridNitroSQLiteSpec::executeRaw);
+ prototype.registerHybridMethod("executeRawAsync", &HybridNitroSQLiteSpec::executeRawAsync);
prototype.registerHybridMethod("prepare", &HybridNitroSQLiteSpec::prepare);
prototype.registerHybridMethod("executeBatch", &HybridNitroSQLiteSpec::executeBatch);
prototype.registerHybridMethod("executeBatchAsync", &HybridNitroSQLiteSpec::executeBatchAsync);
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 87856cee..b49be09b 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 `NitroSQLiteQueryResult` to properly resolve imports.
namespace margelo::nitro::rnnitrosqlite { struct NitroSQLiteQueryResult; }
+// Forward declaration of `NitroSQLiteRawQueryResult` to properly resolve imports.
+namespace margelo::nitro::rnnitrosqlite { struct NitroSQLiteRawQueryResult; }
// Forward declaration of `HybridNitroSQLitePreparedStatementSpec` to properly resolve imports.
namespace margelo::nitro::rnnitrosqlite { class HybridNitroSQLitePreparedStatementSpec; }
// Forward declaration of `BatchQueryResult` to properly resolve imports.
@@ -34,6 +36,7 @@ namespace margelo::nitro::rnnitrosqlite { struct FileLoadResult; }
#include
#include "NitroSQLiteQueryResults.hpp"
#include
+#include "NitroSQLiteRawQueryResult.hpp"
#include
#include "HybridNitroSQLitePreparedStatementSpec.hpp"
#include "BatchQueryResult.hpp"
@@ -80,6 +83,8 @@ namespace margelo::nitro::rnnitrosqlite {
virtual void detach(const std::string& mainDbName, const std::string& alias) = 0;
virtual NitroSQLiteQueryResult 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, const std::optional>& onRows) = 0;
+ virtual NitroSQLiteRawQueryResult executeRaw(const std::string& dbName, const std::string& query, const std::optional, std::string, double>>>>& params) = 0;
+ virtual std::shared_ptr> executeRawAsync(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;
diff --git a/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/NitroSQLiteRawQueryResult.hpp b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/NitroSQLiteRawQueryResult.hpp
new file mode 100644
index 00000000..dfed87c6
--- /dev/null
+++ b/packages/react-native-nitro-sqlite/nitrogen/generated/shared/c++/NitroSQLiteRawQueryResult.hpp
@@ -0,0 +1,98 @@
+///
+/// NitroSQLiteRawQueryResult.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
+#if __has_include()
+#include
+#else
+#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
+#endif
+#if __has_include()
+#include
+#else
+#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
+#endif
+#if __has_include()
+#include
+#else
+#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
+#endif
+
+
+
+#include
+#include
+#include
+#include "NitroSQLiteQueryResults.hpp"
+
+namespace margelo::nitro::rnnitrosqlite {
+
+ /**
+ * A struct which can be represented as a JavaScript object (NitroSQLiteRawQueryResult).
+ */
+ struct NitroSQLiteRawQueryResult final {
+ public:
+ double rowsAffected SWIFT_PRIVATE;
+ std::optional insertId SWIFT_PRIVATE;
+ std::vector columnNames SWIFT_PRIVATE;
+ margelo::nitro::rnnitrosqlite::SQLiteRawQueryResults rows SWIFT_PRIVATE;
+
+ public:
+ NitroSQLiteRawQueryResult() = default;
+ explicit NitroSQLiteRawQueryResult(double rowsAffected, std::optional insertId, std::vector columnNames, margelo::nitro::rnnitrosqlite::SQLiteRawQueryResults rows): rowsAffected(rowsAffected), insertId(insertId), columnNames(columnNames), rows(rows) {}
+
+ public:
+ // NitroSQLiteRawQueryResult is not equatable because these properties are not equatable: rows
+ };
+
+} // namespace margelo::nitro::rnnitrosqlite
+
+namespace margelo::nitro {
+
+ // C++ NitroSQLiteRawQueryResult <> JS NitroSQLiteRawQueryResult (object)
+ template <>
+ struct JSIConverter final {
+ static inline margelo::nitro::rnnitrosqlite::NitroSQLiteRawQueryResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
+ jsi::Object obj = arg.asObject(runtime);
+ return margelo::nitro::rnnitrosqlite::NitroSQLiteRawQueryResult(
+ JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rowsAffected"))),
+ JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "insertId"))),
+ JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "columnNames"))),
+ JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rows")))
+ );
+ }
+ static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::rnnitrosqlite::NitroSQLiteRawQueryResult& arg) {
+ jsi::Object obj(runtime);
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "rowsAffected"), JSIConverter::toJSI(runtime, arg.rowsAffected));
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "insertId"), JSIConverter>::toJSI(runtime, arg.insertId));
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "columnNames"), JSIConverter>::toJSI(runtime, arg.columnNames));
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "rows"), JSIConverter::toJSI(runtime, arg.rows));
+ return obj;
+ }
+ static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
+ if (!value.isObject()) {
+ return false;
+ }
+ jsi::Object obj = value.getObject(runtime);
+ if (!nitro::isPlainObject(runtime, obj)) {
+ return false;
+ }
+ if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rowsAffected")))) return false;
+ if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "insertId")))) return false;
+ if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "columnNames")))) return false;
+ if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rows")))) return false;
+ return true;
+ }
+ };
+
+} // namespace margelo::nitro
diff --git a/packages/react-native-nitro-sqlite/src/__mocks__/nitro.ts b/packages/react-native-nitro-sqlite/src/__mocks__/nitro.ts
index 3c52ddf1..9309d54f 100644
--- a/packages/react-native-nitro-sqlite/src/__mocks__/nitro.ts
+++ b/packages/react-native-nitro-sqlite/src/__mocks__/nitro.ts
@@ -8,6 +8,8 @@ export const HybridNitroSQLite = {
detach: jest.fn(),
execute: jest.fn(),
executeAsync: jest.fn(),
+ executeRaw: jest.fn(),
+ executeRawAsync: jest.fn(),
prepare: jest.fn(),
executeBatch: jest.fn(),
executeBatchAsync: jest.fn(),
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts
index 1cfe6048..299207ae 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts
@@ -18,6 +18,7 @@ import {
NitroSQLiteExceptionType as ExceptionTypeFromErrors,
} from '../errors'
import { HybridNitroSQLite } from '../nitro'
+import { executeRaw, executeRawAsync } from '../operations/execute'
import { init as androidInit } from '../OnLoad.android'
import { init as iosInit } from '../OnLoad'
@@ -32,6 +33,8 @@ describe('entrypoints', () => {
expect(NitroSQLite.native).toBe(HybridNitroSQLite)
expect(NitroSQLite.open).toBe(open)
expect(NitroSQLite.open).not.toBe(HybridNitroSQLite.open)
+ expect(NitroSQLite.executeRaw).toBe(executeRaw)
+ expect(NitroSQLite.executeRawAsync).toBe(executeRawAsync)
expect(iosInit()).toBeUndefined()
})
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/executeRaw.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/executeRaw.test.ts
new file mode 100644
index 00000000..0982390e
--- /dev/null
+++ b/packages/react-native-nitro-sqlite/src/__tests__/executeRaw.test.ts
@@ -0,0 +1,112 @@
+jest.mock('../nitro')
+
+import { NitroSQLite } from '../index'
+import { HybridNitroSQLite } from '../nitro'
+import {
+ closeDatabaseQueue,
+ isDatabaseOpen,
+ openDatabaseQueue,
+} from '../DatabaseQueue'
+import NitroSQLiteError from '../NitroSQLiteError'
+import { deferred, nativeRawResult, nativeResult } from './testUtils'
+
+const dbName = 'execute-raw-test'
+const query = 'SELECT ? AS value'
+const raw = nativeRawResult(['value'], [[7]])
+
+beforeEach(() => {
+ jest.clearAllMocks()
+ jest.mocked(HybridNitroSQLite.executeRaw).mockReturnValue(raw)
+ jest.mocked(HybridNitroSQLite.executeRawAsync).mockResolvedValue(raw)
+ jest.mocked(HybridNitroSQLite.execute).mockReturnValue(nativeResult())
+ jest.mocked(HybridNitroSQLite.executeAsync).mockResolvedValue(nativeResult())
+})
+
+afterEach(() => {
+ if (isDatabaseOpen(dbName)) closeDatabaseQueue(dbName)
+})
+
+it('forwards unmanaged raw queries and parameters without adapting their rows', async () => {
+ expect(NitroSQLite.executeRaw(dbName, query, [7])).toBe(raw)
+ await expect(NitroSQLite.executeRawAsync(dbName, query, [7])).resolves.toBe(
+ raw,
+ )
+ expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledWith(dbName, query, [7])
+ expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
+ dbName,
+ query,
+ [7],
+ )
+})
+
+it('rejects global synchronous raw queries while managed work is pending', async () => {
+ openDatabaseQueue(dbName)
+ expect(NitroSQLite.executeRaw(dbName, query)).toBe(raw)
+
+ const gate = deferred()
+ jest
+ .mocked(HybridNitroSQLite.executeRawAsync)
+ .mockReturnValueOnce(gate.promise)
+ const pending = NitroSQLite.executeRawAsync(dbName, query)
+ try {
+ expect(() => NitroSQLite.executeRaw(dbName, 'SELECT blocked')).toThrow(
+ 'busy',
+ )
+ expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledTimes(1)
+ } finally {
+ gate.resolve(raw)
+ await pending
+ }
+})
+
+it('keeps global raw queries outside an active managed transaction', async () => {
+ openDatabaseQueue(dbName)
+ const entered = deferred()
+ const finish = deferred()
+ const transaction = NitroSQLite.transaction(dbName, async () => {
+ entered.resolve()
+ await finish.promise
+ })
+ await entered.promise
+
+ const pending = NitroSQLite.executeRawAsync(dbName, query)
+ try {
+ expect(() => NitroSQLite.executeRaw(dbName, query)).toThrow('busy')
+ expect(HybridNitroSQLite.executeRaw).not.toHaveBeenCalled()
+ expect(HybridNitroSQLite.executeRawAsync).not.toHaveBeenCalled()
+ } finally {
+ finish.resolve()
+ await transaction
+ await pending
+ }
+ expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
+ dbName,
+ query,
+ undefined,
+ )
+ expect(
+ jest.mocked(HybridNitroSQLite.execute).mock.invocationCallOrder[0],
+ ).toBeLessThan(
+ jest.mocked(HybridNitroSQLite.executeRawAsync).mock.invocationCallOrder[0]!,
+ )
+})
+
+it('normalizes native errors and releases the managed queue after failure', async () => {
+ openDatabaseQueue(dbName)
+ jest.mocked(HybridNitroSQLite.executeRaw).mockImplementationOnce(() => {
+ throw new Error('raw failed')
+ })
+ expect(() => NitroSQLite.executeRaw(dbName, query)).toThrow(NitroSQLiteError)
+ expect(NitroSQLite.executeRaw(dbName, query)).toBe(raw)
+
+ jest
+ .mocked(HybridNitroSQLite.executeRawAsync)
+ .mockRejectedValueOnce('raw async failed')
+ await expect(
+ NitroSQLite.executeRawAsync(dbName, query),
+ ).rejects.toMatchObject({
+ name: 'NitroSQLiteError',
+ message: 'raw async failed',
+ })
+ await expect(NitroSQLite.executeRawAsync(dbName, query)).resolves.toBe(raw)
+})
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/mock.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/mock.test.ts
index 24e14d71..64b55214 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/mock.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/mock.test.ts
@@ -29,6 +29,86 @@ describe('Node SQLite mock', () => {
expect(NitroSQLite.open).toBe(open)
})
+ it('returns raw rows that keep repeated column names', async () => {
+ const database = open({ name: 'raw' })
+ database.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
+ database.execute(
+ 'CREATE TABLE pets (id INTEGER PRIMARY KEY, name TEXT, owner_id INTEGER, photo BLOB)',
+ )
+ database.execute(
+ "INSERT INTO users (id, name) VALUES (1, 'Dave'), (2, 'Ritesh')",
+ )
+ database.execute(
+ 'INSERT INTO pets (id, name, owner_id, photo) VALUES (?, ?, ?, ?)',
+ [7, 'Rex', 1, new Uint8Array([1, 2]).buffer],
+ )
+ const query =
+ 'SELECT users.id, users.name, pets.id, pets.name, pets.photo FROM users LEFT JOIN pets ON pets.owner_id = users.id ORDER BY users.id'
+
+ const result = database.executeRaw(query)
+ expect(result.columnNames).toEqual(['id', 'name', 'id', 'name', 'photo'])
+ expect(result.rows).toEqual([
+ [1, 'Dave', 7, 'Rex', expect.any(ArrayBuffer)],
+ [2, 'Ritesh', null, null, null],
+ ])
+ expect(new Uint8Array(result.rows[0]![4] as ArrayBuffer)).toEqual(
+ new Uint8Array([1, 2]),
+ )
+ await expect(database.executeRawAsync(query)).resolves.toEqual(result)
+
+ expect(
+ database.executeRaw("INSERT INTO users (name) VALUES ('Ada')"),
+ ).toMatchObject({ rowsAffected: 1, insertId: 3, columnNames: [], rows: [] })
+ })
+
+ it('switches a prepared statement between raw and object rows', async () => {
+ const database = open({ name: 'raw-prepared' })
+ const statement = database.prepare('SELECT ? AS a, ? AS b')
+
+ expect(statement.executeRaw([1, 2]).rows).toEqual([[1, 2]])
+ expect(statement.execute([3, 4]).results).toEqual([{ a: 3, b: 4 }])
+ await expect(statement.executeRawAsync([5, 6])).resolves.toMatchObject({
+ columnNames: ['a', 'b'],
+ rows: [[5, 6]],
+ })
+ statement.finalize()
+ })
+
+ it('runs raw queries inside transactions', async () => {
+ const database = open({ name: 'raw-transaction' })
+ database.execute('CREATE TABLE items (value INTEGER)')
+
+ await database.transaction(async (tx) => {
+ tx.executeRaw('INSERT INTO items (value) VALUES (?)', [1])
+ expect(
+ (await tx.executeRawAsync('SELECT value FROM items')).rows,
+ ).toEqual([[1]])
+ })
+ })
+
+ it.each([0, 1])(
+ 'refreshes raw prepared column names after a schema change, id=%s',
+ async (id) => {
+ const database = open({ name: 'raw-schema-change' })
+ database.execute('CREATE TABLE items (id INTEGER)')
+ database.execute('INSERT INTO items VALUES (1)')
+ const statement = database.prepare('SELECT * FROM items WHERE id = ?')
+ database.execute(
+ "ALTER TABLE items ADD COLUMN label TEXT DEFAULT 'added'",
+ )
+
+ const result = statement.executeRaw([id])
+ expect(result.columnNames).toEqual(['id', 'label'])
+ expect(result.rows).toEqual(id === 1 ? [[1, 'added']] : [])
+
+ database.execute('ALTER TABLE items RENAME COLUMN label TO name')
+ const asyncResult = await statement.executeRawAsync([id])
+ expect(asyncResult.columnNames).toEqual(['id', 'name'])
+ expect(asyncResult.rows).toEqual(result.rows)
+ statement.finalize()
+ },
+ )
+
it('binds repeated named parameters in first-occurrence order', () => {
const database = open({ name: 'named' })
expect(
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/prepare.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/prepare.test.ts
index 270d0e1d..f6902e6c 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/prepare.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/prepare.test.ts
@@ -9,7 +9,7 @@ import {
import type { NitroSQLitePreparedStatement } from '../specs/NitroSQLitePreparedStatement.nitro'
import { prepare } from '../operations/prepare'
import { open } from '../operations/session'
-import { nativeResult } from './testUtils'
+import { nativeRawResult, nativeResult } from './testUtils'
const dbName = 'prepare-test'
const query = 'SELECT ? AS value'
@@ -35,6 +35,8 @@ function mockStatement(): NitroSQLitePreparedStatement {
},
execute: jest.fn(() => nativeResult([{ value: 7 }])),
executeAsync: jest.fn(async () => nativeResult([{ value: 8 }])),
+ executeRaw: jest.fn(() => nativeRawResult(['value'], [[9]])),
+ executeRawAsync: jest.fn(async () => nativeRawResult(['value'], [[10]])),
finalize: jest.fn(() => {
finalized = true
}),
@@ -95,6 +97,33 @@ it('keeps synchronous calls from overtaking an asynchronous execution', async ()
statement.finalize()
})
+it('runs raw executions through the statement queue', async () => {
+ const native = mockStatement()
+ jest.mocked(HybridNitroSQLite.prepare).mockReturnValue(native)
+ const statement = prepare(dbName, query)
+
+ expect(statement.executeRaw([9]).rows).toEqual([[9]])
+ const pending = statement.executeRawAsync([10])
+ expect(() => statement.executeRaw()).toThrow('busy with another operation')
+ await expect(pending).resolves.toMatchObject({
+ columnNames: ['value'],
+ rows: [[10]],
+ })
+ expect(native.executeRaw).toHaveBeenCalledWith([9])
+ expect(native.executeRawAsync).toHaveBeenCalledWith([10])
+
+ jest.mocked(native.executeRaw).mockImplementationOnce(() => {
+ throw new Error('raw failed')
+ })
+ expect(() => statement.executeRaw()).toThrow('raw failed')
+ jest.mocked(native.executeRawAsync).mockRejectedValueOnce('async raw failed')
+ await expect(statement.executeRawAsync()).rejects.toMatchObject({
+ name: 'NitroSQLiteError',
+ message: 'async raw failed',
+ })
+ statement.finalize()
+})
+
it('converts preparation and execution errors', async () => {
jest.mocked(HybridNitroSQLite.prepare).mockImplementationOnce(() => {
throw new Error('prepare failed')
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/session.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/session.test.ts
index ab114c76..57af2526 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/session.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/session.test.ts
@@ -5,7 +5,7 @@ import { closeDatabaseQueue, isDatabaseOpen } from '../DatabaseQueue'
import { open } from '../operations/session'
import { execute } from '../operations/execute'
-import { deferred, nativeResult } from './testUtils'
+import { deferred, nativeRawResult, nativeResult } from './testUtils'
const dbName = 'session-test'
const options = { name: dbName, location: 'data' }
@@ -89,6 +89,45 @@ describe('open', () => {
expect(isDatabaseOpen(dbName)).toBe(false)
})
+ it('routes raw queries through the connection queue', async () => {
+ const db = open(options)
+ const raw = nativeRawResult(['id', 'id'], [[1, 7]])
+ jest.mocked(HybridNitroSQLite.executeRaw).mockReturnValue(raw)
+ jest.mocked(HybridNitroSQLite.executeRawAsync).mockResolvedValue(raw)
+
+ expect(db.executeRaw('SELECT 1', [1])).toBe(raw)
+ await expect(db.executeRawAsync('SELECT 2')).resolves.toBe(raw)
+ expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledWith(
+ dbName,
+ 'SELECT 1',
+ [1],
+ )
+ expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
+ dbName,
+ 'SELECT 2',
+ undefined,
+ )
+
+ const pending = db.executeRawAsync('SELECT 3')
+ expect(() => db.executeRaw('SELECT 4')).toThrow(
+ 'busy with another operation',
+ )
+ await pending
+
+ jest.mocked(HybridNitroSQLite.executeRaw).mockImplementationOnce(() => {
+ throw new Error('raw failed')
+ })
+ expect(() => db.executeRaw('SELECT 5')).toThrow('raw failed')
+ jest
+ .mocked(HybridNitroSQLite.executeRawAsync)
+ .mockRejectedValueOnce('async raw failed')
+ await expect(db.executeRawAsync('SELECT 6')).rejects.toMatchObject({
+ name: 'NitroSQLiteError',
+ message: 'async raw failed',
+ })
+ db.close()
+ })
+
it('delegates transactions through the connection', async () => {
const db = open(options)
await expect(
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/testUtils.ts b/packages/react-native-nitro-sqlite/src/__tests__/testUtils.ts
index ae4d24f2..08e14a25 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/testUtils.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/testUtils.ts
@@ -1,4 +1,7 @@
-import type { NitroSQLiteQueryResult } from '../specs/NitroSQLiteQueryResult.nitro'
+import type {
+ NitroSQLiteQueryResult,
+ NitroSQLiteRawQueryResult,
+} from '../specs/NitroSQLiteQueryResult.nitro'
import type { SQLiteValue } from '../types'
export function nativeResult(
@@ -7,6 +10,17 @@ export function nativeResult(
return { rowsAffected: results.length, results } as NitroSQLiteQueryResult
}
+export function nativeRawResult(
+ columnNames: string[] = [],
+ rows: SQLiteValue[][] = [],
+): NitroSQLiteRawQueryResult {
+ return {
+ rowsAffected: rows.length,
+ columnNames,
+ rows,
+ } as NitroSQLiteRawQueryResult
+}
+
export function deferred() {
let resolve!: (value: T) => void
let reject!: (reason: unknown) => void
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/transaction.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/transaction.test.ts
index 52d1cc6b..6553dd1e 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/transaction.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/transaction.test.ts
@@ -4,7 +4,8 @@ import Database from 'better-sqlite3'
import { HybridNitroSQLite } from '../nitro'
import { closeDatabaseQueue, openDatabaseQueue } from '../DatabaseQueue'
import { transaction } from '../operations/transaction'
-import { deferred, nativeResult } from './testUtils'
+import type { Transaction } from '../types'
+import { deferred, nativeRawResult, nativeResult } from './testUtils'
const dbName = 'transaction-test'
@@ -61,6 +62,44 @@ describe('transaction', () => {
)
})
+ it('runs raw queries inside the transaction with the same guards', async () => {
+ const raw = nativeRawResult(['value'], [[1]])
+ const pendingQuery = deferred()
+ jest.mocked(HybridNitroSQLite.executeRaw).mockReturnValue(raw)
+ jest
+ .mocked(HybridNitroSQLite.executeRawAsync)
+ .mockReturnValue(pendingQuery.promise)
+
+ let handle: Transaction | undefined
+ await transaction(dbName, async (tx) => {
+ handle = tx
+ expect(tx.executeRaw('SELECT ?', [1])).toBe(raw)
+ const pending = tx.executeRawAsync('SELECT ?', [2])
+ expect(() => tx.executeRaw('SELECT sync')).toThrow(
+ 'Await all tx.executeAsync',
+ )
+ pendingQuery.resolve(raw)
+ await expect(pending).resolves.toBe(raw)
+ })
+
+ expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledWith(
+ dbName,
+ 'SELECT ?',
+ [1],
+ )
+ expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
+ dbName,
+ 'SELECT ?',
+ [2],
+ )
+ expect(() => handle!.executeRaw('SELECT 1')).toThrow(
+ 'finalized transaction',
+ )
+ expect(() => handle!.executeRawAsync('SELECT 1')).toThrow(
+ 'finalized transaction',
+ )
+ })
+
it('rejects synchronous transaction work until earlier async queries settle', async () => {
const pendingQuery = deferred>()
jest
diff --git a/packages/react-native-nitro-sqlite/src/index.ts b/packages/react-native-nitro-sqlite/src/index.ts
index 1ab6af75..173bcc42 100644
--- a/packages/react-native-nitro-sqlite/src/index.ts
+++ b/packages/react-native-nitro-sqlite/src/index.ts
@@ -1,7 +1,12 @@
import { transaction } from './operations/transaction'
import { HybridNitroSQLite } from './nitro'
import { open } from './operations/session'
-import { execute, executeAsync } from './operations/execute'
+import {
+ execute,
+ executeAsync,
+ executeRaw,
+ executeRawAsync,
+} from './operations/execute'
import { prepare } from './operations/prepare'
import { init } from './OnLoad'
import { executeBatch, executeBatchAsync } from './operations/executeBatch'
@@ -20,6 +25,8 @@ export const NitroSQLite = {
transaction,
execute,
executeAsync,
+ executeRaw,
+ executeRawAsync,
prepare,
executeBatch,
executeBatchAsync,
@@ -34,6 +41,8 @@ export type {
NitroSQLiteQueryResult,
NitroSQLiteQueryRows,
NitroSQLiteQueryColumnMetadata,
+ NitroSQLiteRawQueryResult,
+ NitroSQLiteRawQueryRows,
} from './specs/NitroSQLiteQueryResult.nitro'
export type { TypeOrmNitroSQLiteConnection } from './typeORM'
export type * from './types'
diff --git a/packages/react-native-nitro-sqlite/src/mock.ts b/packages/react-native-nitro-sqlite/src/mock.ts
index 0971a4a7..4ff95e89 100644
--- a/packages/react-native-nitro-sqlite/src/mock.ts
+++ b/packages/react-native-nitro-sqlite/src/mock.ts
@@ -18,7 +18,13 @@ import {
import NitroSQLiteError from './NitroSQLiteError'
import type { NitroSQLiteConnectionOptions, PreparedStatement } from './types'
import type { MockConnection } from './mock/MockConnection'
-import { executeBatch, executePrepared, executeQuery } from './mock/query'
+import {
+ executeBatch,
+ executePrepared,
+ executePreparedRaw,
+ executeQuery,
+ executeRawQuery,
+} from './mock/query'
import { runTransaction } from './mock/transaction'
export type { MockConnection } from './mock/MockConnection'
@@ -110,6 +116,10 @@ export function open(options: NitroSQLiteConnectionOptions): MockConnection {
runSync(() => executeQuery(database, query, params)),
executeAsync: (query, params) =>
runAsync(() => executeQuery(database, query, params)),
+ executeRaw: (query, params) =>
+ runSync(() => executeRawQuery(database, query, params)),
+ executeRawAsync: (query, params) =>
+ runAsync(() => executeRawQuery(database, query, params)),
executeBatch: (commands) => runSync(() => executeBatch(database, commands)),
executeBatchAsync: (commands) =>
runAsync(() => executeBatch(database, commands), true),
@@ -122,12 +132,19 @@ export function open(options: NitroSQLiteConnectionOptions): MockConnection {
throw new NitroSQLiteError('Prepared statement is finalized.')
return executePrepared(database, statement, params)
}
+ const executeRaw: PreparedStatement['executeRaw'] = (params) => {
+ if (!statement)
+ throw new NitroSQLiteError('Prepared statement is finalized.')
+ return executePreparedRaw(database, statement, params)
+ }
return {
get isFinalized() {
return statement === undefined
},
execute: (params) => runSync(() => execute(params)),
executeAsync: (params) => runAsync(() => execute(params), true),
+ executeRaw: (params) => runSync(() => executeRaw(params)),
+ executeRawAsync: (params) => runAsync(() => executeRaw(params), true),
finalize: () =>
runSync(() => {
// better-sqlite3 releases statement resources when the object is collected.
diff --git a/packages/react-native-nitro-sqlite/src/mock/MockConnection.ts b/packages/react-native-nitro-sqlite/src/mock/MockConnection.ts
index fbfc449a..c0c71ef9 100644
--- a/packages/react-native-nitro-sqlite/src/mock/MockConnection.ts
+++ b/packages/react-native-nitro-sqlite/src/mock/MockConnection.ts
@@ -13,6 +13,8 @@ export type MockConnection = Pick<
| 'delete'
| 'execute'
| 'executeAsync'
+ | 'executeRaw'
+ | 'executeRawAsync'
| 'executeBatch'
| 'executeBatchAsync'
| 'prepare'
diff --git a/packages/react-native-nitro-sqlite/src/mock/query.ts b/packages/react-native-nitro-sqlite/src/mock/query.ts
index 05bf33b6..517bc48f 100644
--- a/packages/react-native-nitro-sqlite/src/mock/query.ts
+++ b/packages/react-native-nitro-sqlite/src/mock/query.ts
@@ -1,4 +1,5 @@
import type BetterSqlite3 from 'better-sqlite3'
+import type { NitroSQLiteRawQueryResult } from '../specs/NitroSQLiteQueryResult.nitro'
import type {
BatchQueryCommand,
QueryResult,
@@ -9,6 +10,7 @@ import type {
type Database = InstanceType
type Statement = BetterSqlite3.Statement>
+type RawStatement = BetterSqlite3.Statement
type BoundValue = string | number | Buffer | null
type ExpandedBatchCommand = { query: string; params?: SQLiteQueryParams }
@@ -37,6 +39,48 @@ export function executePrepared(
} as QueryResult
}
+/** @internal Execute an already prepared statement, returning rows as arrays of column values. */
+export function executePreparedRaw(
+ database: Database,
+ statement: Statement,
+ params?: SQLiteQueryParams,
+): NitroSQLiteRawQueryResult {
+ const bindings = getBindings(statement.source, params)
+ let columnNames: string[] = []
+ let rows: SQLiteValue[][] = []
+ if (statement.reader) {
+ try {
+ // better-sqlite3's types keep the object row type after raw(true).
+ const rawStatement = statement.raw(true) as unknown as RawStatement
+ const rawRows = rawStatement.all(...bindings)
+ // Execution may reprepare the statement after a schema change.
+ columnNames = statement.columns().map((column) => column.name)
+ rows = rawRows.map((row) =>
+ row.map((value, index) => normalizeValue(value, columnNames[index]!)),
+ )
+ } finally {
+ // Raw mode stays on the statement, and prepared statements also serve execute().
+ statement.raw(false)
+ }
+ } else statement.run(...bindings)
+ const { rowsAffected, insertId } = getDatabaseChanges(database)
+ return {
+ rowsAffected,
+ insertId,
+ columnNames,
+ rows,
+ } as NitroSQLiteRawQueryResult
+}
+
+/** @internal Execute one SQL statement and return rows as arrays of column values. */
+export function executeRawQuery(
+ database: Database,
+ query: string,
+ params?: SQLiteQueryParams,
+): NitroSQLiteRawQueryResult {
+ return executePreparedRaw(database, database.prepare(query), params)
+}
+
/** @internal Execute one SQL statement and return Nitro-shaped data. */
export function executeQuery(
database: Database,
@@ -178,24 +222,28 @@ function toBoundValue(value: SQLiteValue): BoundValue {
function normalizeRow(row: Record): QueryResultRow {
const result: QueryResultRow = {}
for (const [key, value] of Object.entries(row)) {
- if (Buffer.isBuffer(value)) {
- const bytes = new ArrayBuffer(value.byteLength)
- new Uint8Array(bytes).set(value)
- result[key] = bytes
- } else if (
- value === null ||
- typeof value === 'string' ||
- typeof value === 'number' ||
- typeof value === 'boolean'
- ) {
- result[key] = value
- } else {
- throw new Error(`Unsupported SQLite value in column ${key}.`)
- }
+ result[key] = normalizeValue(value, key)
}
return result
}
+function normalizeValue(value: unknown, column: string): SQLiteValue {
+ if (Buffer.isBuffer(value)) {
+ const bytes = new ArrayBuffer(value.byteLength)
+ new Uint8Array(bytes).set(value)
+ return bytes
+ }
+ if (
+ value === null ||
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value
+ }
+ throw new Error(`Unsupported SQLite value in column ${column}.`)
+}
+
function getChangeCount(database: Database): number {
return getDatabaseChanges(database).rowsAffected
}
diff --git a/packages/react-native-nitro-sqlite/src/mock/transaction.ts b/packages/react-native-nitro-sqlite/src/mock/transaction.ts
index a9099b62..c425cc0d 100644
--- a/packages/react-native-nitro-sqlite/src/mock/transaction.ts
+++ b/packages/react-native-nitro-sqlite/src/mock/transaction.ts
@@ -1,7 +1,7 @@
import type BetterSqlite3 from 'better-sqlite3'
import NitroSQLiteError from '../NitroSQLiteError'
import type { QueryResultRow, SQLiteQueryParams, Transaction } from '../types'
-import { executeQuery } from './query'
+import { executeQuery, executeRawQuery } from './query'
/** @internal Run a callback while the connection's exclusive queue item owns it. */
export async function runTransaction(
@@ -21,6 +21,14 @@ export async function runTransaction(
)
}
}
+ const track = (operation: Promise): Promise => {
+ pending.add(operation)
+ operation.then(
+ () => pending.delete(operation),
+ () => pending.delete(operation),
+ )
+ return operation
+ }
const finish = (query: 'COMMIT' | 'ROLLBACK') => {
assertSync()
const result = executeQuery(database, query)
@@ -37,15 +45,21 @@ export async function runTransaction(
params?: SQLiteQueryParams,
) => {
assertActive()
- const operation = Promise.resolve().then(() =>
- executeQuery(database, query, params),
+ return track(
+ Promise.resolve().then(() =>
+ executeQuery(database, query, params),
+ ),
)
- pending.add(operation)
- operation.then(
- () => pending.delete(operation),
- () => pending.delete(operation),
+ },
+ executeRaw: (query, params) => {
+ assertSync()
+ return executeRawQuery(database, query, params)
+ },
+ executeRawAsync: async (query, params) => {
+ assertActive()
+ return track(
+ Promise.resolve().then(() => executeRawQuery(database, query, params)),
)
- return operation
},
commit: () => finish('COMMIT'),
rollback: () => finish('ROLLBACK'),
diff --git a/packages/react-native-nitro-sqlite/src/operations/execute.ts b/packages/react-native-nitro-sqlite/src/operations/execute.ts
index 47e0f86c..0aff4973 100644
--- a/packages/react-native-nitro-sqlite/src/operations/execute.ts
+++ b/packages/react-native-nitro-sqlite/src/operations/execute.ts
@@ -1,7 +1,10 @@
import { HybridNitroSQLite } from '../nitro'
import type { QueryResult, QueryResultRow, SQLiteQueryParams } from '../types'
import NitroSQLiteError from '../NitroSQLiteError'
-import type { NitroSQLiteQueryResult } from '../specs/NitroSQLiteQueryResult.nitro'
+import type {
+ NitroSQLiteQueryResult,
+ NitroSQLiteRawQueryResult,
+} from '../specs/NitroSQLiteQueryResult.nitro'
import {
isDatabaseOpen,
queueStatementAsync,
@@ -108,6 +111,94 @@ export async function executeAsyncNative(
}
}
+/** Execute one SQL statement synchronously by database name, returning rows as arrays.
+ * Uses the default managed connection's queue when the database was opened through `open()`.
+ * Otherwise, calls the open native database directly.
+ * @param dbName Name of an open database.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Values bound to the placeholders.
+ * @returns Rows and column names in SQL column order, with affected row count and insert ID.
+ * @throws NitroSQLiteError if the managed connection is busy or the query fails.
+ */
+export function executeRaw(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+): NitroSQLiteRawQueryResult {
+ if (!isDatabaseOpen(dbName)) {
+ return executeRawNative(dbName, query, params)
+ }
+
+ return executeRawManaged(dbName, query, params)
+}
+
+export function executeRawManaged(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+ queueKey: DatabaseQueueKey = dbName,
+): NitroSQLiteRawQueryResult {
+ return startOperationSync(queueKey, () =>
+ executeRawNative(dbName, query, params),
+ )
+}
+
+export function executeRawNative(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+): NitroSQLiteRawQueryResult {
+ try {
+ return HybridNitroSQLite.executeRaw(dbName, query, params)
+ } catch (error) {
+ throw NitroSQLiteError.fromError(error)
+ }
+}
+
+/** Execute one SQL statement asynchronously by database name, returning rows as arrays.
+ * Uses the default managed connection's queue when the database was opened through `open()`.
+ * Otherwise, calls the open native database directly.
+ * @param dbName Name of an open database.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Values bound to the placeholders.
+ * @returns A promise of rows and column names in SQL column order, with affected row count and insert ID.
+ * @throws NitroSQLiteError as a rejected promise if the query fails.
+ */
+export async function executeRawAsync(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+): Promise {
+ if (!isDatabaseOpen(dbName)) {
+ return executeRawAsyncNative(dbName, query, params)
+ }
+
+ return executeRawAsyncManaged(dbName, query, params)
+}
+
+export async function executeRawAsyncManaged(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+ queueKey: DatabaseQueueKey = dbName,
+): Promise {
+ return queueStatementAsync(queueKey, () =>
+ executeRawAsyncNative(dbName, query, params),
+ )
+}
+
+export async function executeRawAsyncNative(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+): Promise {
+ try {
+ return await HybridNitroSQLite.executeRawAsync(dbName, query, params)
+ } catch (error) {
+ throw NitroSQLiteError.fromError(error)
+ }
+}
+
export function buildJSQueryResult(
result: NitroSQLiteQueryResult,
leadingRows?: Row[],
diff --git a/packages/react-native-nitro-sqlite/src/operations/prepare.ts b/packages/react-native-nitro-sqlite/src/operations/prepare.ts
index 3451a8df..ce0e5de7 100644
--- a/packages/react-native-nitro-sqlite/src/operations/prepare.ts
+++ b/packages/react-native-nitro-sqlite/src/operations/prepare.ts
@@ -6,6 +6,7 @@ import type {
QueryResultRow,
SQLiteQueryParams,
} from '../types'
+import type { NitroSQLiteRawQueryResult } from '../specs/NitroSQLiteQueryResult.nitro'
import { buildJSQueryResult } from './execute'
import { queueOperationAsync, startOperationSync } from '../DatabaseQueue'
import type { DatabaseQueueKey } from '../DatabaseQueue'
@@ -55,6 +56,26 @@ export function prepare(
throw NitroSQLiteError.fromError(error)
}
},
+ executeRaw: (params?: SQLiteQueryParams): NitroSQLiteRawQueryResult => {
+ try {
+ return startOperationSync(queueKey, () =>
+ nativeStatement.executeRaw(params),
+ )
+ } catch (error) {
+ throw NitroSQLiteError.fromError(error)
+ }
+ },
+ executeRawAsync: async (
+ params?: SQLiteQueryParams,
+ ): Promise => {
+ try {
+ return await queueOperationAsync(queueKey, () =>
+ nativeStatement.executeRawAsync(params),
+ )
+ } catch (error) {
+ throw NitroSQLiteError.fromError(error)
+ }
+ },
finalize: () => {
try {
startOperationSync(queueKey, () => nativeStatement.finalize())
diff --git a/packages/react-native-nitro-sqlite/src/operations/session.ts b/packages/react-native-nitro-sqlite/src/operations/session.ts
index cfc02703..7a84e004 100644
--- a/packages/react-native-nitro-sqlite/src/operations/session.ts
+++ b/packages/react-native-nitro-sqlite/src/operations/session.ts
@@ -9,7 +9,12 @@ import type {
QueryResultRow,
QueryResult,
} from '../types'
-import { executeAsyncManaged, executeManaged } from './execute'
+import {
+ executeAsyncManaged,
+ executeManaged,
+ executeRawAsyncManaged,
+ executeRawManaged,
+} from './execute'
import { executeBatch, executeBatchAsync } from './executeBatch'
import { prepare } from './prepare'
import NitroSQLiteError from '../NitroSQLiteError'
@@ -145,6 +150,14 @@ export function open(
runOperation(() =>
executeAsyncManaged(connectionId, query, params, queueKey),
),
+ executeRaw: (query: string, params?: SQLiteQueryParams) =>
+ runOperation(() =>
+ executeRawManaged(connectionId, query, params, queueKey),
+ ),
+ executeRawAsync: async (query: string, params?: SQLiteQueryParams) =>
+ runOperation(() =>
+ executeRawAsyncManaged(connectionId, query, params, queueKey),
+ ),
prepare: (query: string) =>
runOperation(() => prepare(connectionId, query, queueKey)),
executeBatch: (commands: BatchQueryCommand[]) =>
diff --git a/packages/react-native-nitro-sqlite/src/operations/transaction.ts b/packages/react-native-nitro-sqlite/src/operations/transaction.ts
index 9f879bf2..0c617ade 100644
--- a/packages/react-native-nitro-sqlite/src/operations/transaction.ts
+++ b/packages/react-native-nitro-sqlite/src/operations/transaction.ts
@@ -5,7 +5,13 @@ import type {
QueryResult,
QueryResultRow,
} from '../types'
-import { executeAsyncNative, executeNative } from './execute'
+import {
+ executeAsyncNative,
+ executeNative,
+ executeRawAsyncNative,
+ executeRawNative,
+} from './execute'
+import type { NitroSQLiteRawQueryResult } from '../specs/NitroSQLiteQueryResult.nitro'
import NitroSQLiteError from '../NitroSQLiteError'
import type { DatabaseQueueKey } from '../DatabaseQueue'
@@ -44,29 +50,25 @@ export const transaction = async (
}
}
- const executeOnTransaction = (
- query: string,
- params?: SQLiteQueryParams,
- ): QueryResult => {
+ const throwIfNotActive = () => {
if (state.current.kind !== 'active') {
throw new NitroSQLiteError(
`Cannot execute query on finalized transaction: ${dbName}`,
)
}
+ }
+
+ const runOnTransaction = (run: () => Value): Value => {
+ throwIfNotActive()
throwIfAsyncPending()
- return executeNative(dbName, query, params)
+ return run()
}
- const executeAsyncOnTransaction = (
- query: string,
- params?: SQLiteQueryParams,
- ): Promise> => {
- if (state.current.kind !== 'active') {
- throw new NitroSQLiteError(
- `Cannot execute query on finalized transaction: ${dbName}`,
- )
- }
- const pending = executeAsyncNative(dbName, query, params)
+ const runAsyncOnTransaction = (
+ run: () => Promise,
+ ): Promise => {
+ throwIfNotActive()
+ const pending = run()
pendingAsyncStatements.add(pending)
pending.then(
() => pendingAsyncStatements.delete(pending),
@@ -75,6 +77,30 @@ export const transaction = async (
return pending
}
+ const executeOnTransaction = (
+ query: string,
+ params?: SQLiteQueryParams,
+ ): QueryResult =>
+ runOnTransaction(() => executeNative(dbName, query, params))
+
+ const executeAsyncOnTransaction = (
+ query: string,
+ params?: SQLiteQueryParams,
+ ): Promise> =>
+ runAsyncOnTransaction(() => executeAsyncNative(dbName, query, params))
+
+ const executeRawOnTransaction = (
+ query: string,
+ params?: SQLiteQueryParams,
+ ): NitroSQLiteRawQueryResult =>
+ runOnTransaction(() => executeRawNative(dbName, query, params))
+
+ const executeRawAsyncOnTransaction = (
+ query: string,
+ params?: SQLiteQueryParams,
+ ): Promise =>
+ runAsyncOnTransaction(() => executeRawAsyncNative(dbName, query, params))
+
const commit = () => {
if (state.current.kind !== 'active') {
throw new NitroSQLiteError(
@@ -130,6 +156,8 @@ export const transaction = async (
commit,
execute: executeOnTransaction,
executeAsync: executeAsyncOnTransaction,
+ executeRaw: executeRawOnTransaction,
+ executeRawAsync: executeRawAsyncOnTransaction,
rollback,
})
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 38f4986d..a9bd407f 100644
--- a/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts
+++ b/packages/react-native-nitro-sqlite/src/specs/NitroSQLite.nitro.ts
@@ -8,6 +8,7 @@ import type {
import type {
NitroSQLiteQueryResult,
NitroSQLiteQueryRows,
+ NitroSQLiteRawQueryResult,
} from './NitroSQLiteQueryResult.nitro'
import type { NitroSQLitePreparedStatement } from './NitroSQLitePreparedStatement.nitro'
@@ -108,6 +109,28 @@ export interface NitroSQLite
params?: SQLiteQueryParams,
onRows?: (rows: NitroSQLiteQueryRows) => void,
): Promise
+ /** Execute one SQL statement synchronously on the calling thread, returning rows as arrays of column values.
+ * @param dbName Name of an open database.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Positional values bound to SQL placeholders.
+ * @returns Column names, rows as value arrays, affected row count, and insert ID.
+ */
+ executeRaw(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+ ): NitroSQLiteRawQueryResult
+ /** Execute one SQL statement on a background thread, returning rows as arrays of column values.
+ * @param dbName Name of an open database.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Positional values bound to SQL placeholders.
+ * @returns A promise of the native raw query result.
+ */
+ executeRawAsync(
+ dbName: string,
+ query: string,
+ params?: SQLiteQueryParams,
+ ): Promise
/** Prepare one SQL statement on an open native connection for repeated execution.
* Finalize the returned statement before closing its connection.
* @param dbName Name or ID of an open database connection.
diff --git a/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts b/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts
index de545bd9..e4ce3c62 100644
--- a/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts
+++ b/packages/react-native-nitro-sqlite/src/specs/NitroSQLitePreparedStatement.nitro.ts
@@ -1,6 +1,9 @@
import type { HybridObject } from 'react-native-nitro-modules'
import type { SQLiteQueryParams } from '../types'
-import type { NitroSQLiteQueryResult } from './NitroSQLiteQueryResult.nitro'
+import type {
+ NitroSQLiteQueryResult,
+ NitroSQLiteRawQueryResult,
+} from './NitroSQLiteQueryResult.nitro'
/** Native prepared statement returned by `NitroSQLite.native.prepare()`. */
export interface NitroSQLitePreparedStatement
@@ -23,6 +26,18 @@ export interface NitroSQLitePreparedStatement
* @returns A promise of the native query result.
*/
executeAsync(params?: SQLiteQueryParams): Promise
+ /** Execute on the calling thread like `execute`, returning rows as arrays of column values.
+ * @param params Values bound to positional placeholders.
+ * @returns Column names, rows as value arrays, affected row count, and insert ID.
+ */
+ executeRaw(params?: SQLiteQueryParams): NitroSQLiteRawQueryResult
+ /** Execute on a background thread like `executeAsync`, returning rows as arrays of column values.
+ * @param params Values bound to positional placeholders.
+ * @returns A promise of the native raw query result.
+ */
+ executeRawAsync(
+ params?: SQLiteQueryParams,
+ ): Promise
/** Release the native statement. Repeated calls are safe. */
finalize(): void
}
diff --git a/packages/react-native-nitro-sqlite/src/specs/NitroSQLiteQueryResult.nitro.ts b/packages/react-native-nitro-sqlite/src/specs/NitroSQLiteQueryResult.nitro.ts
index efab64c1..970b86e9 100644
--- a/packages/react-native-nitro-sqlite/src/specs/NitroSQLiteQueryResult.nitro.ts
+++ b/packages/react-native-nitro-sqlite/src/specs/NitroSQLiteQueryResult.nitro.ts
@@ -24,6 +24,27 @@ export interface NitroSQLiteQueryResult {
readonly metadata?: Record
}
+/** Result rows as arrays of column values, in column order. */
+export type NitroSQLiteRawQueryRows = CustomType<
+ SQLiteValue[][],
+ 'margelo::nitro::rnnitrosqlite::SQLiteRawQueryResults',
+ { include: 'NitroSQLiteQueryResults.hpp' }
+>
+
+/** Native result of one SQL statement, with rows as arrays of column values. */
+export interface NitroSQLiteRawQueryResult {
+ /** SQLite's latest row change count. For a read-only query it may reflect an earlier write. */
+ readonly rowsAffected: number
+ /** Last insert row ID for this connection. It may refer to an earlier statement. */
+ readonly insertId?: number
+
+ /** Result column names in column order. Repeated names are kept. */
+ readonly columnNames: string[]
+
+ /** Rows as arrays of column values, in the order of `columnNames`. */
+ readonly rows: NitroSQLiteRawQueryRows
+}
+
// TODO: Investigate why this cannot be represented in Nitro
// export type NitroQueryResultRow = {
// [key: string]: SQLiteValue
diff --git a/packages/react-native-nitro-sqlite/src/types.ts b/packages/react-native-nitro-sqlite/src/types.ts
index 7ce24aab..51448151 100644
--- a/packages/react-native-nitro-sqlite/src/types.ts
+++ b/packages/react-native-nitro-sqlite/src/types.ts
@@ -1,4 +1,7 @@
-import type { NitroSQLiteQueryResult } from './specs/NitroSQLiteQueryResult.nitro'
+import type {
+ NitroSQLiteQueryResult,
+ NitroSQLiteRawQueryResult,
+} from './specs/NitroSQLiteQueryResult.nitro'
/** Options for `open()` and `NitroSQLite.open()`. */
export interface NitroSQLiteConnectionOptions {
@@ -63,6 +66,10 @@ export interface NitroSQLiteConnection {
execute: ExecuteQuery
/** Queue one SQL statement and resolve with its result. */
executeAsync: ExecuteAsyncQuery
+ /** Execute one SQL statement synchronously, returning rows as arrays of column values. Throws while the connection is busy. */
+ executeRaw: ExecuteRawQuery
+ /** Queue one SQL statement and resolve with rows as arrays of column values. */
+ executeRawAsync: ExecuteRawAsyncQuery
/**
* Prepare one SQL statement on this connection for repeated execution.
* Finalize the returned statement before closing the connection.
@@ -174,6 +181,25 @@ export type ExecuteAsyncQuery = (
params?: SQLiteQueryParams,
) => Promise>
+/** Execute one SQL statement synchronously and return rows as arrays of column values.
+ * Repeated column names are kept, in the order of `columnNames`.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Values bound to the placeholders.
+ */
+export type ExecuteRawQuery = (
+ query: string,
+ params?: SQLiteQueryParams,
+) => NitroSQLiteRawQueryResult
+
+/** Queue one SQL statement and resolve with rows as arrays of column values.
+ * @param query SQL statement with optional positional placeholders.
+ * @param params Values bound to the placeholders.
+ */
+export type ExecuteRawAsyncQuery = (
+ query: string,
+ params?: SQLiteQueryParams,
+) => Promise
+
/**
* A reusable SQL statement bound to the connection that prepared it. Each
* execution resets the statement and its bindings before applying new values.
@@ -186,6 +212,10 @@ export interface PreparedStatement {
execute: ExecutePreparedStatement
/** Queue execution on a background thread and resolve with the query result. */
executeAsync: ExecutePreparedStatementAsync
+ /** Execute on the calling thread like `execute`, returning rows as arrays of column values. */
+ executeRaw: ExecuteRawPreparedStatement
+ /** Queue execution like `executeAsync` and resolve with rows as arrays of column values. */
+ executeRawAsync: ExecuteRawPreparedStatementAsync
/** Release the native statement. Calling this more than once is safe. */
finalize(): void
}
@@ -212,6 +242,23 @@ export type ExecutePreparedStatementAsync = <
params?: SQLiteQueryParams,
) => Promise>
+/**
+ * Execute a prepared statement and return rows as arrays of column values.
+ * Each call replaces the previous parameter bindings.
+ * @param params Values bound to the statement's positional placeholders.
+ */
+export type ExecuteRawPreparedStatement = (
+ params?: SQLiteQueryParams,
+) => NitroSQLiteRawQueryResult
+
+/**
+ * Queue a prepared statement execution and resolve with rows as arrays of column values.
+ * @param params Values bound to the statement's positional placeholders.
+ */
+export type ExecuteRawPreparedStatementAsync = (
+ params?: SQLiteQueryParams,
+) => Promise
+
/** Handle valid only while its transaction callback is active. */
export interface Transaction {
/** Commit now. Marks completion only after SQLite accepts COMMIT.
@@ -229,6 +276,10 @@ export interface Transaction {
execute: ExecuteQuery
/** Execute within this transaction on a background thread. */
executeAsync: ExecuteAsyncQuery
+ /** Execute within this transaction on the calling thread, returning rows as arrays of column values. */
+ executeRaw: ExecuteRawQuery
+ /** Execute within this transaction on a background thread, returning rows as arrays of column values. */
+ executeRawAsync: ExecuteRawAsyncQuery
}
/** One command in a batch. A nested `params` array repeats the query for each parameter set. */