This is an automated email from the ASF dual-hosted git repository.

jduong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 4fc9f9e372 GH-47715: [C++][FlightRPC] ODBC scroll fetch implementation 
(#48041)
4fc9f9e372 is described below

commit 4fc9f9e3723e22449067d8efb17f469708bec98d
Author: Alina (Xi) Li <[email protected]>
AuthorDate: Mon Dec 15 02:46:14 2025 -0800

    GH-47715: [C++][FlightRPC] ODBC scroll fetch implementation (#48041)
    
    ### Rationale for this change
    ODBC scroll fetch implementation. This is part of advance ODBC data 
fetching.
    
    ### What changes are included in this PR?
    - Implement SQLFetchScroll, only fetch orientation `SQL_FETCH_NEXT` is 
supported, which makes it has same effect as SQLFetch
    - Tests
    ### Are these changes tested?
    Tested in local MSVC.
    ### Are there any user-facing changes?
    
    N/A
    * GitHub Issue: #47715
    
    Authored-by: Alina (Xi) Li <[email protected]>
    Signed-off-by: James Duong <[email protected]>
---
 cpp/src/arrow/flight/sql/odbc/odbc_api.cc          |  27 ++++-
 .../arrow/flight/sql/odbc/tests/statement_test.cc  | 114 +++++++++++++++++++--
 2 files changed, 129 insertions(+), 12 deletions(-)

diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc 
b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc
index 464f712a5d..50ea395ec4 100644
--- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc
+++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc
@@ -1075,8 +1075,31 @@ SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT 
fetch_orientation,
   ARROW_LOG(DEBUG) << "SQLFetchScroll called with stmt: " << stmt
                    << ", fetch_orientation: " << fetch_orientation
                    << ", fetch_offset: " << fetch_offset;
-  // GH-47715 TODO: Implement SQLFetchScroll
-  return SQL_INVALID_HANDLE;
+
+  using ODBC::ODBCDescriptor;
+  using ODBC::ODBCStatement;
+  return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() {
+    // Only SQL_FETCH_NEXT forward-only fetching orientation is supported,
+    // meaning the behavior of SQLExtendedFetch is same as SQLFetch.
+    if (fetch_orientation != SQL_FETCH_NEXT) {
+      throw DriverException("Optional feature not supported.", "HYC00");
+    }
+    // Ignore fetch_offset as it's not applicable to SQL_FETCH_NEXT
+    ARROW_UNUSED(fetch_offset);
+
+    ODBCStatement* statement = reinterpret_cast<ODBCStatement*>(stmt);
+
+    // The SQL_ATTR_ROW_ARRAY_SIZE statement attribute specifies the number of 
rows in the
+    // rowset.
+    ODBCDescriptor* ard = statement->GetARD();
+    size_t rows = static_cast<size_t>(ard->GetArraySize());
+    if (statement->Fetch(rows)) {
+      return SQL_SUCCESS;
+    } else {
+      // Reached the end of rowset
+      return SQL_NO_DATA;
+    }
+  });
 }
 
 SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT record_number, SQLSMALLINT 
c_type,
diff --git a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc 
b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc
index bb44ccf724..19caba19ca 100644
--- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc
+++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc
@@ -48,13 +48,13 @@ TYPED_TEST(StatementTest, TestSQLExecDirectSimpleQuery) {
 
   SQLINTEGER val;
 
-  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
   // Verify 1 is returned
   EXPECT_EQ(1, val);
 
   ASSERT_EQ(SQL_NO_DATA, SQLFetch(this->stmt));
 
-  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
   // Invalid cursor state
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000);
 }
@@ -82,14 +82,14 @@ TYPED_TEST(StatementTest, TestSQLExecuteSimpleQuery) {
   ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt));
 
   SQLINTEGER val;
-  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
 
   // Verify 1 is returned
   EXPECT_EQ(1, val);
 
   ASSERT_EQ(SQL_NO_DATA, SQLFetch(this->stmt));
 
-  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
   // Invalid cursor state
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000);
 }
@@ -715,6 +715,100 @@ TYPED_TEST(StatementTest, TestSQLExecDirectRowFetching) {
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000);
 }
 
