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/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..49a8fa89 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,7 +1988,12 @@ SQLRETURN OdbcStatement::sqlExecute() enFetch = NoneFetch; releaseResultSet(); parameterNeedData = 0; - retcode = (this->*execute)(); + updateCountParamArray = -1; + if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 + && execute != &OdbcStatement::executeStatementParamArray ) + retcode = executeStatementParamArray(); + else + retcode = (this->*execute)(); } catch (const SQLException &ex) { @@ -2015,7 +2021,12 @@ SQLRETURN OdbcStatement::sqlExecDirect(SQLCHAR * sql, int sqlLength) { enFetch = NoneFetch; parameterNeedData = 0; - retcode = (this->*execute)(); + updateCountParamArray = -1; + if ( statement->isActiveModify() && applicationParamDescriptor->headArraySize > 1 + && execute != &OdbcStatement::executeStatementParamArray ) + retcode = executeStatementParamArray(); + else + retcode = (this->*execute)(); } catch (const SQLException &ex) { @@ -2744,6 +2755,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 ); } @@ -2988,6 +3011,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; @@ -2995,36 +3019,65 @@ 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; + + 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 ); - 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 = SQL_PARAM_ERROR; - return ret; + statusPtr[rowNumberParamArray] = SQL_PARAM_ERROR; + throw; } - statement->executeStatement(); + updateCountParamArray += statement->getUpdateCount(); 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 ); + restoreBindOffset(); if ( statement->getMoreResults() ) setResultSet (statement->getResultSet(), false); @@ -3639,7 +3692,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; diff --git a/tests/test_array_binding.cpp b/tests/test_array_binding.cpp index 6e4b7075..e071389a 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"; + SKIP_ON_FIREBIRD6(); 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; @@ -805,30 +805,30 @@ 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); } // ============================================================================ // 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(); @@ -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; @@ -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,313 @@ 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); +} + +// ============================================================================ +// 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"); +} + +// ============================================================================ +// 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"); +}