From b87ce12520e875d9b5b14fe40cbc4dd269011d6f Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 15 Oct 2025 16:36:48 -0700 Subject: [PATCH 1/2] Extract SQLFetch & SQLGetData Co-Authored-By: alinalibq --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 30 +- .../odbc/odbc_impl/flight_sql_result_set.cc | 16 +- .../odbc/odbc_impl/flight_sql_result_set.h | 4 +- .../sql/odbc/odbc_impl/odbc_statement.cc | 70 +- .../sql/odbc/odbc_impl/odbc_statement.h | 4 +- .../sql/odbc/odbc_impl/spi/result_set.h | 10 +- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/statement_test.cc | 1275 +++++++++++++++++ 8 files changed, 1363 insertions(+), 47 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 01780f0efe2..80a601049a9 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -823,8 +823,24 @@ SQLRETURN SQLExecute(SQLHSTMT stmt) { SQLRETURN SQLFetch(SQLHSTMT stmt) { ARROW_LOG(DEBUG) << "SQLFetch called with stmt: " << stmt; - // GH-47713 TODO: Implement SQLFetch - return SQL_INVALID_HANDLE; + + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + 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 SQLExtendedFetch(SQLHSTMT stmt, SQLUSMALLINT fetch_orientation, @@ -876,8 +892,14 @@ SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT record_number, SQLSMALLINT c_ty << ", record_number: " << record_number << ", c_type: " << c_type << ", data_ptr: " << data_ptr << ", buffer_length: " << buffer_length << ", indicator_ptr: " << static_cast(indicator_ptr); - // GH-47713 TODO: Implement SQLGetData - return SQL_INVALID_HANDLE; + + using ODBC::ODBCStatement; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + return statement->GetData(record_number, c_type, data_ptr, buffer_length, + indicator_ptr); + }); } SQLRETURN SQLMoreResults(SQLHSTMT stmt) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.cc b/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.cc index 19149b3c48d..721f8176114 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.cc @@ -17,6 +17,8 @@ #include "arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.h" +#include + #include #include "arrow/flight/types.h" #include "arrow/scalar.h" @@ -212,14 +214,14 @@ void FlightSqlResultSet::Cancel() { current_chunk_.data = nullptr; } -bool FlightSqlResultSet::GetData(int column_n, int16_t target_type, int precision, - int scale, void* buffer, size_t buffer_length, - ssize_t* str_len_buffer) { +SQLRETURN FlightSqlResultSet::GetData(int column_n, int16_t target_type, int precision, + int scale, void* buffer, size_t buffer_length, + ssize_t* str_len_buffer) { reset_get_data_ = true; // Check if the offset is already at the end. int64_t& value_offset = get_data_offsets_[column_n - 1]; if (value_offset == -1) { - return false; + return SQL_NO_DATA; } ColumnBinding binding(util::ConvertCDataTypeFromV2ToV3(target_type), precision, scale, @@ -235,7 +237,11 @@ bool FlightSqlResultSet::GetData(int column_n, int16_t target_type, int precisio diagnostics_, nullptr); // If there was truncation, the converter would have reported it to the diagnostics. - return diagnostics_.HasWarning(); + if (diagnostics_.HasWarning()) { + return SQL_SUCCESS_WITH_INFO; + } else { + return SQL_SUCCESS; + } } std::shared_ptr FlightSqlResultSet::GetMetadata() { return metadata_; } diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.h b/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.h index 6083b332824..d08f72f2fba 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/flight_sql_result_set.h @@ -60,8 +60,8 @@ class FlightSqlResultSet : public ResultSet { void Cancel() override; - bool GetData(int column_n, int16_t target_type, int precision, int scale, void* buffer, - size_t buffer_length, ssize_t* str_len_buffer) override; + SQLRETURN GetData(int column_n, int16_t target_type, int precision, int scale, + void* buffer, size_t buffer_length, ssize_t* str_len_buffer) override; size_t Move(size_t rows, size_t bind_offset, size_t bind_type, uint16_t* row_status_array) override; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.cc index d452e77db1d..33431bc0cd0 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.cc @@ -129,6 +129,9 @@ SQLSMALLINT getc_typeForSQLType(const DescriptorRecord& record) { case SQL_WLONGVARCHAR: return SQL_C_WCHAR; + case SQL_BIT: + return SQL_C_BIT; + case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: @@ -146,13 +149,20 @@ SQLSMALLINT getc_typeForSQLType(const DescriptorRecord& record) { case SQL_BIGINT: return record.is_unsigned ? SQL_C_UBIGINT : SQL_C_SBIGINT; + case SQL_NUMERIC: + case SQL_DECIMAL: + return SQL_C_NUMERIC; + + case SQL_FLOAT: case SQL_REAL: return SQL_C_FLOAT; - case SQL_FLOAT: case SQL_DOUBLE: return SQL_C_DOUBLE; + case SQL_GUID: + return SQL_C_GUID; + case SQL_DATE: case SQL_TYPE_DATE: return SQL_C_TYPE_DATE; @@ -165,32 +175,32 @@ SQLSMALLINT getc_typeForSQLType(const DescriptorRecord& record) { case SQL_TYPE_TIMESTAMP: return SQL_C_TYPE_TIMESTAMP; - case SQL_C_INTERVAL_DAY: - return SQL_INTERVAL_DAY; - case SQL_C_INTERVAL_DAY_TO_HOUR: - return SQL_INTERVAL_DAY_TO_HOUR; - case SQL_C_INTERVAL_DAY_TO_MINUTE: - return SQL_INTERVAL_DAY_TO_MINUTE; - case SQL_C_INTERVAL_DAY_TO_SECOND: - return SQL_INTERVAL_DAY_TO_SECOND; - case SQL_C_INTERVAL_HOUR: - return SQL_INTERVAL_HOUR; - case SQL_C_INTERVAL_HOUR_TO_MINUTE: - return SQL_INTERVAL_HOUR_TO_MINUTE; - case SQL_C_INTERVAL_HOUR_TO_SECOND: - return SQL_INTERVAL_HOUR_TO_SECOND; - case SQL_C_INTERVAL_MINUTE: - return SQL_INTERVAL_MINUTE; - case SQL_C_INTERVAL_MINUTE_TO_SECOND: - return SQL_INTERVAL_MINUTE_TO_SECOND; - case SQL_C_INTERVAL_SECOND: - return SQL_INTERVAL_SECOND; - case SQL_C_INTERVAL_YEAR: - return SQL_INTERVAL_YEAR; - case SQL_C_INTERVAL_YEAR_TO_MONTH: - return SQL_INTERVAL_YEAR_TO_MONTH; - case SQL_C_INTERVAL_MONTH: - return SQL_INTERVAL_MONTH; + case SQL_INTERVAL_DAY: + return SQL_C_INTERVAL_DAY; + case SQL_INTERVAL_DAY_TO_HOUR: + return SQL_C_INTERVAL_DAY_TO_HOUR; + case SQL_INTERVAL_DAY_TO_MINUTE: + return SQL_C_INTERVAL_DAY_TO_MINUTE; + case SQL_INTERVAL_DAY_TO_SECOND: + return SQL_C_INTERVAL_DAY_TO_SECOND; + case SQL_INTERVAL_HOUR: + return SQL_C_INTERVAL_HOUR; + case SQL_INTERVAL_HOUR_TO_MINUTE: + return SQL_C_INTERVAL_HOUR_TO_MINUTE; + case SQL_INTERVAL_HOUR_TO_SECOND: + return SQL_C_INTERVAL_HOUR_TO_SECOND; + case SQL_INTERVAL_MINUTE: + return SQL_C_INTERVAL_MINUTE; + case SQL_INTERVAL_MINUTE_TO_SECOND: + return SQL_C_INTERVAL_MINUTE_TO_SECOND; + case SQL_INTERVAL_SECOND: + return SQL_C_INTERVAL_SECOND; + case SQL_INTERVAL_YEAR: + return SQL_C_INTERVAL_YEAR; + case SQL_INTERVAL_YEAR_TO_MONTH: + return SQL_C_INTERVAL_YEAR_TO_MONTH; + case SQL_INTERVAL_MONTH: + return SQL_C_INTERVAL_MONTH; default: throw DriverException("Unknown SQL type: " + std::to_string(record.concise_type), @@ -691,9 +701,9 @@ void ODBCStatement::CloseCursor(bool suppress_errors) { has_reached_end_of_result_ = false; } -bool ODBCStatement::GetData(SQLSMALLINT record_number, SQLSMALLINT c_type, - SQLPOINTER data_ptr, SQLLEN buffer_length, - SQLLEN* indicator_ptr) { +SQLRETURN ODBCStatement::GetData(SQLSMALLINT record_number, SQLSMALLINT c_type, + SQLPOINTER data_ptr, SQLLEN buffer_length, + SQLLEN* indicator_ptr) { if (record_number == 0) { throw DriverException("Bookmarks are not supported", "07009"); } else if (record_number > ird_->GetRecords().size()) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h index 8e128db1bda..31c2a1a9c65 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h @@ -77,8 +77,8 @@ class ODBCStatement : public ODBCHandle { inline SQLULEN GetRowsetSize() { return rowset_size_; } - bool GetData(SQLSMALLINT record_number, SQLSMALLINT c_type, SQLPOINTER data_ptr, - SQLLEN buffer_length, SQLLEN* indicator_ptr); + SQLRETURN GetData(SQLSMALLINT record_number, SQLSMALLINT c_type, SQLPOINTER data_ptr, + SQLLEN buffer_length, SQLLEN* indicator_ptr); /** * @brief Closes the cursor. This does _not_ un-prepare the statement or change diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h b/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h index a273d62f63d..550ca357258 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h @@ -24,6 +24,8 @@ #include "arrow/flight/sql/odbc/odbc_impl/types.h" +#include + namespace arrow::flight::sql::odbc { class ResultSetMetadata; @@ -87,10 +89,10 @@ class ResultSet { /// \param buffer Target buffer to be populated. /// \param buffer_length Target buffer length. /// \param strlen_buffer Buffer that holds the length of value being fetched. - /// \returns true if there is more data to fetch from the current cell; - /// false if the whole value was already fetched. - virtual bool GetData(int column, int16_t target_type, int precision, int scale, - void* buffer, size_t buffer_length, ssize_t* strlen_buffer) = 0; + /// \returns SQLRETURN for SQLGetData. + virtual SQLRETURN GetData(int column, int16_t target_type, int precision, int scale, + void* buffer, size_t buffer_length, + ssize_t* strlen_buffer) = 0; }; } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 4bc240637e7..cf3e15451d9 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -35,6 +35,7 @@ add_arrow_test(flight_sql_odbc_test odbc_test_suite.cc odbc_test_suite.h connection_test.cc + statement_test.cc # Enable Protobuf cleanup after test execution # GH-46889: move protobuf_test_util to a more common location ../../../../engine/substrait/protobuf_test_util.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc new file mode 100644 index 00000000000..59bdbe0d9cb --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -0,0 +1,1275 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +#include "arrow/flight/sql/odbc/tests/odbc_test_suite.h" + +#include "arrow/flight/sql/odbc/odbc_impl/platform.h" + +#include +#include +#include + +#include + +#include +#include + +namespace arrow::flight::sql::odbc { + +template +class StatementTest : public T {}; + +class StatementMockTest : public FlightSQLODBCMockTestBase {}; +class StatementRemoteTest : public FlightSQLODBCRemoteTestBase {}; +using TestTypes = ::testing::Types; +TYPED_TEST_SUITE(StatementTest, TestTypes); + +TYPED_TEST(StatementTest, TestSQLExecDirectSimpleQuery) { + std::wstring wsql = L"SELECT 1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLINTEGER val; + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0)); + // 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)); + // Invalid cursor state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectInvalidQuery) { + std::wstring wsql = L"SELECT;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_ERROR, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + // ODBC provides generic error code HY000 to all statement errors + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHY000); +} + +TYPED_TEST(StatementTest, TestSQLExecuteSimpleQuery) { + std::wstring wsql = L"SELECT 1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLPrepare(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLExecute(this->stmt)); + + // Fetch data + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLINTEGER val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0)); + + // 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)); + // Invalid cursor state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState24000); +} + +TYPED_TEST(StatementTest, TestSQLPrepareInvalidQuery) { + std::wstring wsql = L"SELECT;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_ERROR, + SQLPrepare(this->stmt, &sql0[0], static_cast(sql0.size()))); + // ODBC provides generic error code HY000 to all statement errors + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHY000); + + ASSERT_EQ(SQL_ERROR, SQLExecute(this->stmt)); + // Verify function sequence error state is returned + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHY010); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectDataQuery) { + std::wstring wsql = this->GetQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN buf_len = sizeof(stiny_int_val); + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), stiny_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), stiny_int_val); + + // Unsigned Tiny Int + uint8_t utiny_int_val; + buf_len = sizeof(utiny_int_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), utiny_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), utiny_int_val); + + // Signed Small Int + int16_t ssmall_int_val; + buf_len = sizeof(ssmall_int_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ssmall_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ssmall_int_val); + + // Unsigned Small Int + uint16_t usmall_int_val; + buf_len = sizeof(usmall_int_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 7, SQL_C_USHORT, &usmall_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), usmall_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 8, SQL_C_USHORT, &usmall_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), usmall_int_val); + + // Signed Integer + SQLINTEGER slong_val; + buf_len = sizeof(slong_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 9, SQL_C_SLONG, &slong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), slong_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 10, SQL_C_SLONG, &slong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), slong_val); + + // Unsigned Integer + SQLUINTEGER ulong_val; + buf_len = sizeof(ulong_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 11, SQL_C_ULONG, &ulong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ulong_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 12, SQL_C_ULONG, &ulong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ulong_val); + + // Signed Big Int + SQLBIGINT sbig_int_val; + buf_len = sizeof(sbig_int_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), sbig_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), sbig_int_val); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val; + buf_len = sizeof(ubig_int_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ubig_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ubig_int_val); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + buf_len = sizeof(SQL_NUMERIC_STRUCT); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 17, SQL_C_NUMERIC, &decimal_val, buf_len, &ind)); + // Check for negative decimal_val value + EXPECT_EQ(0, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + memset(&decimal_val, 0, sizeof(decimal_val)); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 18, SQL_C_NUMERIC, &decimal_val, buf_len, &ind)); + // Check for positive decimal_val value + EXPECT_EQ(1, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + // Float + float float_val; + buf_len = sizeof(float_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 19, SQL_C_FLOAT, &float_val, buf_len, &ind)); + // Get minimum negative float value + EXPECT_EQ(-std::numeric_limits::max(), float_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 20, SQL_C_FLOAT, &float_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), float_val); + + // Double + SQLDOUBLE double_val; + buf_len = sizeof(double_val); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 21, SQL_C_DOUBLE, &double_val, buf_len, &ind)); + // Get minimum negative double value + EXPECT_EQ(-std::numeric_limits::max(), double_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 22, SQL_C_DOUBLE, &double_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), double_val); + + // Bit + bool bit_val; + buf_len = sizeof(bit_val); + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 23, SQL_C_BIT, &bit_val, buf_len, &ind)); + EXPECT_EQ(false, bit_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 24, SQL_C_BIT, &bit_val, buf_len, &ind)); + EXPECT_EQ(true, bit_val); + + // Characters + + // Char + SQLCHAR char_val[2]; + buf_len = sizeof(SQLCHAR) * 2; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &ind)); + EXPECT_EQ('Z', char_val[0]); + + // WChar + SQLWCHAR wchar_val[2]; + size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); + buf_len = wchar_size * 2; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 26, SQL_C_WCHAR, &wchar_val, buf_len, &ind)); + EXPECT_EQ(L'你', wchar_val[0]); + + // WVarchar + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 27, SQL_C_WCHAR, &wvarchar_val, buf_len, &ind)); + EXPECT_EQ(L'你', wvarchar_val[0]); + EXPECT_EQ(L'好', wvarchar_val[1]); + + // varchar + SQLCHAR varchar_val[4]; + buf_len = sizeof(SQLCHAR) * 4; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 28, SQL_C_CHAR, &varchar_val, buf_len, &ind)); + EXPECT_EQ('X', varchar_val[0]); + EXPECT_EQ('Y', varchar_val[1]); + EXPECT_EQ('Z', varchar_val[2]); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + buf_len = sizeof(date_var); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 29, SQL_C_TYPE_DATE, &date_var, buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, date_var.day); + EXPECT_EQ(1, date_var.month); + EXPECT_EQ(1400, date_var.year); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 30, SQL_C_TYPE_DATE, &date_var, buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, date_var.day); + EXPECT_EQ(12, date_var.month); + EXPECT_EQ(9999, date_var.year); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + buf_len = sizeof(timestamp_var); + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, ×tamp_var, + buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, timestamp_var.day); + EXPECT_EQ(1, timestamp_var.month); + EXPECT_EQ(1400, timestamp_var.year); + EXPECT_EQ(0, timestamp_var.hour); + EXPECT_EQ(0, timestamp_var.minute); + EXPECT_EQ(0, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, ×tamp_var, + buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, timestamp_var.day); + EXPECT_EQ(12, timestamp_var.month); + EXPECT_EQ(9999, timestamp_var.year); + EXPECT_EQ(23, timestamp_var.hour); + EXPECT_EQ(59, timestamp_var.minute); + EXPECT_EQ(59, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); +} + +TEST_F(StatementRemoteTest, TestSQLExecDirectTimeQuery) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + + std::wstring wsql = + LR"( + SELECT CAST(TIME '00:00:00' AS TIME) AS time_min, + CAST(TIME '23:59:59' AS TIME) AS time_max; + )"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQL_TIME_STRUCT time_var{}; + SQLLEN buf_len = sizeof(time_var); + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_TYPE_TIME, &time_var, buf_len, &ind)); + // Check min values for time. + EXPECT_EQ(0, time_var.hour); + EXPECT_EQ(0, time_var.minute); + EXPECT_EQ(0, time_var.second); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 2, SQL_C_TYPE_TIME, &time_var, buf_len, &ind)); + // Check max values for time. + EXPECT_EQ(23, time_var.hour); + EXPECT_EQ(59, time_var.minute); + EXPECT_EQ(59, time_var.second); +} + +TEST_F(StatementMockTest, TestSQLExecDirectVarbinaryQuery) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + + std::wstring wsql = L"SELECT X'ABCDEF' AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val[0], buf_len, &ind)); + EXPECT_EQ('\xAB', varbinary_val[0]); + EXPECT_EQ('\xCD', varbinary_val[1]); + EXPECT_EQ('\xEF', varbinary_val[2]); +} + +// Tests with SQL_C_DEFAULT as the target type + +TEST_F(StatementRemoteTest, TestSQLExecDirectDataQueryDefaultType) { + // Test with default types. Only testing target types supported by server. + + std::wstring wsql = this->GetQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // Numeric Types + // Signed Integer + SQLINTEGER slong_val; + SQLLEN buf_len = sizeof(slong_val); + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 9, SQL_C_DEFAULT, &slong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), slong_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 10, SQL_C_DEFAULT, &slong_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), slong_val); + + // Signed Big Int + SQLBIGINT sbig_int_val; + buf_len = sizeof(sbig_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 13, SQL_C_DEFAULT, &sbig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), sbig_int_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 14, SQL_C_DEFAULT, &sbig_int_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), sbig_int_val); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + buf_len = sizeof(SQL_NUMERIC_STRUCT); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 17, SQL_C_DEFAULT, &decimal_val, buf_len, &ind)); + // Check for negative decimal_val value + EXPECT_EQ(0, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + memset(&decimal_val, 0, sizeof(decimal_val)); + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 18, SQL_C_DEFAULT, &decimal_val, buf_len, &ind)); + // Check for positive decimal_val value + EXPECT_EQ(1, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + // Float + float float_val; + buf_len = sizeof(float_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 19, SQL_C_DEFAULT, &float_val, buf_len, &ind)); + // Get minimum negative float value + EXPECT_EQ(-std::numeric_limits::max(), float_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 20, SQL_C_DEFAULT, &float_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), float_val); + + // Double + SQLDOUBLE double_val; + buf_len = sizeof(double_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 21, SQL_C_DEFAULT, &double_val, buf_len, &ind)); + // Get minimum negative double value + EXPECT_EQ(-std::numeric_limits::max(), double_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 22, SQL_C_DEFAULT, &double_val, buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), double_val); + + // Bit + bool bit_val; + buf_len = sizeof(bit_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 23, SQL_C_DEFAULT, &bit_val, buf_len, &ind)); + EXPECT_EQ(false, bit_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 24, SQL_C_DEFAULT, &bit_val, buf_len, &ind)); + EXPECT_EQ(true, bit_val); + + // Characters + + // Char will be fetched as wchar by default + SQLWCHAR wchar_val[2]; + size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); + buf_len = wchar_size * 2; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 25, SQL_C_DEFAULT, &wchar_val, buf_len, &ind)); + EXPECT_EQ(L'Z', wchar_val[0]); + + // WChar + SQLWCHAR wchar_val2[2]; + buf_len = wchar_size * 2; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 26, SQL_C_DEFAULT, &wchar_val2, buf_len, &ind)); + EXPECT_EQ(L'你', wchar_val2[0]); + + // WVarchar + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 27, SQL_C_DEFAULT, &wvarchar_val, buf_len, &ind)); + EXPECT_EQ(L'你', wvarchar_val[0]); + EXPECT_EQ(L'好', wvarchar_val[1]); + + // Varchar will be fetched as WVarchar by default + SQLWCHAR wvarchar_val2[4]; + buf_len = wchar_size * 4; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 28, SQL_C_DEFAULT, &wvarchar_val2, buf_len, &ind)); + EXPECT_EQ(L'X', wvarchar_val2[0]); + EXPECT_EQ(L'Y', wvarchar_val2[1]); + EXPECT_EQ(L'Z', wvarchar_val2[2]); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + buf_len = sizeof(date_var); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 29, SQL_C_DEFAULT, &date_var, buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, date_var.day); + EXPECT_EQ(1, date_var.month); + EXPECT_EQ(1400, date_var.year); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 30, SQL_C_DEFAULT, &date_var, buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, date_var.day); + EXPECT_EQ(12, date_var.month); + EXPECT_EQ(9999, date_var.year); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + buf_len = sizeof(timestamp_var); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 31, SQL_C_DEFAULT, ×tamp_var, buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, timestamp_var.day); + EXPECT_EQ(1, timestamp_var.month); + EXPECT_EQ(1400, timestamp_var.year); + EXPECT_EQ(0, timestamp_var.hour); + EXPECT_EQ(0, timestamp_var.minute); + EXPECT_EQ(0, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 32, SQL_C_DEFAULT, ×tamp_var, buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, timestamp_var.day); + EXPECT_EQ(12, timestamp_var.month); + EXPECT_EQ(9999, timestamp_var.year); + EXPECT_EQ(23, timestamp_var.hour); + EXPECT_EQ(59, timestamp_var.minute); + EXPECT_EQ(59, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); +} + +TEST_F(StatementRemoteTest, TestSQLExecDirectTimeQueryDefaultType) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + + std::wstring wsql = + LR"( + SELECT CAST(TIME '00:00:00' AS TIME) AS time_min, + CAST(TIME '23:59:59' AS TIME) AS time_max; + )"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQL_TIME_STRUCT time_var{}; + SQLLEN buf_len = sizeof(time_var); + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_DEFAULT, &time_var, buf_len, &ind)); + // Check min values for time. + EXPECT_EQ(0, time_var.hour); + EXPECT_EQ(0, time_var.minute); + EXPECT_EQ(0, time_var.second); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 2, SQL_C_DEFAULT, &time_var, buf_len, &ind)); + // Check max values for time. + EXPECT_EQ(23, time_var.hour); + EXPECT_EQ(59, time_var.minute); + EXPECT_EQ(59, time_var.second); +} + +TEST_F(StatementRemoteTest, TestSQLExecDirectVarbinaryQueryDefaultType) { + // Limitation on mock test server prevents SQL_C_DEFAULT from working properly. + // Mock server has type `DENSE_UNION` for varbinary. + // Note that not all remote servers support "from_hex" function + + std::wstring wsql = L"SELECT from_hex('ABCDEF') AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_DEFAULT, &varbinary_val[0], buf_len, &ind)); + EXPECT_EQ('\xAB', varbinary_val[0]); + EXPECT_EQ('\xCD', varbinary_val[1]); + EXPECT_EQ('\xEF', varbinary_val[2]); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectGuidQueryUnsupported) { + // Query GUID as string as SQLite does not support GUID + std::wstring wsql = L"SELECT 'C77313CF-4E08-47CE-B6DF-94DD2FCF3541' AS guid;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLGUID guid_var; + SQLLEN buf_len = sizeof(guid_var); + SQLLEN ind; + ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_GUID, &guid_var, buf_len, &ind)); + // GUID is not supported by ODBC + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorStateHY000); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectRowFetching) { + std::wstring wsql = + LR"( + SELECT 1 AS small_table + UNION ALL + SELECT 2 + UNION ALL + SELECT 3; + )"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + // Fetch row 1 + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + 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); + + // Fetch row 2 + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind)); + + // Verify 2 is returned + EXPECT_EQ(2, val); + + // Fetch row 3 + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind)); + + // Verify 3 is returned + EXPECT_EQ(3, val); + + // Verify result set has no more data beyond row 3 + ASSERT_EQ(SQL_NO_DATA, SQLFetch(this->stmt)); + + 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, 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 sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(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_PRIOR is the only supported fetch orientation. + + std::wstring wsql = L"SELECT 1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(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 sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + const int len = 17; + SQLCHAR char_val[len]; + SQLLEN buf_len = sizeof(SQLCHAR) * len; + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val, buf_len, &ind)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + EXPECT_EQ(std::string("VERY LONG STRING"), ODBC::SqlStringToString(char_val)); + EXPECT_EQ(21, ind); + + // Fetch same column 2nd time + const int len2 = 2; + SQLCHAR char_val2[len2]; + buf_len = sizeof(SQLCHAR) * len2; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val2, buf_len, &ind)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + EXPECT_EQ(std::string(" "), ODBC::SqlStringToString(char_val2)); + EXPECT_EQ(5, ind); + + // Fetch same column 3rd time + const int len3 = 5; + SQLCHAR char_val3[len3]; + buf_len = sizeof(SQLCHAR) * len3; + + // Verify that there is no more truncation reports. The full string has been fetched. + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val3, buf_len, &ind)); + + EXPECT_EQ(std::string("here"), ODBC::SqlStringToString(char_val3)); + EXPECT_EQ(4, ind); + + // Attempt to fetch data 4th time + SQLCHAR char_val4[len]; + // Verify SQL_NO_DATA is returned + ASSERT_EQ(SQL_NO_DATA, SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val4, 0, &ind)); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectWVarcharTruncation) { + std::wstring wsql = L"SELECT 'VERY LONG Unicode STRING 句子 here' AS wstring_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + const int len = 28; + SQLWCHAR wchar_val[len]; + size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); + SQLLEN buf_len = wchar_size * len; + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val, buf_len, &ind)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + EXPECT_EQ(std::wstring(L"VERY LONG Unicode STRING 句子"), std::wstring(wchar_val)); + EXPECT_EQ(32 * wchar_size, ind); + + // Fetch same column 2nd time + const int len2 = 2; + SQLWCHAR wchar_val2[len2]; + buf_len = wchar_size * len2; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val2, buf_len, &ind)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + EXPECT_EQ(std::wstring(L" "), std::wstring(wchar_val2)); + EXPECT_EQ(5 * wchar_size, ind); + + // Fetch same column 3rd time + const int len3 = 5; + SQLWCHAR wchar_val3[len3]; + buf_len = wchar_size * len3; + + // Verify that there is no more truncation reports. The full string has been fetched. + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val3, buf_len, &ind)); + + EXPECT_EQ(std::wstring(L"here"), std::wstring(wchar_val3)); + EXPECT_EQ(4 * wchar_size, ind); + + // Attempt to fetch data 4th time + SQLWCHAR wchar_val4[len]; + // Verify SQL_NO_DATA is returned + ASSERT_EQ(SQL_NO_DATA, SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val4, 0, &ind)); +} + +TEST_F(StatementMockTest, TestSQLExecDirectVarbinaryTruncation) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + + std::wstring wsql = L"SELECT X'ABCDEFAB' AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val[0], buf_len, &ind)); + // Verify binary truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + EXPECT_EQ('\xAB', varbinary_val[0]); + EXPECT_EQ('\xCD', varbinary_val[1]); + EXPECT_EQ('\xEF', varbinary_val[2]); + EXPECT_EQ(4, ind); + + // Fetch same column 2nd time + std::vector varbinary_val2(1); + buf_len = varbinary_val2.size(); + + // Verify that there is no more truncation reports. The full binary has been fetched. + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val2[0], buf_len, &ind)); + + EXPECT_EQ('\xAB', varbinary_val[0]); + EXPECT_EQ(1, ind); + + // Attempt to fetch data 3rd time + std::vector varbinary_val3(1); + buf_len = varbinary_val3.size(); + // Verify SQL_NO_DATA is returned + ASSERT_EQ(SQL_NO_DATA, + SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val3[0], buf_len, &ind)); +} + +TYPED_TEST(StatementTest, DISABLED_TestSQLExecDirectFloatTruncation) { + // Test is disabled until float truncation is supported. + // GH-46985: return warning message instead of error on float truncation case + std::wstring wsql; + if constexpr (std::is_same_v) { + wsql = std::wstring(L"SELECT CAST(1.234 AS REAL) AS float_val"); + } else if constexpr (std::is_same_v) { + wsql = std::wstring(L"SELECT CAST(1.234 AS FLOAT) AS float_val"); + } + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + int16_t ssmall_int_val; + + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 1, SQL_C_SSHORT, &ssmall_int_val, 0, 0)); + // Verify float truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01S07); + + EXPECT_EQ(1, ssmall_int_val); +} + +TEST_F(StatementRemoteTest, TestSQLExecDirectNullQuery) { + // Limitation on mock test server prevents null from working properly, so use remote + // server instead. Mock server has type `DENSE_UNION` for null column data. + + std::wstring wsql = L"SELECT null as null_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLINTEGER val; + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, &ind)); + + // Verify SQL_NULL_DATA is returned for indicator + EXPECT_EQ(SQL_NULL_DATA, ind); +} + +TEST_F(StatementMockTest, TestSQLExecDirectTruncationQueryNullIndicator) { + // Driver should not error out when indicator is null if the cell is non-null + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + + std::wstring wsql = + LR"( + SELECT 1, + 'VERY LONG STRING here' AS string_col, + 'VERY LONG Unicode STRING 句子 here' AS wstring_col, + X'ABCDEFAB' AS c_varbinary; + )"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLINTEGER val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0)); + // Verify 1 is returned for non-truncation case. + EXPECT_EQ(1, val); + + // Char + const int len = 17; + 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)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + // WChar + const int len2 = 28; + SQLWCHAR wchar_val[len2]; + size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); + buf_len = wchar_size * len2; + ASSERT_EQ(SQL_SUCCESS_WITH_INFO, + SQLGetData(this->stmt, 3, SQL_C_WCHAR, &wchar_val, buf_len, 0)); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); + + // varbinary + std::vector 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)); + // Verify binary truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState01004); +} + +TEST_F(StatementRemoteTest, TestSQLExecDirectNullQueryNullIndicator) { + // Limitation on mock test server prevents null from working properly, so use remote + // server instead. Mock server has type `DENSE_UNION` for null column data. + + std::wstring wsql = L"SELECT null as null_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + SQLINTEGER val; + + ASSERT_EQ(SQL_ERROR, SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0)); + // Verify invalid null indicator is reported, as it is required + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState22002); +} + +TYPED_TEST(StatementTest, TestSQLExecDirectIgnoreInvalidBufLen) { + // Verify the driver ignores invalid buffer length for fixed data types + + std::wstring wsql = this->GetQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ASSERT_EQ(SQL_SUCCESS, + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size()))); + + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN invalid_buf_len = -1; + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), stiny_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), stiny_int_val); + + // Unsigned Tiny Int + uint8_t utiny_int_val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), utiny_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), utiny_int_val); + + // Signed Small Int + int16_t ssmall_int_val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ssmall_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ssmall_int_val); + + // Unsigned Small Int + uint16_t usmall_int_val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 7, SQL_C_USHORT, &usmall_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), usmall_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 8, SQL_C_USHORT, &usmall_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), usmall_int_val); + + // Signed Integer + SQLINTEGER slong_val; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 9, SQL_C_SLONG, &slong_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), slong_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 10, SQL_C_SLONG, &slong_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), slong_val); + + // Unsigned Integer + SQLUINTEGER ulong_val; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 11, SQL_C_ULONG, &ulong_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ulong_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 12, SQL_C_ULONG, &ulong_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ulong_val); + + // Signed Big Int + SQLBIGINT sbig_int_val; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), sbig_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), sbig_int_val); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val; + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::min(), ubig_int_val); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val, + invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), ubig_int_val); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 17, SQL_C_NUMERIC, &decimal_val, + invalid_buf_len, &ind)); + // Check for negative decimal_val value + EXPECT_EQ(0, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + memset(&decimal_val, 0, sizeof(decimal_val)); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 18, SQL_C_NUMERIC, &decimal_val, + invalid_buf_len, &ind)); + // Check for positive decimal_val value + EXPECT_EQ(1, decimal_val.sign); + EXPECT_EQ(0, decimal_val.scale); + EXPECT_EQ(38, decimal_val.precision); + EXPECT_THAT(decimal_val.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0)); + + // Float + float float_val; + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 19, SQL_C_FLOAT, &float_val, invalid_buf_len, &ind)); + // Get minimum negative float value + EXPECT_EQ(-std::numeric_limits::max(), float_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 20, SQL_C_FLOAT, &float_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), float_val); + + // Double + SQLDOUBLE double_val; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 21, SQL_C_DOUBLE, &double_val, invalid_buf_len, &ind)); + // Get minimum negative double value + EXPECT_EQ(-std::numeric_limits::max(), double_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 22, SQL_C_DOUBLE, &double_val, invalid_buf_len, &ind)); + EXPECT_EQ(std::numeric_limits::max(), double_val); + + // Bit + bool bit_val; + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 23, SQL_C_BIT, &bit_val, invalid_buf_len, &ind)); + EXPECT_EQ(false, bit_val); + + ASSERT_EQ(SQL_SUCCESS, + SQLGetData(this->stmt, 24, SQL_C_BIT, &bit_val, invalid_buf_len, &ind)); + EXPECT_EQ(true, bit_val); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 29, SQL_C_TYPE_DATE, &date_var, + invalid_buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, date_var.day); + EXPECT_EQ(1, date_var.month); + EXPECT_EQ(1400, date_var.year); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 30, SQL_C_TYPE_DATE, &date_var, + invalid_buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, date_var.day); + EXPECT_EQ(12, date_var.month); + EXPECT_EQ(9999, date_var.year); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, ×tamp_var, + invalid_buf_len, &ind)); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(1, timestamp_var.day); + EXPECT_EQ(1, timestamp_var.month); + EXPECT_EQ(1400, timestamp_var.year); + EXPECT_EQ(0, timestamp_var.hour); + EXPECT_EQ(0, timestamp_var.minute); + EXPECT_EQ(0, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); + + ASSERT_EQ(SQL_SUCCESS, SQLGetData(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, ×tamp_var, + invalid_buf_len, &ind)); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(31, timestamp_var.day); + EXPECT_EQ(12, timestamp_var.month); + EXPECT_EQ(9999, timestamp_var.year); + EXPECT_EQ(23, timestamp_var.hour); + EXPECT_EQ(59, timestamp_var.minute); + EXPECT_EQ(59, timestamp_var.second); + EXPECT_EQ(0, timestamp_var.fraction); +} + +} // namespace arrow::flight::sql::odbc From a5ad7061a1cf74b8f0a5e6c8d9ae151a59eb773b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 10 Nov 2025 11:22:47 -0800 Subject: [PATCH 2/2] Fix doxygen Use `\return` not `\returns` --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 2 +- cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h | 4 +--- cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 80a601049a9..08a4b80a659 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -830,7 +830,7 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { ODBCStatement* statement = reinterpret_cast(stmt); // The SQL_ATTR_ROW_ARRAY_SIZE statement attribute specifies the number of rows in the - // rowset. + // rowset. Retrieve it with GetArraySize. ODBCDescriptor* ard = statement->GetARD(); size_t rows = static_cast(ard->GetArraySize()); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h index 31c2a1a9c65..a58e195a1b6 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_statement.h @@ -58,9 +58,7 @@ class ODBCStatement : public ODBCHandle { void ExecutePrepared(); void ExecuteDirect(const std::string& query); - /** - * @brief Returns true if the number of rows fetch was greater than zero. - */ + /// \brief Return true if the number of rows fetch was greater than zero. bool Fetch(size_t rows); bool IsPrepared() const; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h b/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h index 550ca357258..120e6132f19 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/spi/result_set.h @@ -89,7 +89,7 @@ class ResultSet { /// \param buffer Target buffer to be populated. /// \param buffer_length Target buffer length. /// \param strlen_buffer Buffer that holds the length of value being fetched. - /// \returns SQLRETURN for SQLGetData. + /// \return SQLRETURN for SQLGetData. virtual SQLRETURN GetData(int column, int16_t target_type, int precision, int scale, void* buffer, size_t buffer_length, ssize_t* strlen_buffer) = 0;