+TYPED_TEST(StatementTest, TestSQLFetchScrollRowFetching) {
+  SQLLEN rows_fetched;
+  SQLSetStmtAttr(this->stmt, SQL_ATTR_ROWS_FETCHED_PTR, &rows_fetched, 0);
+
+  std::wstring wsql =
+      LR"(
+   SELECT 1 AS small_table
+   UNION ALL
+   SELECT 2
+   UNION ALL
+   SELECT 3;
+ )";
+  std::vector<SQLWCHAR> sql0(wsql.begin(), wsql.end());
+
+  ASSERT_EQ(SQL_SUCCESS,
+            SQLExecDirect(this->stmt, &sql0[0], 
static_cast<SQLINTEGER>(sql0.size())));
+
+  // Fetch row 1
+  ASSERT_EQ(SQL_SUCCESS, SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0));
+
+  SQLINTEGER val;
+  SQLLEN buf_len = sizeof(val);
+  SQLLEN ind;
+
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, 
&ind));
+  // Verify 1 is returned
+  EXPECT_EQ(1, val);
+  // Verify 1 row is fetched
+  EXPECT_EQ(1, rows_fetched);
+
+  // Fetch row 2
+  ASSERT_EQ(SQL_SUCCESS, SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0));
+
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, 
&ind));
+
+  // Verify 2 is returned
+  EXPECT_EQ(2, val);
+  // Verify 1 row is fetched in the last SQLFetchScroll call
+  EXPECT_EQ(1, rows_fetched);
+
+  // Fetch row 3
+  ASSERT_EQ(SQL_SUCCESS, SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0));
+
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, 
&ind));
+
+  // Verify 3 is returned
+  EXPECT_EQ(3, val);
+  // Verify 1 row is fetched in the last SQLFetchScroll call
+  EXPECT_EQ(1, rows_fetched);
+
+  // Verify result set has no more data beyond row 3
+  ASSERT_EQ(SQL_NO_DATA, SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0));
+
+  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, &ind));
+  // Invalid cursor state
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000);
+}
+
+TYPED_TEST(StatementTest, TestSQLFetchScrollUnsupportedOrientation) {
+  // SQL_FETCH_NEXT is the only supported fetch orientation.
+
+  std::wstring wsql = L"SELECT 1;";
+  std::vector<SQLWCHAR> sql0(wsql.begin(), wsql.end());
+
+  ASSERT_EQ(SQL_SUCCESS,
+            SQLExecDirect(this->stmt, &sql0[0], 
static_cast<SQLINTEGER>(sql0.size())));
+
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_PRIOR, 0));
+
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHYC00);
+
+  SQLLEN fetch_offset = 1;
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_RELATIVE, 
fetch_offset));
+
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHYC00);
+
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_ABSOLUTE, 
fetch_offset));
+
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHYC00);
+
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_FIRST, 0));
+
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHYC00);
+
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_LAST, 0));
+
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHYC00);
+
+  ASSERT_EQ(SQL_ERROR, SQLFetchScroll(this->stmt, SQL_FETCH_BOOKMARK, 
fetch_offset));
+
+  // DM returns state HY106 for SQL_FETCH_BOOKMARK
+  VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHY106);
+}
+
 TYPED_TEST(StatementTest, TestSQLExecDirectVarcharTruncation) {
   std::wstring wsql = L"SELECT 'VERY LONG STRING here' AS string_col;";
   std::vector<SQLWCHAR> sql0(wsql.begin(), wsql.end());
@@ -881,7 +975,7 @@ TYPED_TEST(StatementTest, 
DISABLED_TestSQLExecDirectFloatTruncation) {
   int16_t ssmall_int_val;
 
   ASSERT_EQ(SQL_SUCCESS_WITH_INFO,
-            SQLGetData(this->stmt, 1, SQL_C_SSHORT, &ssmall_int_val, 0, 0));
+            SQLGetData(this->stmt, 1, SQL_C_SSHORT, &ssmall_int_val, 0, 
nullptr));
   // Verify float truncation is reported
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01S07);
 
@@ -929,7 +1023,7 @@ TEST_F(StatementMockTest, 
TestSQLExecDirectTruncationQueryNullIndicator) {
   ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt));
 
   SQLINTEGER val;
-  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
   // Verify 1 is returned for non-truncation case.
   EXPECT_EQ(1, val);
 
@@ -938,7 +1032,7 @@ TEST_F(StatementMockTest, 
TestSQLExecDirectTruncationQueryNullIndicator) {
   SQLCHAR char_val[len];
   SQLLEN buf_len = sizeof(SQLCHAR) * len;
   ASSERT_EQ(SQL_SUCCESS_WITH_INFO,
-            SQLGetData(this->stmt, 2, SQL_C_CHAR, &char_val, buf_len, 0));
+            SQLGetData(this->stmt, 2, SQL_C_CHAR, &char_val, buf_len, 
nullptr));
   // Verify string truncation is reported
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004);
 
@@ -948,7 +1042,7 @@ TEST_F(StatementMockTest, 
TestSQLExecDirectTruncationQueryNullIndicator) {
   size_t wchar_size = GetSqlWCharSize();
   buf_len = wchar_size * len2;
   ASSERT_EQ(SQL_SUCCESS_WITH_INFO,
-            SQLGetData(this->stmt, 3, SQL_C_WCHAR, &wchar_val, buf_len, 0));
+            SQLGetData(this->stmt, 3, SQL_C_WCHAR, &wchar_val, buf_len, 
nullptr));
   // Verify string truncation is reported
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004);
 
@@ -956,7 +1050,7 @@ TEST_F(StatementMockTest, 
TestSQLExecDirectTruncationQueryNullIndicator) {
   std::vector<int8_t> varbinary_val(3);
   buf_len = varbinary_val.size();
   ASSERT_EQ(SQL_SUCCESS_WITH_INFO,
-            SQLGetData(this->stmt, 4, SQL_C_BINARY, &varbinary_val[0], 
buf_len, 0));
+            SQLGetData(this->stmt, 4, SQL_C_BINARY, &varbinary_val[0], 
buf_len, nullptr));
   // Verify binary truncation is reported
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004);
 }
@@ -975,7 +1069,7 @@ TEST_F(StatementRemoteTest, 
TestSQLExecDirectNullQueryNullIndicator) {
 
   SQLINTEGER val;
 
-  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0));
+  ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 
nullptr));
   // Verify invalid null indicator is reported, as it is required
   VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState22002);
 }

Reply via email to