From 744d868dabdce370950004224e9ab645b7ba143b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 16 Oct 2025 13:43:09 -0700 Subject: [PATCH 1/2] Extract SQLBindCol implementation Co-Authored-By: alinalibq --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 12 +- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/statement_test.cc | 584 ++++++++++++++++++ 3 files changed, 595 insertions(+), 2 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..cd47bbc1b09 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -857,8 +857,16 @@ SQLRETURN SQLBindCol(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-47716 TODO: Implement SQLBindCol - return SQL_INVALID_HANDLE; + + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + // GH-47021: implement driver to return indicator value when data pointer is null + ODBCStatement* statement = reinterpret_cast(stmt); + ODBCDescriptor* ard = statement->GetARD(); + ard->BindCol(record_number, c_type, data_ptr, buffer_length, indicator_ptr); + return SQL_SUCCESS; + }); } SQLRETURN SQLCloseCursor(SQLHSTMT stmt) { 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..776b6fcf14f --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -0,0 +1,584 @@ +// 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, TestSQLBindColDataQuery) { + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val_min; + int8_t stiny_int_val_max; + SQLLEN buf_len = 0; + SQLLEN ind; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val_max, buf_len, &ind)); + + // Unsigned Tiny Int + uint8_t utiny_int_val_min; + uint8_t utiny_int_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val_max, buf_len, &ind)); + + // Signed Small Int + int16_t ssmall_int_val_min; + int16_t ssmall_int_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val_max, buf_len, &ind)); + + // Unsigned Small Int + uint16_t usmall_int_val_min; + uint16_t usmall_int_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 7, SQL_C_USHORT, &usmall_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 8, SQL_C_USHORT, &usmall_int_val_max, buf_len, &ind)); + + // Signed Integer + SQLINTEGER slong_val_min; + SQLINTEGER slong_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 9, SQL_C_SLONG, &slong_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 10, SQL_C_SLONG, &slong_val_max, buf_len, &ind)); + + // Unsigned Integer + SQLUINTEGER ulong_val_min; + SQLUINTEGER ulong_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 11, SQL_C_ULONG, &ulong_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 12, SQL_C_ULONG, &ulong_val_max, buf_len, &ind)); + + // Signed Big Int + SQLBIGINT sbig_int_val_min; + SQLBIGINT sbig_int_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val_max, buf_len, &ind)); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val_min; + SQLUBIGINT ubig_int_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val_max, buf_len, &ind)); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val_neg; + SQL_NUMERIC_STRUCT decimal_val_pos; + memset(&decimal_val_neg, 0, sizeof(decimal_val_neg)); + memset(&decimal_val_pos, 0, sizeof(decimal_val_pos)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 17, SQL_C_NUMERIC, &decimal_val_neg, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 18, SQL_C_NUMERIC, &decimal_val_pos, buf_len, &ind)); + + // Float + float float_val_min; + float float_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 19, SQL_C_FLOAT, &float_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 20, SQL_C_FLOAT, &float_val_max, buf_len, &ind)); + + // Double + SQLDOUBLE double_val_min; + SQLDOUBLE double_val_max; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 21, SQL_C_DOUBLE, &double_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 22, SQL_C_DOUBLE, &double_val_max, buf_len, &ind)); + + // Bit + bool bit_val_false; + bool bit_val_true; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 23, SQL_C_BIT, &bit_val_false, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 24, SQL_C_BIT, &bit_val_true, buf_len, &ind)); + + // Characters + SQLCHAR char_val[2]; + buf_len = sizeof(SQLCHAR) * 2; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &ind)); + + SQLWCHAR wchar_val[2]; + size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); + buf_len = wchar_size * 2; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 26, SQL_C_WCHAR, &wchar_val, buf_len, &ind)); + + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 27, SQL_C_WCHAR, &wvarchar_val, buf_len, &ind)); + + SQLCHAR varchar_val[4]; + buf_len = sizeof(SQLCHAR) * 4; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 28, SQL_C_CHAR, &varchar_val, buf_len, &ind)); + + // Date and Timestamp + SQL_DATE_STRUCT date_val_min{}, date_val_max{}; + buf_len = 0; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 29, SQL_C_TYPE_DATE, &date_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 30, SQL_C_TYPE_DATE, &date_val_max, buf_len, &ind)); + + SQL_TIMESTAMP_STRUCT timestamp_val_min{}, timestamp_val_max{}; + + EXPECT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, + ×tamp_val_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, + ×tamp_val_max, buf_len, &ind)); + + // Execute query and fetch data once since there is only 1 row. + 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)); + + // Data verification + + // Signed Tiny Int + EXPECT_EQ(std::numeric_limits::min(), stiny_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), stiny_int_val_max); + + // Unsigned Tiny Int + EXPECT_EQ(std::numeric_limits::min(), utiny_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), utiny_int_val_max); + + // Signed Small Int + EXPECT_EQ(std::numeric_limits::min(), ssmall_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), ssmall_int_val_max); + + // Unsigned Small Int + EXPECT_EQ(std::numeric_limits::min(), usmall_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), usmall_int_val_max); + + // Signed Long + EXPECT_EQ(std::numeric_limits::min(), slong_val_min); + EXPECT_EQ(std::numeric_limits::max(), slong_val_max); + + // Unsigned Long + EXPECT_EQ(std::numeric_limits::min(), ulong_val_min); + EXPECT_EQ(std::numeric_limits::max(), ulong_val_max); + + // Signed Big Int + EXPECT_EQ(std::numeric_limits::min(), sbig_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), sbig_int_val_max); + + // Unsigned Big Int + EXPECT_EQ(std::numeric_limits::min(), ubig_int_val_min); + EXPECT_EQ(std::numeric_limits::max(), ubig_int_val_max); + + // Decimal + EXPECT_EQ(0, decimal_val_neg.sign); + EXPECT_EQ(0, decimal_val_neg.scale); + EXPECT_EQ(38, decimal_val_neg.precision); + EXPECT_THAT(decimal_val_neg.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + EXPECT_EQ(1, decimal_val_pos.sign); + EXPECT_EQ(0, decimal_val_pos.scale); + EXPECT_EQ(38, decimal_val_pos.precision); + EXPECT_THAT(decimal_val_pos.val, ::testing::ElementsAre(0xFF, 0xC9, 0x9A, 0x3B, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + // Float + EXPECT_EQ(-std::numeric_limits::max(), float_val_min); + EXPECT_EQ(std::numeric_limits::max(), float_val_max); + + // Double + EXPECT_EQ(-std::numeric_limits::max(), double_val_min); + EXPECT_EQ(std::numeric_limits::max(), double_val_max); + + // Bit + EXPECT_EQ(false, bit_val_false); + EXPECT_EQ(true, bit_val_true); + + // Characters + EXPECT_EQ('Z', char_val[0]); + EXPECT_EQ(L'你', wchar_val[0]); + EXPECT_EQ(L'你', wvarchar_val[0]); + EXPECT_EQ(L'好', wvarchar_val[1]); + + EXPECT_EQ('X', varchar_val[0]); + EXPECT_EQ('Y', varchar_val[1]); + EXPECT_EQ('Z', varchar_val[2]); + + // Date + EXPECT_EQ(1, date_val_min.day); + EXPECT_EQ(1, date_val_min.month); + EXPECT_EQ(1400, date_val_min.year); + + EXPECT_EQ(31, date_val_max.day); + EXPECT_EQ(12, date_val_max.month); + EXPECT_EQ(9999, date_val_max.year); + + // Timestamp + EXPECT_EQ(1, timestamp_val_min.day); + EXPECT_EQ(1, timestamp_val_min.month); + EXPECT_EQ(1400, timestamp_val_min.year); + EXPECT_EQ(0, timestamp_val_min.hour); + EXPECT_EQ(0, timestamp_val_min.minute); + EXPECT_EQ(0, timestamp_val_min.second); + EXPECT_EQ(0, timestamp_val_min.fraction); + + EXPECT_EQ(31, timestamp_val_max.day); + EXPECT_EQ(12, timestamp_val_max.month); + EXPECT_EQ(9999, timestamp_val_max.year); + EXPECT_EQ(23, timestamp_val_max.hour); + EXPECT_EQ(59, timestamp_val_max.minute); + EXPECT_EQ(59, timestamp_val_max.second); + EXPECT_EQ(0, timestamp_val_max.fraction); +} + +TEST_F(StatementRemoteTest, TestSQLBindColTimeQuery) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + + SQL_TIME_STRUCT time_var_min{}; + SQL_TIME_STRUCT time_var_max{}; + SQLLEN buf_len = sizeof(time_var_min); + SQLLEN ind; + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 1, SQL_C_TYPE_TIME, &time_var_min, buf_len, &ind)); + + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 2, SQL_C_TYPE_TIME, &time_var_max, buf_len, &ind)); + + 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)); + + // Check min values for time. + EXPECT_EQ(0, time_var_min.hour); + EXPECT_EQ(0, time_var_min.minute); + EXPECT_EQ(0, time_var_min.second); + + // Check max values for time. + EXPECT_EQ(23, time_var_max.hour); + EXPECT_EQ(59, time_var_max.minute); + EXPECT_EQ(59, time_var_max.second); +} + +TEST_F(StatementMockTest, TestSQLBindColVarbinaryQuery) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ASSERT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 1, SQL_C_BINARY, &varbinary_val[0], buf_len, &ind)); + + 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)); + + // Check varbinary values + EXPECT_EQ('\xAB', varbinary_val[0]); + EXPECT_EQ('\xCD', varbinary_val[1]); + EXPECT_EQ('\xEF', varbinary_val[2]); +} + +TEST_F(StatementRemoteTest, TestSQLBindColNullQuery) { + // 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. + + SQLINTEGER val; + SQLLEN ind; + + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_LONG, &val, 0, &ind)); + + 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)); + + // Verify SQL_NULL_DATA is returned for indicator + EXPECT_EQ(SQL_NULL_DATA, ind); +} + +TEST_F(StatementRemoteTest, TestSQLBindColNullQueryNullIndicator) { + // 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. + + SQLINTEGER val; + + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_LONG, &val, 0, 0)); + + 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_ERROR, SQLFetch(this->stmt)); + // Verify invalid null indicator is reported, as it is required + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, kErrorState22002); +} + +TYPED_TEST(StatementTest, TestSQLBindColRowFetching) { + SQLINTEGER val; + SQLLEN buf_len = sizeof(val); + SQLLEN ind; + + // Same variable will be used for column 1, the value of `val` + // should be updated after every SQLFetch call. + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_LONG, &val, buf_len, &ind)); + + 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)); + + // Verify 1 is returned + EXPECT_EQ(1, val); + + // Fetch row 2 + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // Verify 2 is returned + EXPECT_EQ(2, val); + + // Fetch row 3 + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // 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)); +} + +TYPED_TEST(StatementTest, TestSQLBindColRowArraySize) { + // Set SQL_ATTR_ROW_ARRAY_SIZE to fetch 3 rows at once + + constexpr SQLULEN rows = 3; + SQLINTEGER val[rows]; + SQLLEN buf_len = sizeof(val); + SQLLEN ind[rows]; + + // Same variable will be used for column 1, the value of `val` + // should be updated after every SQLFetch call. + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_LONG, val, buf_len, ind)); + + SQLLEN rows_fetched; + ASSERT_EQ(SQL_SUCCESS, + 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()))); + + ASSERT_EQ(SQL_SUCCESS, SQLSetStmtAttr(this->stmt, SQL_ATTR_ROW_ARRAY_SIZE, + reinterpret_cast(rows), 0)); + + // Fetch 3 rows at once + ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); + + // Verify 3 rows are fetched + EXPECT_EQ(3, rows_fetched); + + // Verify 1 is returned + EXPECT_EQ(1, val[0]); + // Verify 2 is returned + EXPECT_EQ(2, val[1]); + // Verify 3 is returned + EXPECT_EQ(3, val[2]); + + // Verify result set has no more data beyond row 3 + ASSERT_EQ(SQL_NO_DATA, SQLFetch(this->stmt)); +} + +TYPED_TEST(StatementTest, DISABLED_TestSQLBindColIndicatorOnly) { + // GH-47021: implement driver to return indicator value when data pointer is null + + // Verify driver supports null data pointer with valid indicator pointer + + // Numeric Types + + // Signed Tiny Int + SQLLEN stiny_int_ind; + EXPECT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_STINYINT, 0, 0, &stiny_int_ind)); + + // Characters + SQLLEN buf_len = sizeof(SQLCHAR) * 2; + SQLLEN char_val_ind; + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 25, SQL_C_CHAR, 0, buf_len, &char_val_ind)); + + // Execute query and fetch data once since there is only 1 row. + 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)); + + // Verify values for indicator pointer + // Signed Tiny Int + EXPECT_EQ(1, stiny_int_ind); + + // Char array + EXPECT_EQ(1, char_val_ind); +} + +TYPED_TEST(StatementTest, TestSQLBindColIndicatorOnlySQLUnbind) { + // Verify driver supports valid indicator pointer after unbinding all columns + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN stiny_int_ind; + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, 0, &stiny_int_ind)); + + // Characters + SQLCHAR char_val[2]; + SQLLEN buf_len = sizeof(SQLCHAR) * 2; + SQLLEN char_val_ind; + EXPECT_EQ(SQL_SUCCESS, + SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &char_val_ind)); + + // Driver should still be able to execute queries after unbinding columns + EXPECT_EQ(SQL_SUCCESS, SQLFreeStmt(this->stmt, SQL_UNBIND)); + + // Execute query and fetch data once since there is only 1 row. + 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)); + + // GH-47021: implement driver to return indicator value when data pointer is null and + // uncomment the checks Verify values for indicator pointer Signed Tiny Int + // EXPECT_EQ(1, stiny_int_ind); + + // Char array + // EXPECT_EQ(1, char_val_ind); +} + +} // namespace arrow::flight::sql::odbc From 87e634a276c2954addebd680ed036eb4aa9327b8 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 10 Nov 2025 14:34:40 -0800 Subject: [PATCH 2/2] Use `ASSERT` instead of `EXPECT` gtest checks --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 2 +- .../flight/sql/odbc/tests/statement_test.cc | 76 +++++++++---------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index cd47bbc1b09..923193d6171 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -861,7 +861,7 @@ SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT record_number, SQLSMALLINT c_ty using ODBC::ODBCDescriptor; using ODBC::ODBCStatement; return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { - // GH-47021: implement driver to return indicator value when data pointer is null + // GH-47021 TODO: implement driver to return indicator value when data pointer is null ODBCStatement* statement = reinterpret_cast(stmt); ODBCDescriptor* ard = statement->GetARD(); ard->BindCol(record_number, c_type, data_ptr, buffer_length, indicator_ptr); 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 776b6fcf14f..0872ff71883 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -46,80 +46,80 @@ TYPED_TEST(StatementTest, TestSQLBindColDataQuery) { SQLLEN buf_len = 0; SQLLEN ind; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val_max, buf_len, &ind)); // Unsigned Tiny Int uint8_t utiny_int_val_min; uint8_t utiny_int_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val_max, buf_len, &ind)); // Signed Small Int int16_t ssmall_int_val_min; int16_t ssmall_int_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val_max, buf_len, &ind)); // Unsigned Small Int uint16_t usmall_int_val_min; uint16_t usmall_int_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 7, SQL_C_USHORT, &usmall_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 8, SQL_C_USHORT, &usmall_int_val_max, buf_len, &ind)); // Signed Integer SQLINTEGER slong_val_min; SQLINTEGER slong_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 9, SQL_C_SLONG, &slong_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 10, SQL_C_SLONG, &slong_val_max, buf_len, &ind)); // Unsigned Integer SQLUINTEGER ulong_val_min; SQLUINTEGER ulong_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 11, SQL_C_ULONG, &ulong_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 12, SQL_C_ULONG, &ulong_val_max, buf_len, &ind)); // Signed Big Int SQLBIGINT sbig_int_val_min; SQLBIGINT sbig_int_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val_max, buf_len, &ind)); // Unsigned Big Int SQLUBIGINT ubig_int_val_min; SQLUBIGINT ubig_int_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val_max, buf_len, &ind)); // Decimal @@ -128,76 +128,76 @@ TYPED_TEST(StatementTest, TestSQLBindColDataQuery) { memset(&decimal_val_neg, 0, sizeof(decimal_val_neg)); memset(&decimal_val_pos, 0, sizeof(decimal_val_pos)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 17, SQL_C_NUMERIC, &decimal_val_neg, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 18, SQL_C_NUMERIC, &decimal_val_pos, buf_len, &ind)); // Float float float_val_min; float float_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 19, SQL_C_FLOAT, &float_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 20, SQL_C_FLOAT, &float_val_max, buf_len, &ind)); // Double SQLDOUBLE double_val_min; SQLDOUBLE double_val_max; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 21, SQL_C_DOUBLE, &double_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 22, SQL_C_DOUBLE, &double_val_max, buf_len, &ind)); // Bit bool bit_val_false; bool bit_val_true; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 23, SQL_C_BIT, &bit_val_false, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 24, SQL_C_BIT, &bit_val_true, buf_len, &ind)); // Characters SQLCHAR char_val[2]; buf_len = sizeof(SQLCHAR) * 2; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &ind)); SQLWCHAR wchar_val[2]; size_t wchar_size = arrow::flight::sql::odbc::GetSqlWCharSize(); buf_len = wchar_size * 2; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 26, SQL_C_WCHAR, &wchar_val, buf_len, &ind)); SQLWCHAR wvarchar_val[3]; buf_len = wchar_size * 3; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 27, SQL_C_WCHAR, &wvarchar_val, buf_len, &ind)); SQLCHAR varchar_val[4]; buf_len = sizeof(SQLCHAR) * 4; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 28, SQL_C_CHAR, &varchar_val, buf_len, &ind)); // Date and Timestamp SQL_DATE_STRUCT date_val_min{}, date_val_max{}; buf_len = 0; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 29, SQL_C_TYPE_DATE, &date_val_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 30, SQL_C_TYPE_DATE, &date_val_max, buf_len, &ind)); SQL_TIMESTAMP_STRUCT timestamp_val_min{}, timestamp_val_max{}; @@ -322,10 +322,10 @@ TEST_F(StatementRemoteTest, TestSQLBindColTimeQuery) { SQLLEN buf_len = sizeof(time_var_min); SQLLEN ind; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_TYPE_TIME, &time_var_min, buf_len, &ind)); - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 2, SQL_C_TYPE_TIME, &time_var_max, buf_len, &ind)); std::wstring wsql = @@ -510,7 +510,7 @@ TYPED_TEST(StatementTest, TestSQLBindColRowArraySize) { } TYPED_TEST(StatementTest, DISABLED_TestSQLBindColIndicatorOnly) { - // GH-47021: implement driver to return indicator value when data pointer is null + // GH-47021 TODO: implement driver to return indicator value when data pointer is null // Verify driver supports null data pointer with valid indicator pointer @@ -523,7 +523,7 @@ TYPED_TEST(StatementTest, DISABLED_TestSQLBindColIndicatorOnly) { // Characters SQLLEN buf_len = sizeof(SQLCHAR) * 2; SQLLEN char_val_ind; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 25, SQL_C_CHAR, 0, buf_len, &char_val_ind)); // Execute query and fetch data once since there is only 1 row. @@ -551,14 +551,14 @@ TYPED_TEST(StatementTest, TestSQLBindColIndicatorOnlySQLUnbind) { // Signed Tiny Int int8_t stiny_int_val; SQLLEN stiny_int_ind; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, 0, &stiny_int_ind)); // Characters SQLCHAR char_val[2]; SQLLEN buf_len = sizeof(SQLCHAR) * 2; SQLLEN char_val_ind; - EXPECT_EQ(SQL_SUCCESS, + ASSERT_EQ(SQL_SUCCESS, SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &char_val_ind)); // Driver should still be able to execute queries after unbinding columns @@ -573,8 +573,8 @@ TYPED_TEST(StatementTest, TestSQLBindColIndicatorOnlySQLUnbind) { ASSERT_EQ(SQL_SUCCESS, SQLFetch(this->stmt)); - // GH-47021: implement driver to return indicator value when data pointer is null and - // uncomment the checks Verify values for indicator pointer Signed Tiny Int + // GH-47021 TODO: implement driver to return indicator value when data pointer is null + // and uncomment the checks Verify values for indicator pointer Signed Tiny Int // EXPECT_EQ(1, stiny_int_ind); // Char array