From 6d9cffdbb9c65670df0fea2c5f1c7c6e9b0f70ad Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:04:46 -0300 Subject: [PATCH 1/3] tests: SQL_ATTR_KEYSET_SIZE and the IRD before SQLPrepare KeysetSizeDoesNotChangeRowArraySize sets SQL_ATTR_KEYSET_SIZE and reads both it and SQL_ATTR_ROW_ARRAY_SIZE back, then sets SQL_ROWSET_SIZE and checks the two stay independent. IrdHeaderFieldsReadableBeforePrepare reads SQL_DESC_ROWS_PROCESSED_PTR, SQL_DESC_ARRAY_STATUS_PTR and SQL_DESC_ALLOC_TYPE from the IRD of a statement that has not been prepared. IrdRecordFieldsBeforePrepareAreHY007 expects HY007 for SQL_DESC_COUNT and a record field in that state, and success for both once the statement is prepared. --- tests/test_descriptor.cpp | 56 ++++++++++++++++++++++++++++++++ tests/test_scrollable_cursor.cpp | 25 ++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/tests/test_descriptor.cpp b/tests/test_descriptor.cpp index 3060b8ff..ec0180a6 100644 --- a/tests/test_descriptor.cpp +++ b/tests/test_descriptor.cpp @@ -366,3 +366,59 @@ TEST_F(CopyDescCrashTest, SetDescCountToZeroUnbindsAll) { SQLFreeHandle(SQL_HANDLE_DESC, hDesc); } + +// ===== IRD before SQLPrepare (#307) ===== +TEST_F(DescriptorTest, IrdHeaderFieldsReadableBeforePrepare) { + SQLHDESC hIrd = SQL_NULL_HDESC; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_IMP_ROW_DESC, &hIrd, 0, NULL))); + ASSERT_NE(hIrd, (SQLHDESC)SQL_NULL_HDESC); + + SQLULEN rowsFetched = 0; + SQLUSMALLINT rowStatus[4] = {}; + 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))); + + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetDescField(hIrd, 0, SQL_DESC_ROWS_PROCESSED_PTR, &ptr, 0, NULL); + if (ret == SQL_ERROR && GetSqlState(SQL_HANDLE_DESC, hIrd) == "HY007") { + // unixODBC answers HY007 for an unprepared IRD before the driver sees the call + GTEST_SKIP() << "the driver manager enforces HY007 itself: " << GetOdbcError(SQL_HANDLE_DESC, hIrd); + } + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_DESC, hIrd); + EXPECT_EQ(ptr, (SQLPOINTER)&rowsFetched); + + ptr = NULL; + ret = SQLGetDescField(hIrd, 0, SQL_DESC_ARRAY_STATUS_PTR, &ptr, 0, NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_DESC, hIrd); + EXPECT_EQ(ptr, (SQLPOINTER)rowStatus); + + SQLSMALLINT allocType = 0; + ret = SQLGetDescField(hIrd, 0, SQL_DESC_ALLOC_TYPE, &allocType, 0, NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_DESC, hIrd); + EXPECT_EQ(allocType, SQL_DESC_ALLOC_AUTO); +} + +TEST_F(DescriptorTest, IrdRecordFieldsBeforePrepareAreHY007) { + SQLHDESC hIrd = SQL_NULL_HDESC; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_IMP_ROW_DESC, &hIrd, 0, NULL))); + ASSERT_NE(hIrd, (SQLHDESC)SQL_NULL_HDESC); + + SQLSMALLINT count = -1; + SQLRETURN ret = SQLGetDescField(hIrd, 0, SQL_DESC_COUNT, &count, 0, NULL); + EXPECT_EQ(ret, SQL_ERROR); + EXPECT_EQ(GetSqlState(SQL_HANDLE_DESC, hIrd), "HY007"); + + SQLSMALLINT type = 0; + ret = SQLGetDescField(hIrd, 1, SQL_DESC_TYPE, &type, 0, NULL); + EXPECT_EQ(ret, SQL_ERROR); + EXPECT_EQ(GetSqlState(SQL_HANDLE_DESC, hIrd), "HY007"); + + // Both answer once the statement is prepared + ret = SQLPrepare(hStmt, (SQLCHAR*)"SELECT 1 AS A FROM RDB$DATABASE", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + ret = SQLGetDescField(hIrd, 0, SQL_DESC_COUNT, &count, 0, NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_DESC, hIrd); + EXPECT_EQ(count, 1); + ret = SQLGetDescField(hIrd, 1, SQL_DESC_TYPE, &type, 0, NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_DESC, hIrd); +} diff --git a/tests/test_scrollable_cursor.cpp b/tests/test_scrollable_cursor.cpp index 7f3d2dbe..46da644a 100644 --- a/tests/test_scrollable_cursor.cpp +++ b/tests/test_scrollable_cursor.cpp @@ -193,3 +193,28 @@ TEST_F(ScrollableCursorTest, RewindAfterEnd) { ASSERT_TRUE(SQL_SUCCEEDED(ret)); EXPECT_EQ(FetchID(), 1); } + +// ===== SQL_ATTR_KEYSET_SIZE is its own attribute, not the rowset size (#307) ===== +TEST_F(ScrollableCursorTest, KeysetSizeDoesNotChangeRowArraySize) { + SQLULEN v = 0; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, &v, 0, NULL))); + EXPECT_EQ(v, 1u); + + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ATTR_KEYSET_SIZE, (SQLPOINTER)7, 0))); + v = 0; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, &v, 0, NULL))); + EXPECT_EQ(v, 1u) << "SQL_ATTR_KEYSET_SIZE must not touch the rowset size"; + v = 0; + SQLRETURN ret = SQLGetStmtAttr(hStmt, SQL_ATTR_KEYSET_SIZE, &v, 0, NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + EXPECT_EQ(v, 7u); + + // The ODBC 2 name of the rowset size still sets it, and leaves the keyset size alone + ASSERT_TRUE(SQL_SUCCEEDED(SQLSetStmtAttr(hStmt, SQL_ROWSET_SIZE, (SQLPOINTER)3, 0))); + v = 0; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, &v, 0, NULL))); + EXPECT_EQ(v, 3u); + v = 0; + ASSERT_TRUE(SQL_SUCCEEDED(SQLGetStmtAttr(hStmt, SQL_ATTR_KEYSET_SIZE, &v, 0, NULL))); + EXPECT_EQ(v, 7u); +} From 465ac2bb865fe5d00ee0333b28ca3cbba9b35dc1 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:04:46 -0300 Subject: [PATCH 2/3] Keep SQL_ATTR_KEYSET_SIZE apart from the rowset size sqlSetStmtAttr handled SQL_ATTR_KEYSET_SIZE in the same case as the ODBC 2 SQL_ROWSET_SIZE and stored it in the ARD's array size. An application that set a keyset size (ODBC 2 applications and generic layers do, with values such as 100) silently got a rowset of that size, and the next SQLFetch wrote that many rows into buffers bound for one, with no rows-fetched counter or status array to warn it. The value now goes to its own field, read back by sqlGetStmtAttr; the driver has no keyset-driven cursors, so the number is stored and returned as the specification allows. --- OdbcStatement.cpp | 10 ++++++++++ OdbcStatement.h | 1 + 2 files changed, 11 insertions(+) diff --git a/OdbcStatement.cpp b/OdbcStatement.cpp index 028a40d9..8a18ac6a 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; + keysetSize = 0; fetchNext = &ResultSet::nextFetch; schemaFetchData = true; metaData = NULL; @@ -2577,6 +2578,11 @@ SQLRETURN OdbcStatement::sqlGetStmtAttr(int attribute, SQLPOINTER ptr, int buffe TRACE02(SQL_ROWSET_SIZE,value); break; + case SQL_ATTR_KEYSET_SIZE: + value = keysetSize; + TRACE02(SQL_ATTR_KEYSET_SIZE,value); + break; + case SQL_ATTR_MAX_ROWS: // SQL_MAX_ROWS 1 value = maxRows; TRACE02(SQL_ATTR_MAX_ROWS,value); @@ -3434,6 +3440,10 @@ SQLRETURN OdbcStatement::sqlSetStmtAttr(int attribute, SQLPOINTER ptr, int lengt break; case SQL_ATTR_KEYSET_SIZE: // 8 + keysetSize = (uintptr_t) ptr; + TRACE02(SQL_ATTR_KEYSET_SIZE,(intptr_t) ptr); + break; + case SQL_ROWSET_SIZE: // 9 applicationRowDescriptor->headArraySize = (intptr_t) ptr; TRACE02(SQL_ROWSET_SIZE,(intptr_t) ptr); diff --git a/OdbcStatement.h b/OdbcStatement.h index aca8696a..9c90e575 100644 --- a/OdbcStatement.h +++ b/OdbcStatement.h @@ -192,6 +192,7 @@ class OdbcStatement : public OdbcObject bool asyncEnable; int rowNumber; int rowNumberParamArray; + SQLULEN keysetSize; int lastRowsetSize; SQLLEN indicatorRowNumber; int maxRows; From dc6f9e62bd897107cd07fb53d9846f88471dc36e Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 7 Sep 2026 03:04:46 -0300 Subject: [PATCH 3/3] Answer HY007 for IRD record fields before the statement is prepared sqlGetDescField refused every field of an undefined descriptor with HY091 "Invalid descriptor field identifier". For the IRD that state means "not prepared yet", and the header fields (the array-status and rows-processed pointers among them, which the application itself set through SQLSetStmtAttr) do not depend on the result set at all. The header fields are now returned regardless of the prepared state. SQL_DESC_COUNT and the record fields of an unprepared IRD answer HY007 "Associated statement is not prepared"; other descriptor types keep the HY091 they gave before. --- OdbcDesc.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/OdbcDesc.cpp b/OdbcDesc.cpp index fdc252e2..8c5c32a6 100644 --- a/OdbcDesc.cpp +++ b/OdbcDesc.cpp @@ -336,7 +336,22 @@ SQLRETURN OdbcDesc::sqlGetDescField(int recNumber, int fieldId, SQLPOINTER ptr, DescRecord *record = NULL; if ( bDefined == false ) - return sqlReturn (SQL_ERROR, "HY091", "Invalid descriptor field identifier"); + { + switch (fieldId) + { + case SQL_DESC_ALLOC_TYPE: + case SQL_DESC_ARRAY_SIZE: + case SQL_DESC_ARRAY_STATUS_PTR: + case SQL_DESC_BIND_OFFSET_PTR: + case SQL_DESC_BIND_TYPE: + case SQL_DESC_ROWS_PROCESSED_PTR: + break; // header fields do not depend on the result set + default: + if ( headType == odtImplementationRow ) + return sqlReturn (SQL_ERROR, "HY007", "Associated statement is not prepared"); + return sqlReturn (SQL_ERROR, "HY091", "Invalid descriptor field identifier"); + } + } if ( recNumber > headCount ) return sqlReturn (SQL_NO_DATA_FOUND, "HY021", "Inconsistent descriptor information");