diff --git a/example/tests/unit/specs/operations/execute.spec.ts b/example/tests/unit/specs/operations/execute.spec.ts index 6476ec7a..72ae0977 100644 --- a/example/tests/unit/specs/operations/execute.spec.ts +++ b/example/tests/unit/specs/operations/execute.spec.ts @@ -137,6 +137,46 @@ export default function registerExecuteUnitTests() { }) }) + describe('Bind errors', () => { + it('throws when execute receives an extra parameter without exposing it', () => { + const extraParameter = 'do-not-expose-sync-parameter' + + try { + testDb.execute('SELECT ?', [1, extraParameter]) + throw new Error('Expected execute to throw for the extra parameter') + } catch (error: unknown) { + if (!isNitroSQLiteError(error)) { + throw new Error('Should have thrown a valid NitroSQLiteError') + } + + expect(error.message).toContain('parameter 2') + expect(error.message).toContain('25') + expect(error.message).toContain('column index out of range') + expect(error.message.includes(extraParameter)).toBe(false) + } + }) + + it('rejects when executeAsync receives an extra parameter without exposing it', async () => { + const extraParameter = 'do-not-expose-async-parameter' + + try { + await testDb.executeAsync('SELECT ?', [1, extraParameter]) + throw new Error( + 'Expected executeAsync to reject for the extra parameter', + ) + } catch (error: unknown) { + if (!isNitroSQLiteError(error)) { + throw new Error('Should have thrown a valid NitroSQLiteError') + } + + expect(error.message).toContain('parameter 2') + expect(error.message).toContain('25') + expect(error.message).toContain('column index out of range') + expect(error.message.includes(extraParameter)).toBe(false) + } + }) + }) + describe('ArrayBuffer support', () => { describe('execute', () => { it('stores and reads ArrayBuffer values from BLOB columns', () => { diff --git a/example/tests/unit/specs/operations/executeBatch.spec.ts b/example/tests/unit/specs/operations/executeBatch.spec.ts index 9ceca983..4b67bb9f 100644 --- a/example/tests/unit/specs/operations/executeBatch.spec.ts +++ b/example/tests/unit/specs/operations/executeBatch.spec.ts @@ -1,4 +1,4 @@ -import { chance, expect } from '@tests/unit/common' +import { chance, expect, isNitroSQLiteError } from '@tests/unit/common' import type { BatchQueryCommand } from 'react-native-nitro-sqlite' import { describe, it } from '@tests/TestApi' import { testDb } from '@tests/db' @@ -94,5 +94,55 @@ export default function registerExecuteBatchUnitTests() { }, ]) }) + + it('throws when executeBatch receives an extra parameter without exposing it', () => { + const extraParameter = 'do-not-expose-batch-parameter' + + try { + testDb.executeBatch([ + { + query: 'SELECT ?', + params: [1, extraParameter], + }, + ]) + throw new Error( + 'Expected executeBatch to throw for the extra parameter', + ) + } catch (error: unknown) { + if (!isNitroSQLiteError(error)) { + throw new Error('Should have thrown a valid NitroSQLiteError') + } + + expect(error.message).toContain('parameter 2') + expect(error.message).toContain('25') + expect(error.message).toContain('column index out of range') + expect(error.message.includes(extraParameter)).toBe(false) + } + }) + + it('rejects when executeBatchAsync receives an extra parameter without exposing it', async () => { + const extraParameter = 'do-not-expose-batch-async-parameter' + + try { + await testDb.executeBatchAsync([ + { + query: 'SELECT ?', + params: [1, extraParameter], + }, + ]) + throw new Error( + 'Expected executeBatchAsync to reject for the extra parameter', + ) + } catch (error: unknown) { + if (!isNitroSQLiteError(error)) { + throw new Error('Should have thrown a valid NitroSQLiteError') + } + + expect(error.message).toContain('parameter 2') + expect(error.message).toContain('25') + expect(error.message).toContain('column index out of range') + expect(error.message.includes(extraParameter)).toBe(false) + } + }) }) } diff --git a/packages/react-native-nitro-sqlite/cpp/operations.cpp b/packages/react-native-nitro-sqlite/cpp/operations.cpp index ae61dc65..17315a38 100644 --- a/packages/react-native-nitro-sqlite/cpp/operations.cpp +++ b/packages/react-native-nitro-sqlite/cpp/operations.cpp @@ -121,24 +121,30 @@ void bindStatement(sqlite3_stmt* statement, const SQLiteQueryParams& values) { for (int valueIndex = 0; valueIndex < values.size(); valueIndex++) { int sqliteIndex = valueIndex + 1; SQLiteValue value = values.at(valueIndex); + int bindStatus = SQLITE_OK; if (std::holds_alternative(value)) { - sqlite3_bind_null(statement, sqliteIndex); + bindStatus = sqlite3_bind_null(statement, sqliteIndex); } else if (std::holds_alternative(value)) { - sqlite3_bind_int(statement, sqliteIndex, std::get(value)); + bindStatus = sqlite3_bind_int(statement, sqliteIndex, std::get(value)); } else if (std::holds_alternative(value)) { // Bind whole numbers as INTEGER so vec0 rowid/pk/partition (which reject REAL) work; SQLite still coerces to REAL for REAL columns. double doubleValue = std::get(value); if (std::trunc(doubleValue) == doubleValue && doubleValue >= kInt64MinAsDouble && doubleValue < kInt64UpperBoundAsDouble) { - sqlite3_bind_int64(statement, sqliteIndex, static_cast(doubleValue)); + bindStatus = sqlite3_bind_int64(statement, sqliteIndex, static_cast(doubleValue)); } else { - sqlite3_bind_double(statement, sqliteIndex, doubleValue); + bindStatus = sqlite3_bind_double(statement, sqliteIndex, doubleValue); } } else if (std::holds_alternative(value)) { const auto stringValue = std::get(value); - sqlite3_bind_text(statement, sqliteIndex, stringValue.c_str(), stringValue.length(), SQLITE_TRANSIENT); + bindStatus = sqlite3_bind_text(statement, sqliteIndex, stringValue.c_str(), stringValue.length(), SQLITE_TRANSIENT); } else if (std::holds_alternative>(value)) { const auto arrayBufferValue = std::get>(value); - sqlite3_bind_blob(statement, sqliteIndex, arrayBufferValue->data(), arrayBufferValue->size(), SQLITE_STATIC); + bindStatus = sqlite3_bind_blob(statement, sqliteIndex, arrayBufferValue->data(), arrayBufferValue->size(), SQLITE_STATIC); + } + + if (bindStatus != SQLITE_OK) { + throw NitroSQLiteException::SqlExecution("Failed to bind parameter " + std::to_string(sqliteIndex) + " (SQLite error " + + std::to_string(bindStatus) + "): " + sqlite3_errstr(bindStatus)); } } }