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/5] 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/5] 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(); +} From 1b700da3cffc488f9f560d0f154e30166946275d Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 6 Sep 2026 18:18:53 -0300 Subject: [PATCH 3/5] tests: cover rowset fetches on every fetch path and binding layout Eight more block-cursor cases: SQLFetchScroll(SQL_FETCH_NEXT) and SQLExtendedFetch on a forward-only cursor, a rowset larger than the result set, row-wise binding, a static scrollable cursor in every orientation, and the two catalog paths (SQLTables results are static cursors, SQLGetTypeInfo results are not). Each asserts the rows-fetched counter, the status array and the bound values on every rowset, and a zero counter on SQL_NO_DATA. ColumnWiseBindingWithOffset is skipped with the reason: any bind-offset pointer sends the statement down the row-wise fetch path, which steps by SQL_ATTR_ROW_BIND_TYPE per row, and that is 0 for column-wise binding, so every row of the rowset lands on the same address. --- tests/test_block_cursor.cpp | 300 ++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/tests/test_block_cursor.cpp b/tests/test_block_cursor.cpp index 1dce0171..44d02ffa 100644 --- a/tests/test_block_cursor.cpp +++ b/tests/test_block_cursor.cpp @@ -6,6 +6,8 @@ // again; in particular SQLPrepare / SQLExecDirect must not discard them. #include "test_helpers.h" +#include +#include class BlockCursorTest : public OdbcConnectedTest { protected: @@ -18,6 +20,16 @@ class BlockCursorTest : public OdbcConnectedTest { " UNION ALL SELECT 5 FROM rdb$database UNION ALL SELECT 6 FROM rdb$database"; } + // Six rows with a second, character column. + static const char* SixRowsWithNamesSql() { + return "SELECT 1, CAST('r1' AS VARCHAR(4)) FROM rdb$database" + " UNION ALL SELECT 2, CAST('r2' AS VARCHAR(4)) FROM rdb$database" + " UNION ALL SELECT 3, CAST('r3' AS VARCHAR(4)) FROM rdb$database" + " UNION ALL SELECT 4, CAST('r4' AS VARCHAR(4)) FROM rdb$database" + " UNION ALL SELECT 5, CAST('r5' AS VARCHAR(4)) FROM rdb$database" + " UNION ALL SELECT 6, CAST('r6' AS VARCHAR(4)) FROM rdb$database"; + } + SQLULEN rowsFetched_ = 777; SQLUSMALLINT rowStatus_[kArraySize] = {9, 9, 9, 9}; SQLINTEGER values_[kArraySize] = {-1, -1, -1, -1}; @@ -45,6 +57,61 @@ class BlockCursorTest : public OdbcConnectedTest { } } + // One SQLFetchScroll, expected to return `expectRows` rows starting at + // `firstValue`, with the counter and the status array written. + void FetchScrollExpect(SQLSMALLINT orientation, SQLLEN offset, + SQLULEN expectRows, SQLINTEGER firstValue) { + ResetOutputs(); + SQLRETURN ret = SQLFetchScroll(hStmt, orientation, offset); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, expectRows); + for (SQLULEN i = 0; i < kArraySize; i++) { + if (i < expectRows) { + EXPECT_EQ(rowStatus_[i], SQL_ROW_SUCCESS) << "row status " << i; + EXPECT_EQ(values_[i], firstValue + (SQLINTEGER)i) << "value " << i; + } else { + EXPECT_EQ(rowStatus_[i], SQL_ROW_NOROW) << "row status " << i; + } + } + } + + // Opens a catalog result set through `open` on a fresh statement and + // counts its rows with the default rowset of one, then opens it again + // on hStmt with the rowset attributes set beforehand and checks that + // every fetch reports a consistent rowset and that the rowsets add up. + void ExpectCatalogRowsets(const std::function& open) { + SQLHSTMT ref = AllocExtraStmt(); + SQLRETURN ret = open(ref); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, ref); + SQLULEN total = 0; + while (SQL_SUCCEEDED(SQLFetch(ref))) total++; + SQLFreeHandle(SQL_HANDLE_STMT, ref); + ASSERT_GT(total, kArraySize) << "need more rows than one rowset"; + + SetRowsetAttrs(); + ret = open(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLULEN sum = 0; + bool sawPartial = false; + for (;;) { + ResetOutputs(); + ret = SQLFetch(hStmt); + if (ret == SQL_NO_DATA) break; + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + ASSERT_FALSE(sawPartial) << "a partial rowset must be the last one"; + ASSERT_GE(rowsFetched_, 1u); + ASSERT_LE(rowsFetched_, kArraySize); + for (SQLULEN i = 0; i < kArraySize; i++) + EXPECT_EQ(rowStatus_[i], i < rowsFetched_ ? SQL_ROW_SUCCESS : SQL_ROW_NOROW) + << "row status " << i << " after " << sum << " rows"; + sawPartial = rowsFetched_ < kArraySize; + sum += rowsFetched_; + } + EXPECT_EQ(sum, total); + EXPECT_EQ(rowsFetched_, 0u) << "SQL_NO_DATA must report zero rows fetched"; + } + // 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() { @@ -124,3 +191,236 @@ TEST_F(BlockCursorTest, RowsFetchedPtrSurvivesHandleReuse) { ExecDirect(SixRowsSql()); FetchAndVerifySixRows(); } + +// --- Other fetch entry points, layouts and cursor kinds -------------------- + +// SQLFetchScroll(SQL_FETCH_NEXT) on a forward-only cursor takes the same path +// as SQLFetch and must report the rowset the same way, including a zero +// counter on SQL_NO_DATA. +TEST_F(BlockCursorTest, FetchScrollNextRowsets) { + SetRowsetAttrs(); + ExecDirect(SixRowsSql()); + BindColumn(); + + FetchScrollExpect(SQL_FETCH_NEXT, 0, 4, 1); + FetchScrollExpect(SQL_FETCH_NEXT, 0, 2, 5); + + ResetOutputs(); + SQLRETURN ret = SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_NO_DATA); + EXPECT_EQ(rowsFetched_, 0u) << "SQL_NO_DATA must report zero rows fetched"; +} + +// SQLExtendedFetch receives the counter and the status array as arguments. +TEST_F(BlockCursorTest, ExtendedFetchRowsets) { + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, + (SQLPOINTER)kArraySize, 0))); + ExecDirect(SixRowsSql()); + BindColumn(); + + ResetOutputs(); + SQLRETURN ret = SQLExtendedFetch(hStmt, SQL_FETCH_NEXT, 0, &rowsFetched_, rowStatus_); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 4u); + 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 = SQLExtendedFetch(hStmt, SQL_FETCH_NEXT, 0, &rowsFetched_, rowStatus_); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 2u); + 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); + + ResetOutputs(); + ret = SQLExtendedFetch(hStmt, SQL_FETCH_NEXT, 0, &rowsFetched_, rowStatus_); + EXPECT_EQ(ret, SQL_NO_DATA); + EXPECT_EQ(rowsFetched_, 0u) << "SQL_NO_DATA must report zero rows fetched"; +} + +// A rowset larger than the whole result set: one partial fetch, then +// SQL_NO_DATA. +TEST_F(BlockCursorTest, ArraySizeLargerThanResultSet) { + constexpr SQLULEN kBig = 10; + SQLULEN fetched = 777; + SQLUSMALLINT status[kBig]; + SQLINTEGER values[kBig]; + SQLLEN indicators[kBig]; + for (SQLULEN i = 0; i < kBig; i++) { + status[i] = 9; + values[i] = -1; + } + + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)kBig, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &fetched, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_STATUS_PTR, status, 0))); + ExecDirect(SixRowsSql()); + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, values, 0, indicators))); + + SQLRETURN ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(fetched, 6u); + for (SQLULEN i = 0; i < kBig; i++) { + if (i < 6) { + EXPECT_EQ(status[i], SQL_ROW_SUCCESS) << "row status " << i; + EXPECT_EQ(values[i], (SQLINTEGER)(i + 1)) << "value " << i; + } else { + EXPECT_EQ(status[i], SQL_ROW_NOROW) << "row status " << i; + EXPECT_EQ(values[i], -1) << "value " << i << " must be untouched"; + } + } + + fetched = 777; + ret = SQLFetch(hStmt); + EXPECT_EQ(ret, SQL_NO_DATA); + EXPECT_EQ(fetched, 0u) << "SQL_NO_DATA must report zero rows fetched"; +} + +// Row-wise binding: SQL_ATTR_ROW_BIND_TYPE is the size of one row structure, +// and every bound address is advanced by it for each row of the rowset. +TEST_F(BlockCursorTest, RowWiseBinding) { + struct Row { + SQLINTEGER id; + SQLLEN idInd; + SQLCHAR name[8]; + SQLLEN nameInd; + }; + Row rows[kArraySize]; + memset(rows, 0, sizeof(rows)); + + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_BIND_TYPE, + (SQLPOINTER)sizeof(Row), 0))); + SetRowsetAttrs(); + ExecDirect(SixRowsWithNamesSql()); + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, &rows[0].id, 0, &rows[0].idInd))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 2, SQL_C_CHAR, rows[0].name, + sizeof(rows[0].name), &rows[0].nameInd))); + + ResetOutputs(); + SQLRETURN ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 4u); + for (SQLULEN i = 0; i < kArraySize; i++) { + char expected[8]; + snprintf(expected, sizeof(expected), "r%d", (int)(i + 1)); + EXPECT_EQ(rowStatus_[i], SQL_ROW_SUCCESS) << "row status " << i; + EXPECT_EQ(rows[i].id, (SQLINTEGER)(i + 1)) << "id " << i; + EXPECT_EQ(rows[i].idInd, (SQLLEN)sizeof(SQLINTEGER)) << "id indicator " << i; + EXPECT_STREQ((const char*)rows[i].name, expected) << "name " << i; + EXPECT_EQ(rows[i].nameInd, 2) << "name indicator " << i; + } + + memset(rows, 0, sizeof(rows)); + ResetOutputs(); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 2u); + EXPECT_EQ(rows[0].id, 5); + EXPECT_STREQ((const char*)rows[0].name, "r5"); + EXPECT_EQ(rows[1].id, 6); + EXPECT_STREQ((const char*)rows[1].name, "r6"); + EXPECT_EQ(rows[2].id, 0) << "row 2 must be untouched"; + EXPECT_EQ(rowStatus_[2], SQL_ROW_NOROW); +} + +// Column-wise binding with SQL_ATTR_ROW_BIND_OFFSET_PTR: the offset is added +// to every bound data and indicator address when the rowset is fetched, and +// the rows still step through the arrays element by element. +// +// Skipped: the driver routes any bind-offset pointer through its row-wise +// fetch path, which advances the addresses by SQL_ATTR_ROW_BIND_TYPE per +// row. That is 0 for column-wise binding, so every row of the rowset is +// written to the same (shifted) first element while the counter and the +// status array still report a full rowset. +TEST_F(BlockCursorTest, ColumnWiseBindingWithOffset) { + GTEST_SKIP() << "Driver defect: column-wise binding with SQL_ATTR_ROW_BIND_OFFSET_PTR " + "writes every row of a rowset to the same address (row stride 0)"; + + constexpr SQLULEN kSlots = 32; + SQLINTEGER values[kSlots]; + SQLLEN indicators[kSlots]; + for (SQLULEN i = 0; i < kSlots; i++) { + values[i] = -1; + indicators[i] = -99; + } + // A multiple of both element sizes, so the shifted rows start on an + // element boundary of each array. + SQLLEN offset = 8 * sizeof(SQLLEN); + const SQLULEN valueBase = offset / sizeof(SQLINTEGER); + const SQLULEN indBase = offset / sizeof(SQLLEN); + + SetRowsetAttrs(); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, &offset, 0))); + ExecDirect(SixRowsSql()); + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, values, 0, indicators))); + + ResetOutputs(); + SQLRETURN ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 4u); + for (SQLULEN i = 0; i < kArraySize; i++) { + EXPECT_EQ(values[valueBase + i], (SQLINTEGER)(i + 1)) << "shifted value " << i; + EXPECT_EQ(indicators[indBase + i], (SQLLEN)sizeof(SQLINTEGER)) << "shifted indicator " << i; + EXPECT_EQ(values[i], -1) << "unshifted slot " << i << " must be untouched"; + } + + // Dropping the offset to zero before the next fetch moves the rows back + // to the start of the arrays. + offset = 0; + ResetOutputs(); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 2u); + EXPECT_EQ(values[0], 5); + EXPECT_EQ(values[1], 6); + EXPECT_EQ(indicators[0], (SQLLEN)sizeof(SQLINTEGER)); + EXPECT_EQ(indicators[1], (SQLLEN)sizeof(SQLINTEGER)); + EXPECT_EQ(values[2], -1) << "slot 2 must be untouched by a two-row rowset"; +} + +// A static scrollable cursor fetches rowsets in every orientation and +// reports each one through the same counter and status array. +TEST_F(BlockCursorTest, StaticCursorRowsets) { + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_STATIC, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_CURSOR_SCROLLABLE, + (SQLPOINTER)SQL_SCROLLABLE, 0))); + SetRowsetAttrs(); + ExecDirect(SixRowsSql()); + BindColumn(); + + FetchScrollExpect(SQL_FETCH_NEXT, 0, 4, 1); + FetchScrollExpect(SQL_FETCH_NEXT, 0, 2, 5); + + ResetOutputs(); + SQLRETURN ret = SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_NO_DATA); + EXPECT_EQ(rowsFetched_, 0u) << "SQL_NO_DATA must report zero rows fetched"; + + FetchScrollExpect(SQL_FETCH_FIRST, 0, 4, 1); + FetchScrollExpect(SQL_FETCH_ABSOLUTE, 5, 2, 5); + FetchScrollExpect(SQL_FETCH_ABSOLUTE, 3, 4, 3); + FetchScrollExpect(SQL_FETCH_LAST, 0, 4, 3); + FetchScrollExpect(SQL_FETCH_FIRST, 0, 4, 1); + FetchScrollExpect(SQL_FETCH_RELATIVE, 2, 4, 3); +} + +// Catalog result sets from SQLTables take the static-cursor fetch path. +TEST_F(BlockCursorTest, CatalogRowsetsViaSqlTables) { + ExpectCatalogRowsets([](SQLHSTMT s) { + return SQLTables(s, NULL, 0, NULL, 0, NULL, 0, NULL, 0); + }); +} + +// SQLGetTypeInfo's result set takes the ordinary forward fetch path. +TEST_F(BlockCursorTest, CatalogRowsetsViaSqlGetTypeInfo) { + ExpectCatalogRowsets([](SQLHSTMT s) { + return SQLGetTypeInfo(s, SQL_ALL_TYPES); + }); +} From 239fb39a6cb719e161605593b49f95d47f94b20c Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:10:45 -0300 Subject: [PATCH 4/5] tests: column-wise binding with SQL_ATTR_ROW_BIND_OFFSET_PTR on both fetch paths ColumnWiseBindingWithOffset comes out of its skip, and a static-cursor variant is added; the body moves into a fixture helper the two share. Six rows, rowset 4, SQLBindCol(SQL_C_SLONG, values, 0, indicators) with an offset of 8 * sizeof(SQLLEN) bytes, then a second rowset with the offset back at zero; every shifted slot, every unshifted slot and both indicator arrays are checked. --- tests/test_block_cursor.cpp | 98 +++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/tests/test_block_cursor.cpp b/tests/test_block_cursor.cpp index 44d02ffa..14b12f24 100644 --- a/tests/test_block_cursor.cpp +++ b/tests/test_block_cursor.cpp @@ -57,6 +57,54 @@ class BlockCursorTest : public OdbcConnectedTest { } } + // Column-wise binding with SQL_ATTR_ROW_BIND_OFFSET_PTR: every bound + // address is shifted by the offset and the rows still step by element + // size. Two rowsets, the second with the offset back at zero. + void ColumnWiseOffsetCase(bool useFetchScroll) { + constexpr SQLULEN kSlots = 32; + SQLINTEGER values[kSlots]; + SQLLEN indicators[kSlots]; + for (SQLULEN i = 0; i < kSlots; i++) { + values[i] = -1; + indicators[i] = -99; + } + // A multiple of both element sizes, so the shifted rows start on an + // element boundary of each array. + SQLLEN offset = 8 * sizeof(SQLLEN); + const SQLULEN valueBase = offset / sizeof(SQLINTEGER); + const SQLULEN indBase = offset / sizeof(SQLLEN); + + SetRowsetAttrs(); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, &offset, 0))); + ExecDirect(SixRowsSql()); + ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, values, 0, indicators))); + + ResetOutputs(); + SQLRETURN ret = useFetchScroll ? SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0) : SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 4u); + for (SQLULEN i = 0; i < kArraySize; i++) { + EXPECT_EQ(rowStatus_[i], SQL_ROW_SUCCESS) << "row status " << i; + EXPECT_EQ(values[valueBase + i], (SQLINTEGER)(i + 1)) << "shifted value " << i; + EXPECT_EQ(indicators[indBase + i], (SQLLEN)sizeof(SQLINTEGER)) << "shifted indicator " << i; + EXPECT_EQ(values[i], -1) << "unshifted slot " << i << " must be untouched"; + } + + // Dropping the offset to zero before the next fetch moves the rows + // back to the start of the arrays. + offset = 0; + ResetOutputs(); + ret = useFetchScroll ? SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0) : SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(rowsFetched_, 2u); + EXPECT_EQ(values[0], 5); + EXPECT_EQ(values[1], 6); + EXPECT_EQ(indicators[0], (SQLLEN)sizeof(SQLINTEGER)); + EXPECT_EQ(indicators[1], (SQLLEN)sizeof(SQLINTEGER)); + EXPECT_EQ(values[2], -1) << "slot 2 must be untouched by a two-row rowset"; + EXPECT_EQ(values[valueBase], 1) << "the first rowset must stay where it was written"; + } + // One SQLFetchScroll, expected to return `expectRows` rows starting at // `firstValue`, with the counter and the status array written. void FetchScrollExpect(SQLSMALLINT orientation, SQLLEN offset, @@ -339,49 +387,15 @@ TEST_F(BlockCursorTest, RowWiseBinding) { // written to the same (shifted) first element while the counter and the // status array still report a full rowset. TEST_F(BlockCursorTest, ColumnWiseBindingWithOffset) { - GTEST_SKIP() << "Driver defect: column-wise binding with SQL_ATTR_ROW_BIND_OFFSET_PTR " - "writes every row of a rowset to the same address (row stride 0)"; - - constexpr SQLULEN kSlots = 32; - SQLINTEGER values[kSlots]; - SQLLEN indicators[kSlots]; - for (SQLULEN i = 0; i < kSlots; i++) { - values[i] = -1; - indicators[i] = -99; - } - // A multiple of both element sizes, so the shifted rows start on an - // element boundary of each array. - SQLLEN offset = 8 * sizeof(SQLLEN); - const SQLULEN valueBase = offset / sizeof(SQLINTEGER); - const SQLULEN indBase = offset / sizeof(SQLLEN); - - SetRowsetAttrs(); - ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, &offset, 0))); - ExecDirect(SixRowsSql()); - ASSERT_TRUE(SQL_SUCCEEDED(SQLBindCol(hStmt, 1, SQL_C_SLONG, values, 0, indicators))); - - ResetOutputs(); - SQLRETURN ret = SQLFetch(hStmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); - EXPECT_EQ(rowsFetched_, 4u); - for (SQLULEN i = 0; i < kArraySize; i++) { - EXPECT_EQ(values[valueBase + i], (SQLINTEGER)(i + 1)) << "shifted value " << i; - EXPECT_EQ(indicators[indBase + i], (SQLLEN)sizeof(SQLINTEGER)) << "shifted indicator " << i; - EXPECT_EQ(values[i], -1) << "unshifted slot " << i << " must be untouched"; - } + ColumnWiseOffsetCase(false); +} - // Dropping the offset to zero before the next fetch moves the rows back - // to the start of the arrays. - offset = 0; - ResetOutputs(); - ret = SQLFetch(hStmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); - EXPECT_EQ(rowsFetched_, 2u); - EXPECT_EQ(values[0], 5); - EXPECT_EQ(values[1], 6); - EXPECT_EQ(indicators[0], (SQLLEN)sizeof(SQLINTEGER)); - EXPECT_EQ(indicators[1], (SQLLEN)sizeof(SQLINTEGER)); - EXPECT_EQ(values[2], -1) << "slot 2 must be untouched by a two-row rowset"; +TEST_F(BlockCursorTest, ColumnWiseBindingWithOffsetStaticCursor) { + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_STATIC, 0))); + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_CURSOR_SCROLLABLE, + (SQLPOINTER)SQL_SCROLLABLE, 0))); + ColumnWiseOffsetCase(true); } // A static scrollable cursor fetches rowsets in every orientation and From 5056261895f514c3c72d59e0f9102554530c39eb Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:10:46 -0300 Subject: [PATCH 5/5] Apply the bind offset to column-wise rowsets instead of taking the row-wise path getSchemaFetchData() selected the row-wise fetch branch whenever a bind-offset pointer was set, not only for row-wise binding. That branch advances one byte offset per row by SQL_ATTR_ROW_BIND_TYPE, which is 0 for column-wise binding, so every row of a rowset was written to the same address: the bound address plus the offset. The row-wise branch is now taken only when the bind type is not SQL_BIND_BY_COLUMN. The column-wise branch reads the application's offset once per fetch and passes the row number and the offset to returnDataFromExtendedFetch, which computes offset + elementSize * row for the data and starts the indicator offset at the same base; the row counter no longer travels through the descriptor's pointer. Same change in fetchData and in the static-cursor twin. Fixes #306 --- OdbcStatement.cpp | 21 ++++++++++----------- OdbcStatement.h | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 028a40d9..7461d95c 100644 --- a/OdbcStatement.cpp +++ b/OdbcStatement.cpp @@ -801,8 +801,9 @@ SQLRETURN OdbcStatement::fetchData() } else // if ( schemaExtendedFetchData ) { - SQLLEN bindOffsetPtrData = 0; - SQLLEN bindOffsetPtrInd = 0; + SQLLEN bindOffset = bindOffsetPtrSave ? *bindOffsetPtrSave : 0; + SQLLEN bindOffsetPtrData = bindOffset; + SQLLEN bindOffsetPtrInd = bindOffset; convert->setBindOffsetPtrTo(&bindOffsetPtrData, &bindOffsetPtrInd); while ( nRow < nCountRow && (resultSet->*fetchNext)() ) { @@ -810,10 +811,9 @@ SQLRETURN OdbcStatement::fetchData() ++rowNumber; // Should stand only here!!! if ( fetchRetData == SQL_RD_ON ) - returnDataFromExtendedFetch(); + returnDataFromExtendedFetch( nRow, bindOffset ); bindOffsetPtrInd += sizeof(SQLLEN); - ++bindOffsetPtrTmp; ++nRow; if ( maxRows && nRow == maxRows ) @@ -1136,8 +1136,9 @@ SQLRETURN OdbcStatement::sqlFetchScrollCursorStatic(int orientation, int offset) } else { - SQLLEN bindOffsetPtrData = 0; - SQLLEN bindOffsetPtrInd = 0; + SQLLEN bindOffset = bindOffsetPtrSave ? *bindOffsetPtrSave : 0; + SQLLEN bindOffsetPtrData = bindOffset; + SQLLEN bindOffsetPtrInd = bindOffset; convert->setBindOffsetPtrTo(&bindOffsetPtrData, &bindOffsetPtrInd); while ( nRow < rowsetSize && rowNumber < sqlDiagCursorRowCount ) { @@ -1145,10 +1146,9 @@ SQLRETURN OdbcStatement::sqlFetchScrollCursorStatic(int orientation, int offset) ++countFetched; ++rowNumber; // Should stand only here!!! - returnDataFromExtendedFetch(); + returnDataFromExtendedFetch( nRow, bindOffset ); bindOffsetPtrInd += sizeof(SQLLEN); - ++bindOffsetPtrTmp; ++nRow; if ( maxRows && nRow == maxRows ) @@ -3352,11 +3352,10 @@ SQLRETURN OdbcStatement::returnData() } inline -SQLRETURN OdbcStatement::returnDataFromExtendedFetch() +SQLRETURN OdbcStatement::returnDataFromExtendedFetch( int row, SQLLEN bindOffset ) { SQLRETURN retCode, ret = SQL_SUCCESS; SQLLEN &bindOffsetPtrTo = convert->getBindOffsetPtrTo(); - SQLLEN ¤tRow = *applicationRowDescriptor->headBindOffsetPtr; int count = listBindOut->GetCount(); convert->statusReturnData = true; @@ -3369,7 +3368,7 @@ SQLRETURN OdbcStatement::returnDataFromExtendedFetch() DescRecord *& imp = bindCol->impRecord; DescRecord *& app = bindCol->appRecord; - bindOffsetPtrTo = app->sizeColumnExtendedFetch * currentRow; + bindOffsetPtrTo = bindOffset + app->sizeColumnExtendedFetch * row; retCode = (convert->*imp->fnConv)(imp, app); if ( retCode != SQL_SUCCESS ) { diff --git a/OdbcStatement.h b/OdbcStatement.h index aca8696a..a5676708 100644 --- a/OdbcStatement.h +++ b/OdbcStatement.h @@ -61,7 +61,7 @@ class OdbcStatement : public OdbcObject SQLRETURN sqlMoreResults(); inline SQLRETURN fetchData(); inline SQLRETURN returnData(); - inline SQLRETURN returnDataFromExtendedFetch(); + inline SQLRETURN returnDataFromExtendedFetch( int row, SQLLEN bindOffset ); #ifdef _WIN64 SQLRETURN sqlColAttribute( int column, int fieldId, SQLPOINTER attributePtr, int bufferLength, SQLSMALLINT *strLengthPtr, SQLLEN *numericAttributePtr ); #else @@ -134,7 +134,7 @@ class OdbcStatement : public OdbcObject ~OdbcStatement(); bool isStaticCursor(){ return cursorType != SQL_CURSOR_FORWARD_ONLY && cursorScrollable == SQL_SCROLLABLE || isResultSetFromSystemCatalog; } int getCurrentFetched(){ return countFetched; } - bool getSchemaFetchData(){ return applicationRowDescriptor->headBindType || applicationRowDescriptor->headBindOffsetPtr; } + bool getSchemaFetchData(){ return applicationRowDescriptor->headBindType != SQL_BIND_BY_COLUMN; } inline StatementMetaData *getStatementMetaDataIRD(); inline void clearErrors(); SQLRETURN prepareGetData(int column, DescRecord *recordARD);