From 05d2dcbd0c6781f5ccd99a8797074fba0debb03c Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:01:03 -0300 Subject: [PATCH 1/9] tests: exercise column-wise parameter arrays Eleven ArrayBindingTest cases were skipped citing a crash in the column-wise indicator offset. That crash was fixed in 0a7cc1f (SQLINTEGER to SQLLEN), so the skips come out. ParamOperationPtrSkipRows stays skipped because SQL_ATTR_PARAM_OPERATION_PTR is still not honoured, and the reactivated cases get the Firebird 6 guard the row-wise ones already have. Two new cases bind fixed-length C types with BufferLength = 0, the usage the ODBC specification describes for those types: one with the shape reported in #299 (INTEGER and BIGINT with a NULL in the third parameter set), one across DOUBLE, DATE and TIMESTAMP. --- tests/test_array_binding.cpp | 184 ++++++++++++++++++++++++++++++++--- 1 file changed, 173 insertions(+), 11 deletions(-) diff --git a/tests/test_array_binding.cpp b/tests/test_array_binding.cpp index 6e4b7075..4a2bc558 100644 --- a/tests/test_array_binding.cpp +++ b/tests/test_array_binding.cpp @@ -68,7 +68,7 @@ class ArrayBindingTest : public OdbcConnectedTest { // (ported from psqlodbc arraybinding-test.c test 1) // ============================================================================ TEST_F(ArrayBindingTest, ColumnWiseInsert) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 100; SQLRETURN ret; @@ -134,7 +134,7 @@ TEST_F(ArrayBindingTest, ColumnWiseInsert) { // 2. Column-wise binding — using SQLPrepare + SQLExecute // ============================================================================ TEST_F(ArrayBindingTest, ColumnWisePrepareExecute) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 10; SQLRETURN ret; @@ -257,7 +257,7 @@ TEST_F(ArrayBindingTest, RowWiseInsert) { // 4. Column-wise binding with NULL values // ============================================================================ TEST_F(ArrayBindingTest, ColumnWiseWithNulls) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 5; SQLRETURN ret; @@ -316,7 +316,7 @@ TEST_F(ArrayBindingTest, ColumnWiseWithNulls) { // 5. SQL_ATTR_PARAM_OPERATION_PTR — skip individual rows // ============================================================================ TEST_F(ArrayBindingTest, ParamOperationPtrSkipRows) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + GTEST_SKIP() << "Vanilla driver does not properly handle SQL_ATTR_PARAM_OPERATION_PTR"; const int ARRAY_SIZE = 5; SQLRETURN ret; @@ -384,7 +384,7 @@ TEST_F(ArrayBindingTest, ParamOperationPtrSkipRows) { // 6. Large array — column-wise (like psqlodbc's 10000-row test) // ============================================================================ TEST_F(ArrayBindingTest, LargeColumnWiseArray) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 1000; SQLRETURN ret; @@ -442,7 +442,7 @@ TEST_F(ArrayBindingTest, LargeColumnWiseArray) { // (from psqlodbc params-batch-exec-test.c) // ============================================================================ TEST_F(ArrayBindingTest, ReExecuteWithDifferentData) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int BATCH_SIZE = 5; SQLRETURN ret; @@ -516,7 +516,7 @@ TEST_F(ArrayBindingTest, ReExecuteWithDifferentData) { // SQLSetStmtAttr survive SQLFreeStmt") // ============================================================================ TEST_F(ArrayBindingTest, NewHandleAfterArrayExec) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 3; SQLRETURN ret; @@ -658,7 +658,7 @@ TEST_F(ArrayBindingTest, RowWiseMultipleTypes) { // 10. Column-wise binding with UPDATE statement // ============================================================================ TEST_F(ArrayBindingTest, ColumnWiseUpdate) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); // Insert some initial data { SQLHSTMT hStmt2 = SQL_NULL_HSTMT; @@ -717,7 +717,7 @@ TEST_F(ArrayBindingTest, ColumnWiseUpdate) { // 11. Column-wise binding with DELETE statement // ============================================================================ TEST_F(ArrayBindingTest, ColumnWiseDelete) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); // Insert initial data { SQLHSTMT hStmt2 = SQL_NULL_HSTMT; @@ -828,7 +828,7 @@ TEST_F(ArrayBindingTest, GetInfoParamArraySelects) { // 15. Column-wise binding with integer-only (no strings) // ============================================================================ TEST_F(ArrayBindingTest, ColumnWiseIntegerOnly) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); // Recreate table with integer-only columns ExecIgnoreError("DROP TABLE ARRAY_BIND_TEST"); Commit(); @@ -959,7 +959,7 @@ TEST_F(ArrayBindingTest, RowWiseWithOperationPtr) { // 17. Without status/processed pointers (optional per spec) // ============================================================================ TEST_F(ArrayBindingTest, WithoutStatusPointers) { - GTEST_SKIP() << "Crashes on vanilla master: sizeof(SQLINTEGER) vs sizeof(SQLLEN) bug in OdbcStatement.cpp line 2891"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 3; SQLRETURN ret; @@ -987,3 +987,165 @@ TEST_F(ArrayBindingTest, WithoutStatusPointers) { Commit(); EXPECT_EQ(CountRows(), ARRAY_SIZE); } + +// ============================================================================ +// 18. Column-wise binding, fixed-length C types with BufferLength = 0 (#299) +// The ODBC specification ignores BufferLength for fixed-length C types, +// so the element stride must come from the C type size. +// ============================================================================ +TEST_F(ArrayBindingTest, ColumnWiseFixedLengthBufferLengthZero) { + SKIP_ON_FIREBIRD6(); + ExecIgnoreError("DROP TABLE ARRAY_BIND_TEST"); + Commit(); + ReallocStmt(); + ExecDirect("CREATE TABLE ARRAY_BIND_TEST (I INTEGER, V BIGINT)"); + Commit(); + ReallocStmt(); + + const int N = 5; + SQLINTEGER ids[N] = {1, 2, 3, 4, 5}; + SQLBIGINT vals[N] = {10, 20, 30, 40, 50}; + SQLLEN id_ind[N] = {0, 0, 0, 0, 0}; + SQLLEN val_ind[N] = {0, 0, SQL_NULL_DATA, 0, 0}; + SQLUSMALLINT status[N] = {}; + SQLULEN nprocessed = 0; + SQLRETURN ret; + + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAM_BIND_TYPE, SQL_PARAM_BIND_BY_COLUMN, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(intptr_t)N, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &nprocessed, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAM_STATUS_PTR, status, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLPrepare(hStmt, (SQLCHAR*)"INSERT INTO ARRAY_BIND_TEST (I, V) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + // BufferLength = 0 on both parameters + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + ids, 0, id_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, + vals, 0, val_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLExecute(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(nprocessed, (SQLULEN)N); + for (int i = 0; i < N; i++) { + EXPECT_EQ(status[i], SQL_PARAM_SUCCESS) << "Row " << i; + } + Commit(); + ReallocStmt(); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"SELECT I, V FROM ARRAY_BIND_TEST ORDER BY I", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + SQLINTEGER id = 0; + SQLBIGINT val = 0; + SQLLEN idInd = 0, valInd = 0; + SQLBindCol(hStmt, 1, SQL_C_SLONG, &id, sizeof(id), &idInd); + SQLBindCol(hStmt, 2, SQL_C_SBIGINT, &val, sizeof(val), &valInd); + for (int i = 0; i < N; i++) { + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Row " << i; + EXPECT_EQ(id, ids[i]) << "Row " << i; + if (val_ind[i] == SQL_NULL_DATA) { + EXPECT_EQ(valInd, SQL_NULL_DATA) << "Row " << i; + } else { + EXPECT_NE(valInd, SQL_NULL_DATA) << "Row " << i; + EXPECT_EQ(val, vals[i]) << "Row " << i; + } + } + EXPECT_EQ(SQLFetch(hStmt), SQL_NO_DATA); +} + +// ============================================================================ +// 19. Column-wise binding, BufferLength = 0 across DOUBLE, DATE and TIMESTAMP +// ============================================================================ +TEST_F(ArrayBindingTest, ColumnWiseFixedLengthTypesBufferLengthZero) { + SKIP_ON_FIREBIRD6(); + ExecIgnoreError("DROP TABLE ARRAY_BIND_TEST"); + Commit(); + ReallocStmt(); + ExecDirect("CREATE TABLE ARRAY_BIND_TEST (I INTEGER, D DOUBLE PRECISION, DT DATE, TS TIMESTAMP)"); + Commit(); + ReallocStmt(); + + const int N = 3; + SQLINTEGER ids[N] = {1, 2, 3}; + double dbls[N] = {1.5, 2.5, 3.5}; + SQL_DATE_STRUCT dates[N] = {}; + SQL_TIMESTAMP_STRUCT stamps[N] = {}; + SQLLEN ind[N] = {0, 0, 0}; + SQLULEN nprocessed = 0; + SQLRETURN ret; + + for (int i = 0; i < N; i++) { + dates[i].year = (SQLSMALLINT)(2020 + i); + dates[i].month = (SQLUSMALLINT)(1 + i); + dates[i].day = (SQLUSMALLINT)(10 + i); + stamps[i].year = (SQLSMALLINT)(2021 + i); + stamps[i].month = (SQLUSMALLINT)(2 + i); + stamps[i].day = (SQLUSMALLINT)(20 + i); + stamps[i].hour = (SQLUSMALLINT)i; + stamps[i].minute = 30; + stamps[i].second = 45; + stamps[i].fraction = 0; + } + + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAM_BIND_TYPE, SQL_PARAM_BIND_BY_COLUMN, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(intptr_t)N, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &nprocessed, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + ids, 0, ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, + dbls, 0, ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_TYPE_DATE, SQL_TYPE_DATE, 0, 0, + dates, 0, ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 4, SQL_PARAM_INPUT, SQL_C_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP, 0, 0, + stamps, 0, ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"INSERT INTO ARRAY_BIND_TEST (I, D, DT, TS) VALUES (?, ?, ?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(nprocessed, (SQLULEN)N); + Commit(); + ReallocStmt(); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"SELECT I, D, DT, TS FROM ARRAY_BIND_TEST ORDER BY I", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + SQLINTEGER id = 0; + double dbl = 0; + SQL_DATE_STRUCT date = {}; + SQL_TIMESTAMP_STRUCT stamp = {}; + SQLLEN idInd = 0, dblInd = 0, dateInd = 0, stampInd = 0; + SQLBindCol(hStmt, 1, SQL_C_SLONG, &id, sizeof(id), &idInd); + SQLBindCol(hStmt, 2, SQL_C_DOUBLE, &dbl, sizeof(dbl), &dblInd); + SQLBindCol(hStmt, 3, SQL_C_TYPE_DATE, &date, sizeof(date), &dateInd); + SQLBindCol(hStmt, 4, SQL_C_TYPE_TIMESTAMP, &stamp, sizeof(stamp), &stampInd); + for (int i = 0; i < N; i++) { + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Row " << i; + EXPECT_EQ(id, ids[i]) << "Row " << i; + EXPECT_DOUBLE_EQ(dbl, dbls[i]) << "Row " << i; + EXPECT_EQ(date.year, dates[i].year) << "Row " << i; + EXPECT_EQ(date.month, dates[i].month) << "Row " << i; + EXPECT_EQ(date.day, dates[i].day) << "Row " << i; + EXPECT_EQ(stamp.year, stamps[i].year) << "Row " << i; + EXPECT_EQ(stamp.month, stamps[i].month) << "Row " << i; + EXPECT_EQ(stamp.day, stamps[i].day) << "Row " << i; + EXPECT_EQ(stamp.hour, stamps[i].hour) << "Row " << i; + EXPECT_EQ(stamp.minute, stamps[i].minute) << "Row " << i; + EXPECT_EQ(stamp.second, stamps[i].second) << "Row " << i; + } + EXPECT_EQ(SQLFetch(hStmt), SQL_NO_DATA); +} From 9348d2f2b5f404934455c58efc8f235f78e55ead Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:01:03 -0300 Subject: [PATCH 2/9] Step column-wise parameter arrays by the C type size when BufferLength is 0 SQLBindParameter stores BufferLength as the element stride of a column-wise parameter array. The ODBC specification ignores BufferLength for fixed-length C types and applications pass 0 there, so every parameter set read element 0: the driver inserted N rows carrying the first set's values and reported SQL_SUCCESS. bindInputOutputParam now falls back to the C type size when the stride is 0, the way bindOutputColumn already does for SQLBindCol. getConciseSize gains SQL_C_GUID, for which it returned the type code (-11) and would have produced a negative stride. Character and binary C types keep BufferLength as their stride, so string arrays behave as before. Fixes #299 --- OdbcDesc.cpp | 3 +++ OdbcStatement.cpp | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/OdbcDesc.cpp b/OdbcDesc.cpp index fdc252e2..ead5816d 100644 --- a/OdbcDesc.cpp +++ b/OdbcDesc.cpp @@ -1566,6 +1566,9 @@ int OdbcDesc::getConciseSize(int type, int length) case SQL_C_UBIGINT: return 8; + case SQL_C_GUID: + return sizeof(SQLGUID); + case SQL_DECIMAL: case SQL_C_NUMERIC: return sizeof(tagSQL_NUMERIC_STRUCT); diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 028a40d9..77097a75 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -2744,6 +2744,18 @@ void OdbcStatement::bindInputOutputParam(int param, DescRecord * recordApp) recordApp->fnConv = convert->getAdressFunction( recordApp, record ); + switch ( recordApp->conciseType ) + { + case SQL_C_CHAR: + case SQL_C_WCHAR: + case SQL_C_BINARY: + break; + + default: + if ( !recordApp->sizeColumnExtendedFetch ) + recordApp->sizeColumnExtendedFetch = ipd->getConciseSize( recordApp->conciseType, recordApp->length ); + } + // if ( convert->isIdentity() ) addBindParam ( param, record, recordApp ); } From f0ccd92bc487584cd74e5f242c11093c0000dfd7 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:01:03 -0300 Subject: [PATCH 3/9] Run the parameter-array executor when SQL_ATTR_PARAMSET_SIZE is set after SQLPrepare The executor is chosen at prepare time from the paramset size in force at that moment. An application that prepares first and sets SQL_ATTR_PARAMSET_SIZE afterwards, which the specification allows and which pyodbc's fast_executemany does, got a single execution of the first parameter set, reported as success. sqlExecute and sqlExecDirect now switch to executeStatementParamArray when the paramset size is greater than 1 at execute time. --- OdbcStatement.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 77097a75..426fa0a4 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -1987,7 +1987,11 @@ SQLRETURN OdbcStatement::sqlExecute() enFetch = NoneFetch; releaseResultSet(); parameterNeedData = 0; - retcode = (this->*execute)(); + if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 + && execute != &OdbcStatement::executeStatementParamArray ) + retcode = executeStatementParamArray(); + else + retcode = (this->*execute)(); } catch (const SQLException &ex) { @@ -2015,7 +2019,11 @@ SQLRETURN OdbcStatement::sqlExecDirect(SQLCHAR * sql, int sqlLength) { enFetch = NoneFetch; parameterNeedData = 0; - retcode = (this->*execute)(); + if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 + && execute != &OdbcStatement::executeStatementParamArray ) + retcode = executeStatementParamArray(); + else + retcode = (this->*execute)(); } catch (const SQLException &ex) { From b3969d441d42ec6284c89d7bb3d1d85898aaf5f9 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:29:24 -0300 Subject: [PATCH 4/9] tests: activate the SQL_ATTR_PARAM_OPERATION_PTR cases ParamOperationPtrSkipRows and RowWiseWithOperationPtr were skipped because the driver does not honour the operation array. Both get the Firebird 6 guard the other array cases carry and run from here on. --- tests/test_array_binding.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_array_binding.cpp b/tests/test_array_binding.cpp index 4a2bc558..aac04983 100644 --- a/tests/test_array_binding.cpp +++ b/tests/test_array_binding.cpp @@ -316,7 +316,7 @@ TEST_F(ArrayBindingTest, ColumnWiseWithNulls) { // 5. SQL_ATTR_PARAM_OPERATION_PTR — skip individual rows // ============================================================================ TEST_F(ArrayBindingTest, ParamOperationPtrSkipRows) { - GTEST_SKIP() << "Vanilla driver does not properly handle SQL_ATTR_PARAM_OPERATION_PTR"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 5; SQLRETURN ret; @@ -894,7 +894,7 @@ TEST_F(ArrayBindingTest, ColumnWiseIntegerOnly) { // 16. Row-wise with SQL_ATTR_PARAM_OPERATION_PTR // ============================================================================ TEST_F(ArrayBindingTest, RowWiseWithOperationPtr) { - GTEST_SKIP() << "Vanilla driver does not properly handle SQL_ATTR_PARAM_OPERATION_PTR"; + SKIP_ON_FIREBIRD6(); const int ARRAY_SIZE = 4; SQLRETURN ret; From d862a80664d87f7566e9a9c4b9681eda61cd6a37 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:29:24 -0300 Subject: [PATCH 5/9] Honour SQL_ATTR_PARAM_OPERATION_PTR in parameter-array execution The operation array was stored in the APD (SQL_DESC_ARRAY_STATUS_PTR) but executeStatementParamArray never read it, so a set marked SQL_PARAM_IGNORE was executed like any other and an application had no way to skip a row short of rebuilding its arrays. The loop now skips ignored sets, leaves their status at SQL_PARAM_UNUSED and does not count them in SQL_ATTR_PARAMS_PROCESSED_PTR. The status array is filled with SQL_PARAM_UNUSED before the loop, so sets that were never reached read as unused, and status elements are addressed by set number instead of through a moving pointer. The processed count is kept per set instead of being written once after the loop, so it includes a failed set as the specification describes. --- OdbcStatement.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 426fa0a4..ca4140ff 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -3008,6 +3008,7 @@ SQLRETURN OdbcStatement::executeStatementParamArray() : &rowCount; SQLUSMALLINT *statusPtr = implementationParamDescriptor->headArrayStatusPtr ? implementationParamDescriptor->headArrayStatusPtr : NULL; + SQLUSMALLINT *operationPtr = applicationParamDescriptor->headArrayStatusPtr; int rowSize = applicationParamDescriptor->headBindType; int nCountRow = applicationParamDescriptor->headArraySize; SQLLEN *&headBindOffsetPtr = applicationParamDescriptor->headBindOffsetPtr; @@ -3018,8 +3019,21 @@ SQLRETURN OdbcStatement::executeStatementParamArray() headBindOffsetPtr = &bindOffsetPtrTmp; *rowCountPt = rowNumberParamArray = 0; + if ( statusPtr ) + for ( int n = 0; n < nCountRow; ++n ) + statusPtr[n] = SQL_PARAM_UNUSED; + while ( rowNumberParamArray < nCountRow ) { + if ( operationPtr && operationPtr[rowNumberParamArray] == SQL_PARAM_IGNORE ) + { + bindOffsetPtrTmp += rowSize; + ++rowNumberParamArray; + continue; + } + + ++*rowCountPt; + if ( arrayColumnWiseBinding ) bindOffsetIndColumnWiseBinding = ( bindOffsetPtrTmp + rowNumberParamArray ) * sizeof ( SQLLEN ); @@ -3028,21 +3042,20 @@ SQLRETURN OdbcStatement::executeStatementParamArray() headBindOffsetPtr = bindOffsetPtrSave; convert->setBindOffsetPtrFrom ( applicationParamDescriptor->headBindOffsetPtr, applicationParamDescriptor->headBindOffsetPtr ); if ( statusPtr ) - *statusPtr = SQL_PARAM_ERROR; + statusPtr[rowNumberParamArray] = SQL_PARAM_ERROR; return ret; } statement->executeStatement(); if ( statusPtr ) - *statusPtr++ = ret == SQL_SUCCESS_WITH_INFO ? SQL_PARAM_SUCCESS_WITH_INFO : SQL_PARAM_SUCCESS; + statusPtr[rowNumberParamArray] = ret == SQL_SUCCESS_WITH_INFO ? SQL_PARAM_SUCCESS_WITH_INFO : SQL_PARAM_SUCCESS; bindOffsetPtrTmp += rowSize; parameterNeedData = 1; ++rowNumberParamArray; } - *rowCountPt = rowNumberParamArray; headBindOffsetPtr = bindOffsetPtrSave; convert->setBindOffsetPtrFrom ( applicationParamDescriptor->headBindOffsetPtr, applicationParamDescriptor->headBindOffsetPtr ); From c88ae0484df2ddd5d10a45ff1b6eceb88ce13828 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:35:26 -0300 Subject: [PATCH 6/9] tests: SQLRowCount and SQLGetInfo after a parameter-array execute Two cases check the row count after an array INSERT (five sets, five) and after an array UPDATE whose sets touch one, three and zero rows (four), and that SQLMoreResults has nothing further. The two SQLGetInfo cases now expect SQL_PARC_NO_BATCH and SQL_PAS_NO_SELECT, the values the driver's behaviour matches. --- tests/test_array_binding.cpp | 94 ++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/tests/test_array_binding.cpp b/tests/test_array_binding.cpp index aac04983..d3b7a5d4 100644 --- a/tests/test_array_binding.cpp +++ b/tests/test_array_binding.cpp @@ -805,23 +805,23 @@ TEST_F(ArrayBindingTest, ParamsetSizeOneIsNormal) { } // ============================================================================ -// 13. SQLGetInfo reports SQL_PARC_BATCH for SQL_PARAM_ARRAY_ROW_COUNTS +// 13. SQLGetInfo reports SQL_PARC_NO_BATCH for SQL_PARAM_ARRAY_ROW_COUNTS // ============================================================================ TEST_F(ArrayBindingTest, GetInfoParamArrayRowCounts) { SQLUINTEGER value = 0; SQLRETURN ret = SQLGetInfo(hDbc, SQL_PARAM_ARRAY_ROW_COUNTS, &value, sizeof(value), NULL); ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(value, (SQLUINTEGER)SQL_PARC_BATCH); + EXPECT_EQ(value, (SQLUINTEGER)SQL_PARC_NO_BATCH); } // ============================================================================ -// 14. SQLGetInfo reports SQL_PAS_BATCH for SQL_PARAM_ARRAY_SELECTS +// 14. SQLGetInfo reports SQL_PAS_NO_SELECT for SQL_PARAM_ARRAY_SELECTS // ============================================================================ TEST_F(ArrayBindingTest, GetInfoParamArraySelects) { SQLUINTEGER value = 0; SQLRETURN ret = SQLGetInfo(hDbc, SQL_PARAM_ARRAY_SELECTS, &value, sizeof(value), NULL); ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(value, (SQLUINTEGER)SQL_PAS_BATCH); + EXPECT_EQ(value, (SQLUINTEGER)SQL_PAS_NO_SELECT); } // ============================================================================ @@ -1149,3 +1149,89 @@ TEST_F(ArrayBindingTest, ColumnWiseFixedLengthTypesBufferLengthZero) { } EXPECT_EQ(SQLFetch(hStmt), SQL_NO_DATA); } + +// ============================================================================ +// 20. SQLRowCount after an array INSERT is the total over all parameter sets +// (SQL_PARC_NO_BATCH); SQLMoreResults has nothing further +// ============================================================================ +TEST_F(ArrayBindingTest, RowCountAfterArrayInsert) { + SKIP_ON_FIREBIRD6(); + const int N = 5; + SQLINTEGER ids[N] = {1, 2, 3, 4, 5}; + SQLCHAR strs[N][20] = {"a", "b", "c", "d", "e"}; + SQLLEN id_ind[N] = {0, 0, 0, 0, 0}; + SQLLEN str_ind[N] = {SQL_NTS, SQL_NTS, SQL_NTS, SQL_NTS, SQL_NTS}; + SQLULEN nprocessed = 0; + SQLRETURN ret; + + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(intptr_t)N, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &nprocessed, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + ids, 0, id_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 19, 0, + strs, 20, str_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(nprocessed, (SQLULEN)N); + + SQLLEN rowCount = -1; + ret = SQLRowCount(hStmt, &rowCount); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(rowCount, (SQLLEN)N); + EXPECT_EQ(SQLMoreResults(hStmt), SQL_NO_DATA); + + Commit(); + EXPECT_EQ(CountRows(), N); +} + +// ============================================================================ +// 21. SQLRowCount after an array UPDATE adds up the rows each set touched; +// a set that matches nothing contributes zero +// ============================================================================ +TEST_F(ArrayBindingTest, RowCountAfterArrayUpdate) { + SKIP_ON_FIREBIRD6(); + ExecDirect("INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (1, 'one')"); + ExecDirect("INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (2, 'two')"); + ExecDirect("INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (2, 'two')"); + ExecDirect("INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (2, 'two')"); + Commit(); + ReallocStmt(); + + // Three sets: I = 1 touches one row, I = 2 touches three, I = 999 touches none + const int N = 3; + SQLINTEGER ids[N] = {1, 2, 999}; + SQLCHAR strs[N][20] = {"uno", "dos", "none"}; + SQLLEN id_ind[N] = {0, 0, 0}; + SQLLEN str_ind[N] = {SQL_NTS, SQL_NTS, SQL_NTS}; + SQLULEN nprocessed = 0; + SQLRETURN ret; + + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(intptr_t)N, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &nprocessed, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 19, 0, + strs, 20, str_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + ids, 0, id_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"UPDATE ARRAY_BIND_TEST SET T = ? WHERE I = ?", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(nprocessed, (SQLULEN)N); + + SQLLEN rowCount = -1; + ret = SQLRowCount(hStmt, &rowCount); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(rowCount, (SQLLEN)4); + + Commit(); + EXPECT_EQ(GetValue(1), "uno"); + EXPECT_EQ(GetValue(2), "dos"); +} From cc72fe6b149dd6f2f1eb39c3c8421c54f7cffb34 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 02:35:26 -0300 Subject: [PATCH 7/9] Return the cumulative row count after a parameter-array execute SQLGetInfo advertised SQL_PARC_BATCH, one row count per parameter set stepped with SQLMoreResults, but the driver kept only the last set's count and SQLMoreResults answered SQL_NO_DATA, so an application saw 1 (or 0 when the last set matched nothing) for a five-row insert. executeStatementParamArray now adds up the update count of every set and sqlRowCount returns that total while the last execute was an array execute; a single execute resets it. SQL_PARAM_ARRAY_ROW_COUNTS reports SQL_PARC_NO_BATCH accordingly. SQL_PARAM_ARRAY_SELECTS reports SQL_PAS_NO_SELECT: a result-set statement with a paramset larger than 1 is not routed to the array executor and runs once with the first set, which neither SQL_PAS_BATCH nor SQL_PAS_NO_BATCH describes. --- InfoItems.h | 4 ++-- OdbcStatement.cpp | 7 ++++++- OdbcStatement.h | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/InfoItems.h b/InfoItems.h index 956da487..7101997e 100644 --- a/InfoItems.h +++ b/InfoItems.h @@ -53,8 +53,8 @@ CITEM (SQL_DRIVER_NAME, DRIVER_NAME) CITEM (SQL_DRIVER_ODBC_VER, ODBC_DRIVER_VERSION) -NITEM (SQL_PARAM_ARRAY_ROW_COUNTS, SQL_PARC_BATCH) -NITEM (SQL_PARAM_ARRAY_SELECTS, SQL_PAS_BATCH) +NITEM (SQL_PARAM_ARRAY_ROW_COUNTS, SQL_PARC_NO_BATCH) +NITEM (SQL_PARAM_ARRAY_SELECTS, SQL_PAS_NO_SELECT) CITEM (SQL_DRIVER_VER, DRIVER_VERSION) CITEM (SQL_ROW_UPDATES, "Y") diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index ca4140ff..96544472 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -174,6 +174,7 @@ OdbcStatement::OdbcStatement(OdbcConnection *connect, int statementNumber) statement = connection->connection->createInternalStatement(); bulkInsert = NULL; execute = &OdbcStatement::executeStatement; + updateCountParamArray = -1; fetchNext = &ResultSet::nextFetch; schemaFetchData = true; metaData = NULL; @@ -1987,6 +1988,7 @@ SQLRETURN OdbcStatement::sqlExecute() enFetch = NoneFetch; releaseResultSet(); parameterNeedData = 0; + updateCountParamArray = -1; if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 && execute != &OdbcStatement::executeStatementParamArray ) retcode = executeStatementParamArray(); @@ -2019,6 +2021,7 @@ SQLRETURN OdbcStatement::sqlExecDirect(SQLCHAR * sql, int sqlLength) { enFetch = NoneFetch; parameterNeedData = 0; + updateCountParamArray = -1; if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 && execute != &OdbcStatement::executeStatementParamArray ) retcode = executeStatementParamArray(); @@ -3018,6 +3021,7 @@ SQLRETURN OdbcStatement::executeStatementParamArray() headBindOffsetPtr = &bindOffsetPtrTmp; *rowCountPt = rowNumberParamArray = 0; + updateCountParamArray = 0; if ( statusPtr ) for ( int n = 0; n < nCountRow; ++n ) @@ -3047,6 +3051,7 @@ SQLRETURN OdbcStatement::executeStatementParamArray() } statement->executeStatement(); + updateCountParamArray += statement->getUpdateCount(); if ( statusPtr ) statusPtr[rowNumberParamArray] = ret == SQL_SUCCESS_WITH_INFO ? SQL_PARAM_SUCCESS_WITH_INFO : SQL_PARAM_SUCCESS; @@ -3672,7 +3677,7 @@ SQLRETURN OdbcStatement::sqlRowCount(SQLLEN *rowCount) if ( enFetch != NoneFetch ) *rowCount = rowNumber; else if ( statement->isActive() ) - *rowCount = statement->getUpdateCount(); + *rowCount = updateCountParamArray >= 0 ? updateCountParamArray : statement->getUpdateCount(); else *rowCount = -1; } diff --git a/OdbcStatement.h b/OdbcStatement.h index aca8696a..af949532 100644 --- a/OdbcStatement.h +++ b/OdbcStatement.h @@ -192,6 +192,7 @@ class OdbcStatement : public OdbcObject bool asyncEnable; int rowNumber; int rowNumberParamArray; + SQLLEN updateCountParamArray; int lastRowsetSize; SQLLEN indicatorRowNumber; int maxRows; From c44c66dd3a6bfbf14b71ad7342786355887ad3d7 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:01:35 -0300 Subject: [PATCH 8/9] tests: server error inside a parameter array One case runs a five-set INSERT whose third set violates NOT NULL, checks the return code and SQLSTATE, the processed count (3, the failed set included), the status array (success, success, error, unused, unused), then reuses the same handle for a single-row insert and reads the table back. --- tests/test_array_binding.cpp | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_array_binding.cpp b/tests/test_array_binding.cpp index d3b7a5d4..e071389a 100644 --- a/tests/test_array_binding.cpp +++ b/tests/test_array_binding.cpp @@ -1235,3 +1235,65 @@ TEST_F(ArrayBindingTest, RowCountAfterArrayUpdate) { EXPECT_EQ(GetValue(1), "uno"); EXPECT_EQ(GetValue(2), "dos"); } + +// ============================================================================ +// 22. A server error inside the array marks the failed set, counts it, and +// leaves the handle usable for the next execute (#309) +// ============================================================================ +TEST_F(ArrayBindingTest, ServerErrorInsideArray) { + SKIP_ON_FIREBIRD6(); + const int N = 5; + SQLINTEGER ids[N] = {1, 2, 3, 4, 5}; + SQLCHAR strs[N][20] = {"a", "b", "c", "d", "e"}; + SQLLEN id_ind[N] = {0, 0, SQL_NULL_DATA, 0, 0}; // set 3 violates NOT NULL on I + SQLLEN str_ind[N] = {SQL_NTS, SQL_NTS, SQL_NTS, SQL_NTS, SQL_NTS}; + SQLUSMALLINT status[N] = {9, 9, 9, 9, 9}; + SQLULEN nprocessed = 0; + SQLRETURN ret; + + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(intptr_t)N, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &nprocessed, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAM_STATUS_PTR, status, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + ids, 0, id_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 19, 0, + strs, 20, str_ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)"INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (?, ?)", SQL_NTS); + EXPECT_EQ(ret, SQL_ERROR); + EXPECT_EQ(GetSqlState(SQL_HANDLE_STMT, hStmt), "23000"); + EXPECT_EQ(nprocessed, (SQLULEN)3) << "the failed set counts as processed"; + EXPECT_EQ(status[0], SQL_PARAM_SUCCESS); + EXPECT_EQ(status[1], SQL_PARAM_SUCCESS); + EXPECT_EQ(status[2], SQL_PARAM_ERROR); + EXPECT_EQ(status[3], SQL_PARAM_UNUSED); + EXPECT_EQ(status[4], SQL_PARAM_UNUSED); + + // The same handle, one row + ret = SQLFreeStmt(hStmt, SQL_RESET_PARAMS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLSetStmtAttr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)1, 0); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + SQLINTEGER id = 7; + SQLCHAR str[] = "seven"; + SQLLEN idInd = 0, strInd = SQL_NTS; + ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, + &id, 0, &idInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 19, 0, + str, sizeof(str), &strInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLExecDirect(hStmt, (SQLCHAR*)"INSERT INTO ARRAY_BIND_TEST (I, T) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + Commit(); + EXPECT_EQ(CountRows(), 3); + EXPECT_EQ(GetValue(1), "a"); + EXPECT_EQ(GetValue(2), "b"); + EXPECT_EQ(GetValue(7), "seven"); +} From 49ef47f7cfec61e1c03381e726b1fcdb477c698b Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:01:35 -0300 Subject: [PATCH 9/9] Restore the bind-offset pointer when a parameter-array execute throws executeStatementParamArray points the APD's bind-offset pointer at a local for the duration of the loop and hands the same address to the converter. Both were restored on the normal exit and on the inputParam failure exit, but a server error thrown by executeStatement left them pointing into the dead stack frame; the next execute on that handle read a garbage offset and either crashed or inserted a garbage row. The restore is now a small lambda used on all three exits, the exception path marks the failed set SQL_PARAM_ERROR before rethrowing, and the processed count already includes the failed set since the operation pointer change. Fixes #309 --- OdbcStatement.cpp | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 96544472..49a8fa89 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -3019,6 +3019,12 @@ SQLRETURN OdbcStatement::executeStatementParamArray() SQLLEN bindOffsetPtrTmp = headBindOffsetPtr ? *headBindOffsetPtr : 0; bool arrayColumnWiseBinding = rowSize == SQL_PARAM_BIND_BY_COLUMN; + auto restoreBindOffset = [&]() + { + headBindOffsetPtr = bindOffsetPtrSave; + convert->setBindOffsetPtrFrom ( applicationParamDescriptor->headBindOffsetPtr, applicationParamDescriptor->headBindOffsetPtr ); + }; + headBindOffsetPtr = &bindOffsetPtrTmp; *rowCountPt = rowNumberParamArray = 0; updateCountParamArray = 0; @@ -3041,16 +3047,26 @@ SQLRETURN OdbcStatement::executeStatementParamArray() if ( arrayColumnWiseBinding ) bindOffsetIndColumnWiseBinding = ( bindOffsetPtrTmp + rowNumberParamArray ) * sizeof ( SQLLEN ); - if ( (ret = inputParam( arrayColumnWiseBinding ), ret) && ret != SQL_SUCCESS_WITH_INFO ) + try { - headBindOffsetPtr = bindOffsetPtrSave; - convert->setBindOffsetPtrFrom ( applicationParamDescriptor->headBindOffsetPtr, applicationParamDescriptor->headBindOffsetPtr ); + if ( (ret = inputParam( arrayColumnWiseBinding ), ret) && ret != SQL_SUCCESS_WITH_INFO ) + { + restoreBindOffset(); + if ( statusPtr ) + statusPtr[rowNumberParamArray] = SQL_PARAM_ERROR; + return ret; + } + + statement->executeStatement(); + } + catch ( ... ) + { + restoreBindOffset(); if ( statusPtr ) statusPtr[rowNumberParamArray] = SQL_PARAM_ERROR; - return ret; + throw; } - statement->executeStatement(); updateCountParamArray += statement->getUpdateCount(); if ( statusPtr ) @@ -3061,8 +3077,7 @@ SQLRETURN OdbcStatement::executeStatementParamArray() ++rowNumberParamArray; } - headBindOffsetPtr = bindOffsetPtrSave; - convert->setBindOffsetPtrFrom ( applicationParamDescriptor->headBindOffsetPtr, applicationParamDescriptor->headBindOffsetPtr ); + restoreBindOffset(); if ( statement->getMoreResults() ) setResultSet (statement->getResultSet(), false);