diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index ec30405870f6..204bd0ffb749 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -137,6 +137,11 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLRETURN SQL_API SQLFetch(SQLHSTMT stmt) { return arrow::SQLFetch(stmt); } +SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, + SQLLEN fetchOffset) { + return arrow::SQLFetchScroll(stmt, fetchOrientation, fetchOffset); +} + SQLRETURN SQL_API SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index d5767c5b5c4f..c837b782d225 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -34,6 +34,7 @@ EXPORTS SQLExecDirectW SQLExecute SQLFetch + SQLFetchScroll SQLForeignKeysW SQLFreeEnv SQLFreeConnect diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 8a823ee20866..50c5462f227c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -983,6 +983,33 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { }); } +SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, + SQLLEN fetchOffset) { + LOG_DEBUG("SQLFetchScroll called with stmt: {}, fetchOrientation: {}, fetchOffset: {}", + stmt, fetchOrientation, fetchOffset); + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + if (fetchOrientation != SQL_FETCH_NEXT) { + throw DriverException("Optional feature not supported.", "HYC00"); + } + // fetchOffset is ignored as only SQL_FETCH_NEXT is supported + + ODBCStatement* statement = reinterpret_cast(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(ard->GetArraySize()); + if (statement->Fetch(rows)) { + return SQL_SUCCESS; + } else { + // Reached the end of rowset + return SQL_NO_DATA; + } + }); +} + SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { LOG_DEBUG( @@ -1041,7 +1068,7 @@ SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { }); } -SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { +SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { LOG_DEBUG("SQLRowCount called with stmt: {}, columnCountPtr: {}", stmt, fmt::ptr(rowCountPtr)); // TODO: write tests for SQLRowCount diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 91c45b887389..2a5b3e80a921 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -68,6 +68,7 @@ SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLengt SQLRETURN SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); SQLRETURN SQLExecute(SQLHSTMT stmt); SQLRETURN SQLFetch(SQLHSTMT stmt); +SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset); SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h index 1dd71e12f737..21e2d899ef11 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h @@ -167,6 +167,7 @@ static constexpr std::string_view error_state_HY010 = "HY010"; static constexpr std::string_view error_state_HY024 = "HY024"; static constexpr std::string_view error_state_HY092 = "HY092"; static constexpr std::string_view error_state_HYC00 = "HYC00"; +static constexpr std::string_view error_state_HY106 = "HY106"; static constexpr std::string_view error_state_HY114 = "HY114"; static constexpr std::string_view error_state_HY017 = "HY017"; static constexpr std::string_view error_state_HY118 = "HY118"; 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 5585dba5dd02..4b188b6f002a 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -832,24 +832,28 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectRowFetching) { SQLLEN ind; ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); - EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 1 is returned EXPECT_EQ(val, 1); // Fetch row 2 ret = SQLFetch(this->stmt); - ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 2 is returned EXPECT_EQ(val, 2); // Fetch row 3 ret = SQLFetch(this->stmt); - ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 3 is returned EXPECT_EQ(val, 3); @@ -857,6 +861,78 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectRowFetching) { ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, &ind); + EXPECT_EQ(ret, SQL_ERROR); + + // Invalid cursor state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_24000); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLFetchScrollRowFetching) { + this->connect(); + + SQLLEN rows_fetched; + SQLRETURN ret = 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 sql0(wsql.begin(), wsql.end()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Fetch row 1 + ret = SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + SQLLEN buf_len = sizeof(val); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 1 is returned + EXPECT_EQ(val, 1); + // Verify 1 row is fetched + EXPECT_EQ(rows_fetched, 1); + + // Fetch row 2 + ret = SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 2 is returned + EXPECT_EQ(val, 2); + // Verify 1 row is fetched in the last SQLFetchScroll call + EXPECT_EQ(rows_fetched, 1); + + // Fetch row 3 + ret = SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 3 is returned + EXPECT_EQ(val, 3); + // Verify 1 row is fetched in the last SQLFetchScroll call + EXPECT_EQ(rows_fetched, 1); + + // Verify result set has no more data beyond row 3 + ret = SQLFetchScroll(this->stmt, SQL_FETCH_NEXT, 0); + EXPECT_EQ(ret, SQL_NO_DATA); + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, &ind); EXPECT_EQ(ret, SQL_ERROR); @@ -866,6 +942,53 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectRowFetching) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLFetchScrollUnsupportedOrientation) { + // SQL_FETCH_PRIOR is the only supported fetch orientation. + this->connect(); + + std::wstring wsql = L"SELECT 1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_PRIOR, 0); + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HYC00); + + SQLLEN fetch_offset = 1; + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_RELATIVE, fetch_offset); + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HYC00); + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_ABSOLUTE, fetch_offset); + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HYC00); + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_FIRST, 0); + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HYC00); + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_LAST, 0); + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HYC00); + + ret = SQLFetchScroll(this->stmt, SQL_FETCH_BOOKMARK, fetch_offset); + EXPECT_EQ(ret, SQL_ERROR); + + // DM returns state HY106 for SQL_FETCH_BOOKMARK + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY106); + + this->disconnect(); +} + TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectVarcharTruncation) { this->connect();