Skip to content

Commit 17b7732

Browse files
committed
fix: report zero rows affected for read-only commands
1 parent 1945cf5 commit 17b7732

5 files changed

Lines changed: 139 additions & 160 deletions

File tree

example/tests/unit/specs/operations/executeBatch.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ export default function registerExecuteBatchUnitTests() {
4242
])
4343
})
4444

45+
it('reports zero rows affected for read-only commands', () => {
46+
const id = chance.integer()
47+
testDb.execute(
48+
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
49+
[id, chance.name(), chance.integer(), chance.floating()],
50+
)
51+
52+
const result = testDb.executeBatch([
53+
{
54+
query: 'SELECT * FROM User WHERE id = ?',
55+
params: [id],
56+
},
57+
])
58+
59+
expect(result.rowsAffected).toBe(0)
60+
})
61+
4562
it('Async batch execute', async () => {
4663
const id1 = chance.integer()
4764
const name1 = chance.name()

packages/react-native-nitro-sqlite/cpp/importSqlFile.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,27 @@ SQLiteOperationResult importSqlFile(const std::string& dbName, const std::string
1717
try {
1818
int rowsAffected = 0;
1919
int commands = 0;
20-
sqliteExecuteLiteral(dbName, "BEGIN EXCLUSIVE TRANSACTION");
20+
sqliteExecuteCommand(dbName, "BEGIN EXCLUSIVE TRANSACTION");
2121
while (std::getline(sqFile, line, '\n')) {
2222
if (!line.empty()) {
2323
try {
24-
SQLiteOperationResult result = sqliteExecuteLiteral(dbName, line);
24+
SQLiteOperationResult result = sqliteExecuteCommand(dbName, line);
2525
rowsAffected += result.rowsAffected;
2626
commands++;
2727
} catch (NitroSQLiteException& e) {
28-
sqliteExecuteLiteral(dbName, "ROLLBACK");
28+
sqliteExecuteCommand(dbName, "ROLLBACK");
2929
sqFile.close();
3030
throw NitroSQLiteException::CouldNotLoadFile(fileLocation, "Transaction was rolled back");
3131
}
3232
}
3333
}
3434

3535
sqFile.close();
36-
sqliteExecuteLiteral(dbName, "COMMIT");
36+
sqliteExecuteCommand(dbName, "COMMIT");
3737
return {.rowsAffected = rowsAffected, .commands = commands};
3838
} catch (...) {
3939
sqFile.close();
40-
sqliteExecuteLiteral(dbName, "ROLLBACK");
40+
sqliteExecuteCommand(dbName, "ROLLBACK");
4141
throw NitroSQLiteException(NitroSQLiteExceptionType::UnknownError, "Unexpected error. Transaction was rolled back");
4242
}
4343
} else {

packages/react-native-nitro-sqlite/cpp/operations.cpp

Lines changed: 108 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <iostream>
1010
#include <limits>
1111
#include <map>
12+
#include <memory>
1213
#include <optional>
1314
#include <sqlite3.h>
1415
#include <sstream>
@@ -77,13 +78,13 @@ void sqliteCloseAll() {
7778
void sqliteAttachDb(const std::string& mainDBName, const std::string& docPath, const std::string& databaseToAttach,
7879
const std::string& alias) {
7980
/**
80-
* There is no need to check if mainDBName is opened because sqliteExecuteLiteral will do that.
81+
* There is no need to check if mainDBName is opened because sqliteExecuteCommand will do that.
8182
* */
8283
std::string dbPath = get_db_path(databaseToAttach, docPath);
8384
std::string statement = "ATTACH DATABASE '" + dbPath + "' AS " + alias;
8485

8586
try {
86-
sqliteExecuteLiteral(mainDBName, statement);
87+
sqliteExecuteCommand(mainDBName, statement);
8788
} catch (NitroSQLiteException& e) {
8889
throw NitroSQLiteException(NitroSQLiteExceptionType::UnableToAttachToDatabase,
8990
mainDBName + " was unable to attach another database: " + std::string(e.what()));
@@ -92,12 +93,12 @@ void sqliteAttachDb(const std::string& mainDBName, const std::string& docPath, c
9293

9394
void sqliteDetachDb(const std::string& mainDBName, const std::string& alias) {
9495
/**
95-
* There is no need to check if mainDBName is opened because sqliteExecuteLiteral will do that.
96+
* There is no need to check if mainDBName is opened because sqliteExecuteCommand will do that.
9697
* */
9798
std::string statement = "DETACH DATABASE " + alias;
9899

99100
try {
100-
sqliteExecuteLiteral(mainDBName, statement);
101+
sqliteExecuteCommand(mainDBName, statement);
101102
} catch (NitroSQLiteException& e) {
102103
throw NitroSQLiteException(NitroSQLiteExceptionType::UnableToAttachToDatabase,
103104
mainDBName + " was unable to detach database: " + std::string(e.what()));
@@ -144,173 +145,135 @@ void bindStatement(sqlite3_stmt* statement, const SQLiteQueryParams& values) {
144145
}
145146
}
146147

147-
std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
148-
const std::optional<SQLiteQueryParams>& params) {
149-
if (dbMap.count(dbName) == 0) {
150-
throw NitroSQLiteException::DatabaseNotOpen(dbName);
151-
}
152-
153-
auto db = dbMap[dbName];
148+
namespace {
154149

155-
sqlite3_stmt* statement;
156-
int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &statement, NULL);
157-
if (statementStatus == SQLITE_OK) // statement is correct, bind the passed parameters
158-
{
159-
if (params) {
160-
bindStatement(statement, *params);
150+
struct SQLiteStatementFinalizer {
151+
void operator()(sqlite3_stmt* statement) const noexcept {
152+
if (statement != nullptr) {
153+
sqlite3_finalize(statement);
154+
}
161155
}
162-
} else {
163-
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
164-
}
156+
};
165157

166-
auto isConsuming = true;
167-
auto isFailed = false;
158+
using SQLiteStatement = std::unique_ptr<sqlite3_stmt, SQLiteStatementFinalizer>;
168159

169-
int result, i, count, column_type;
170-
std::string column_name;
171-
ColumnType column_declared_type;
172-
SQLiteQueryResultRow row;
173-
SQLiteQueryResults results;
174-
std::optional<SQLiteQueryTableMetadata> metadata = std::nullopt;
175-
176-
while (isConsuming) {
177-
result = sqlite3_step(statement);
178-
179-
switch (result) {
180-
case SQLITE_ROW:
181-
i = 0;
182-
row = std::unordered_map<std::string, SQLiteValue>();
183-
count = sqlite3_column_count(statement);
184-
185-
while (i < count) {
186-
column_type = sqlite3_column_type(statement, i);
187-
column_name = sqlite3_column_name(statement, i);
188-
switch (column_type) {
189-
190-
case SQLITE_INTEGER: {
191-
auto column_value = sqlite3_column_double(statement, i);
192-
row[column_name] = column_value;
193-
break;
194-
}
195-
case SQLITE_FLOAT: {
196-
auto column_value = sqlite3_column_double(statement, i);
197-
row[column_name] = column_value;
198-
break;
199-
}
200-
case SQLITE_TEXT: {
201-
auto column_value = reinterpret_cast<const char*>(sqlite3_column_text(statement, i));
202-
sqlite3_column_bytes(statement, i);
203-
row[column_name] = column_value;
204-
break;
205-
}
206-
case SQLITE_BLOB: {
207-
int blob_size = sqlite3_column_bytes(statement, i);
208-
const void* blob = sqlite3_column_blob(statement, i);
209-
// Copy the SQLite BLOB into a new native ArrayBuffer.
210-
// This avoids manual memory management and unsafe pointer handling.
211-
if (blob_size > 0) {
212-
const auto* blob_data = reinterpret_cast<const uint8_t*>(blob);
213-
row[column_name] = ArrayBuffer::copy(blob_data, static_cast<size_t>(blob_size));
214-
} else {
215-
// Represent empty BLOBs as an empty, but valid, ArrayBuffer.
216-
row[column_name] = ArrayBuffer::allocate(0);
217-
}
218-
break;
219-
}
220-
case SQLITE_NULL:
221-
// Intentionally left blank to switch to default case
222-
default:
223-
row[column_name] = NullType::null;
224-
break;
225-
}
226-
i++;
227-
}
228-
results.push_back(std::move(row));
229-
break;
230-
case SQLITE_DONE:
231-
i = 0;
232-
count = sqlite3_column_count(statement);
233-
while (i < count) {
234-
column_name = sqlite3_column_name(statement, i);
235-
const char* tp = sqlite3_column_decltype(statement, i);
236-
column_declared_type = mapSQLiteTypeToColumnType(tp);
237-
auto columnMeta = NitroSQLiteQueryColumnMetadata(std::move(column_name), std::move(column_declared_type), i);
238-
239-
if (!metadata) {
240-
metadata = std::make_optional<SQLiteQueryTableMetadata>();
241-
}
242-
metadata->insert({column_name, columnMeta});
243-
i++;
244-
}
245-
isConsuming = false;
246-
break;
247-
default:
248-
isFailed = true;
249-
isConsuming = false;
160+
sqlite3* getOpenDatabase(const std::string& dbName) {
161+
if (dbMap.count(dbName) == 0) {
162+
throw NitroSQLiteException::DatabaseNotOpen(dbName);
250163
}
164+
165+
return dbMap[dbName];
251166
}
252167

253-
sqlite3_finalize(statement);
168+
SQLiteStatement prepareStatement(sqlite3* db, const std::string& query, const std::optional<SQLiteQueryParams>& params) {
169+
sqlite3_stmt* rawStatement = nullptr;
170+
int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &rawStatement, nullptr);
171+
SQLiteStatement statement(rawStatement);
254172

255-
if (isFailed) {
256-
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
257-
}
173+
if (statementStatus != SQLITE_OK) {
174+
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
175+
}
258176

259-
int rowsAffected = sqlite3_changes(db);
260-
long long latestInsertRowId = sqlite3_last_insert_rowid(db);
261-
return std::make_shared<HybridNitroSQLiteQueryResult>(results, static_cast<double>(latestInsertRowId), rowsAffected, metadata);
262-
}
177+
if (params) {
178+
bindStatement(statement.get(), *params);
179+
}
263180

264-
SQLiteOperationResult sqliteExecuteLiteral(const std::string& dbName, const std::string& query) {
265-
// Check if db connection is opened
266-
if (dbMap.count(dbName) == 0) {
267-
throw NitroSQLiteException::DatabaseNotOpen(dbName);
181+
return statement;
268182
}
269183

270-
sqlite3* db = dbMap[dbName];
184+
template <typename OnRow>
185+
void consumeStatement(sqlite3* db, sqlite3_stmt* statement, OnRow&& onRow) {
186+
while (true) {
187+
int result = sqlite3_step(statement);
271188

272-
// SQLite statements need to be compiled before executed
273-
sqlite3_stmt* statement;
189+
if (result == SQLITE_ROW) {
190+
onRow(statement);
191+
continue;
192+
}
274193

275-
// Compile and move result into statement memory spot
276-
int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &statement, NULL);
194+
if (result == SQLITE_DONE) {
195+
return;
196+
}
277197

278-
if (statementStatus != SQLITE_OK) // statemnet is correct, bind the passed parameters
279-
{
280-
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
198+
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
199+
}
281200
}
282201

283-
bool isConsuming = true;
284-
bool isFailed = false;
285-
286-
int result;
287-
std::string column_name;
202+
} // namespace
288203

289-
while (isConsuming) {
290-
result = sqlite3_step(statement);
204+
std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
205+
const std::optional<SQLiteQueryParams>& params) {
206+
auto db = getOpenDatabase(dbName);
207+
auto statement = prepareStatement(db, query, params);
208+
SQLiteQueryResults results;
291209

292-
switch (result) {
293-
case SQLITE_ROW:
294-
isConsuming = true;
295-
break;
210+
consumeStatement(db, statement.get(), [&](sqlite3_stmt* currentStatement) {
211+
SQLiteQueryResultRow row;
212+
int count = sqlite3_column_count(currentStatement);
213+
214+
for (int i = 0; i < count; i++) {
215+
int columnType = sqlite3_column_type(currentStatement, i);
216+
std::string columnName = sqlite3_column_name(currentStatement, i);
217+
218+
switch (columnType) {
219+
case SQLITE_INTEGER:
220+
case SQLITE_FLOAT:
221+
row[columnName] = sqlite3_column_double(currentStatement, i);
222+
break;
223+
case SQLITE_TEXT: {
224+
auto columnValue = reinterpret_cast<const char*>(sqlite3_column_text(currentStatement, i));
225+
row[columnName] = columnValue;
226+
break;
227+
}
228+
case SQLITE_BLOB: {
229+
int blobSize = sqlite3_column_bytes(currentStatement, i);
230+
const void* blob = sqlite3_column_blob(currentStatement, i);
231+
if (blobSize > 0) {
232+
const auto* blobData = reinterpret_cast<const uint8_t*>(blob);
233+
row[columnName] = ArrayBuffer::copy(blobData, static_cast<size_t>(blobSize));
234+
} else {
235+
row[columnName] = ArrayBuffer::allocate(0);
236+
}
237+
break;
238+
}
239+
case SQLITE_NULL:
240+
default:
241+
row[columnName] = NullType::null;
242+
break;
243+
}
244+
}
296245

297-
case SQLITE_DONE:
298-
isConsuming = false;
299-
break;
246+
results.push_back(std::move(row));
247+
});
300248

301-
default:
302-
isFailed = true;
303-
isConsuming = false;
249+
std::optional<SQLiteQueryTableMetadata> metadata = std::nullopt;
250+
int count = sqlite3_column_count(statement.get());
251+
for (int i = 0; i < count; i++) {
252+
std::string columnName = sqlite3_column_name(statement.get(), i);
253+
ColumnType columnDeclaredType = mapSQLiteTypeToColumnType(sqlite3_column_decltype(statement.get(), i));
254+
auto columnMeta = NitroSQLiteQueryColumnMetadata(columnName, std::move(columnDeclaredType), i);
255+
256+
if (!metadata) {
257+
metadata = std::make_optional<SQLiteQueryTableMetadata>();
304258
}
259+
metadata->insert({columnName, std::move(columnMeta)});
305260
}
306261

307-
sqlite3_finalize(statement);
262+
int rowsAffected = sqlite3_changes(db);
263+
long long latestInsertRowId = sqlite3_last_insert_rowid(db);
264+
return std::make_shared<HybridNitroSQLiteQueryResult>(std::move(results), static_cast<double>(latestInsertRowId), rowsAffected,
265+
std::move(metadata));
266+
}
308267

309-
if (isFailed) {
310-
throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
311-
}
268+
SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query,
269+
const std::optional<SQLiteQueryParams>& params) {
270+
auto db = getOpenDatabase(dbName);
271+
auto statement = prepareStatement(db, query, params);
272+
bool isReadOnly = sqlite3_stmt_readonly(statement.get()) != 0;
273+
274+
consumeStatement(db, statement.get(), [](sqlite3_stmt*) {});
312275

313-
return {.rowsAffected = sqlite3_changes(db)};
276+
return {.rowsAffected = isReadOnly ? 0 : sqlite3_changes(db)};
314277
}
315278

316279
} // namespace margelo::rnnitrosqlite

packages/react-native-nitro-sqlite/cpp/operations.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ void sqliteDetachDb(const std::string& mainDBName, const std::string& alias);
1919
std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
2020
const std::optional<SQLiteQueryParams>& params);
2121

22-
SQLiteOperationResult sqliteExecuteLiteral(const std::string& dbName, const std::string& query);
22+
SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query,
23+
const std::optional<SQLiteQueryParams>& params = std::nullopt);
2324

2425
void sqliteCloseAll();
2526

0 commit comments

Comments
 (0)