From 87b151d7e63ab78fc769cbabf408898afad0cb42 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 6 Sep 2026 16:24:39 -0300 Subject: [PATCH 1/2] Keep SQL_ATTR_ROWS_FETCHED_PTR and SQL_ATTR_ROW_STATUS_PTR across SQLPrepare OdbcDesc::setDefaultImplDesc rebuilds the implementation row descriptor on every prepare and, while doing so, reset SQL_DESC_ROWS_PROCESSED_PTR and SQL_DESC_ARRAY_STATUS_PTR to NULL. An application that set SQL_ATTR_ROWS_FETCHED_PTR / SQL_ATTR_ROW_STATUS_PTR before SQLPrepare or SQLExecDirect never had them written by SQLFetch, so a block cursor could not tell how many rows the last rowset held. The same attributes set after the prepare worked, and the parameter descriptor never reset its own counterparts. Statement attributes persist until the statement is freed or the attribute is set again; SQLPrepare must not clear them. Reported in #301. --- OdbcDesc.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/OdbcDesc.cpp b/OdbcDesc.cpp index fdc252e2..7f9a276e 100644 --- a/OdbcDesc.cpp +++ b/OdbcDesc.cpp @@ -85,9 +85,7 @@ void OdbcDesc::setDefaultImplDesc (StatementMetaData * ptMetaDataOut, StatementM headAllocType = SQL_DESC_ALLOC_AUTO; headArraySize = 1; - headArrayStatusPtr = (SQLUSMALLINT*)NULL; headBindOffsetPtr = (SQLLEN*)NULL; - headRowsProcessedPtr = (SQLULEN*)NULL; headCount = 0; if( metaDataOut == NULL ) From 86af5224a11cc28da5916661e60133b0918d9638 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 6 Sep 2026 16:24:40 -0300 Subject: [PATCH 2/2] tests: cover block cursor attributes set before SQLPrepare Three cases fail on master: SQL_ATTR_ROWS_FETCHED_PTR and SQL_ATTR_ROW_STATUS_PTR set before SQLPrepare, before SQLExecDirect, and kept across SQLFreeStmt(SQL_CLOSE) on a reused handle. The fourth sets them between SQLPrepare and SQLExecute and is the control that already passed. --- tests/CMakeLists.txt | 1 + tests/test_block_cursor.cpp | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tests/test_block_cursor.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79fb28c6..9179a9f0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,6 +67,7 @@ add_executable(firebird_odbc_tests test_param_conversions.cpp test_prepare.cpp test_cursors.cpp + test_block_cursor.cpp test_cursor_commit.cpp test_cursor_name.cpp test_data_at_execution.cpp diff --git a/tests/test_block_cursor.cpp b/tests/test_block_cursor.cpp new file mode 100644 index 00000000..1dce0171 --- /dev/null +++ b/tests/test_block_cursor.cpp @@ -0,0 +1,126 @@ +// tests/test_block_cursor.cpp — Block cursor (SQL_ATTR_ROW_ARRAY_SIZE > 1) tests +// +// Covers the IRD header fields an application reads back after a rowset +// fetch: SQL_ATTR_ROWS_FETCHED_PTR and SQL_ATTR_ROW_STATUS_PTR. Statement +// attributes persist until the statement is freed or the attribute is set +// again; in particular SQLPrepare / SQLExecDirect must not discard them. + +#include "test_helpers.h" + +class BlockCursorTest : public OdbcConnectedTest { +protected: + static constexpr SQLULEN kArraySize = 4; + + // Six rows, no table and no parameters needed. + static const char* SixRowsSql() { + return "SELECT 1 FROM rdb$database UNION ALL SELECT 2 FROM rdb$database" + " UNION ALL SELECT 3 FROM rdb$database UNION ALL SELECT 4 FROM rdb$database" + " UNION ALL SELECT 5 FROM rdb$database UNION ALL SELECT 6 FROM rdb$database"; + } + + SQLULEN rowsFetched_ = 777; + SQLUSMALLINT rowStatus_[kArraySize] = {9, 9, 9, 9}; + SQLINTEGER values_[kArraySize] = {-1, -1, -1, -1}; + SQLLEN indicators_[kArraySize] = {0, 0, 0, 0}; + + void SetRowsetAttrs() { + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, + (SQLPOINTER)kArraySize, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, + &rowsFetched_, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_STATUS_PTR, + rowStatus_, 0))); + } + + void BindColumn() { + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, values_, 0, indicators_))); + } + + // Poison every output so an unwritten value is visible. + void ResetOutputs() { + rowsFetched_ = 777; + for (SQLULEN i = 0; i < kArraySize; i++) { + rowStatus_[i] = 9; + values_[i] = -1; + } + } + + // Expects the six rows to arrive as a full rowset of 4 followed by a + // partial rowset of 2, with the counter and status array written both times. + void FetchAndVerifySixRows() { + ResetOutputs(); + SQLRETURN ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 4u) << "first rowset: SQL_ATTR_ROWS_FETCHED_PTR not written"; + for (SQLULEN i = 0; i < kArraySize; i++) { + EXPECT_EQ(rowStatus_[i], SQL_ROW_SUCCESS) << "row status " << i; + EXPECT_EQ(values_[i], (SQLINTEGER)(i + 1)) << "value " << i; + } + + ResetOutputs(); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 2u) << "last rowset: SQL_ATTR_ROWS_FETCHED_PTR not written"; + EXPECT_EQ(rowStatus_[0], SQL_ROW_SUCCESS); + EXPECT_EQ(rowStatus_[1], SQL_ROW_SUCCESS); + EXPECT_EQ(rowStatus_[2], SQL_ROW_NOROW); + EXPECT_EQ(rowStatus_[3], SQL_ROW_NOROW); + EXPECT_EQ(values_[0], 5); + EXPECT_EQ(values_[1], 6); + + ret = SQLFetch(hStmt); + EXPECT_EQ(ret, SQL_NO_DATA); + } +}; + +// Attributes set before SQLPrepare must survive it. +TEST_F(BlockCursorTest, RowsFetchedPtrSetBeforePrepare) { + SetRowsetAttrs(); + + SQLRETURN ret = SQLPrepare(hStmt, (SQLCHAR*)SixRowsSql(), SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + // The attribute must still be readable after the prepare. + SQLPOINTER ptr = nullptr; + ret = SQLGetStmtAttr(hStmt, SQL_ATTR_ROW_STATUS_PTR, &ptr, 0, nullptr); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(ptr, (SQLPOINTER)rowStatus_) << "SQLPrepare discarded SQL_ATTR_ROW_STATUS_PTR"; + + ret = SQLExecute(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + BindColumn(); + FetchAndVerifySixRows(); +} + +// Attributes set before SQLExecDirect must survive it. +TEST_F(BlockCursorTest, RowsFetchedPtrSetBeforeExecDirect) { + SetRowsetAttrs(); + ExecDirect(SixRowsSql()); + BindColumn(); + FetchAndVerifySixRows(); +} + +// Control: attributes set between SQLPrepare and SQLExecute already worked. +TEST_F(BlockCursorTest, RowsFetchedPtrSetAfterPrepare) { + SQLRETURN ret = SQLPrepare(hStmt, (SQLCHAR*)SixRowsSql(), SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + SetRowsetAttrs(); + ret = SQLExecute(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + BindColumn(); + FetchAndVerifySixRows(); +} + +// A statement handle reused for a second statement keeps the attributes +// (the pattern of an application that configures a handle once). +TEST_F(BlockCursorTest, RowsFetchedPtrSurvivesHandleReuse) { + SetRowsetAttrs(); + ExecDirect(SixRowsSql()); + BindColumn(); + FetchAndVerifySixRows(); + + ASSERT_TRUE(SQL_SUCCEEDED(SQLFreeStmt(hStmt, SQL_CLOSE))); + + ExecDirect(SixRowsSql()); + FetchAndVerifySixRows(); +}