From 7155be7ad77f4db220a84d629a99775edd7554fd Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 2 Apr 2024 07:54:07 +0200 Subject: [PATCH 01/74] Add Arrow Flight SQL ODBC driver Co-authored-by: rscales Add initial framework for odbc dll - Add ARROW_FLIGHT_SQL_ODBC option. If we set `ARROW_FLIGHT_SQL_ODBC=ON`, the flightsql odbc folder will be built - Add odbc api layer for entry_point.cc - builds odbc dll file, with ODBC APIs exported in odbc.def Address James' comments Fix `odbcabstraction` build errors and partially fix `flightsql_odbc` errors Fix boost-variant not found error - Adding dependencies from odbc/vcpkg.json to cpp/vcpkg.json - Fix whereami.cc and .h dependency; ported lates code Update whereami.cc - use `long` instead of `int64`. Fixed namespace issues. - PR CI fix: Add `parquet-testing` back Partial build fix for `flight_sql` folder - Replaced `namespace arrow` and `namespace odbcabstraction` with `using namespace ...` - fix flight_sql_connection.cc Fix `util::nullopt` to use `std::nullopt` - fix std::optional - fix BufferReader - Fix GetSchema - fix json_converter.cc - partial fix configuration.h - partial fix get_info_cache.cc - Fix winsock build error - Comment out `flight_sql` files that cannot build - Comment out configuration and unit tests - Comment out get info cache and system trust store Create initial odbc tests folder Implement SQLAllocEnv Fix cmake build Implement SQLFreeEnv Fix rest of build errors from `flightsql-odbc` - Fix get info errors - Fix for configuration window - added odbcinst library - Fix system trust store - unit test fixes - Add dependency of ARROW_COMPUTE. `arrow/compute/api.h` is used in `flight_sql`. Adding `ARROW_COMPUTE=ON` during build fixed run time unit tests failures. Implement SQLAllocConnect and SQLFreeConnect Fix build issue from static flight sql driver Lint and code style fixes Re-add deleted submodule parquet-testing clang-format lint fix cpplint lint fix Exclude whereami in rat exclude list C++/CLI lint fix Update parquet-testing to match commit from `main` Address Kou's comments ODBC directory lint fixes Catching the lint fixes outside of `flightsql-odbc` code Fix build warnings that get treated as error Implement SQLSetEnvAttr and SQLGetEnvAttr Implement use of ExecuteWithDiagnostics Doxygen Error Fixes and Address comments from Kou and James Address comments from Kou - Updates License.txt - Update cmake toolchain - Move whereami to `vendored` - Use string_view instead of NOLINT std::string Remove `whereami.cc` from arrow util build We are building whereami.cc as part of odbc Fix include headers to replace <> with "" Address comments from James Implement SQLGetDiagField --- .../odbc/ArrowFlightSqlOdbcConfig.cmake.in | 38 ++ cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 50 +++ .../sql/odbc/arrow-flight-sql-odbc.pc.in | 27 ++ cpp/src/arrow/flight/sql/odbc/entry_points.cc | 82 +++++ .../primitive_array_accessor_test.cc | 2 +- .../odbc/flight_sql/config/configuration.cc | 6 +- .../flight_sql_result_set_accessors.h | 4 +- .../flight_sql_result_set_metadata.cc | 4 +- .../odbc/flight_sql/flight_sql_ssl_config.h | 4 +- .../flight_sql_stream_chunk_buffer.h | 6 +- .../sql/odbc/flight_sql/get_info_cache.h | 4 +- .../include/flight_sql/flight_sql_driver.h | 4 +- .../sql/odbc/flight_sql/json_converter.h | 2 +- .../flight_sql/record_batch_transformer.h | 4 +- .../flight_sql/scalar_function_reporter.h | 2 +- .../flight/sql/odbc/flight_sql/system_dsn.cc | 3 +- .../odbc/flight_sql/ui/add_property_window.cc | 2 +- .../sql/odbc/flight_sql/ui/custom_window.cc | 4 +- .../flight_sql/ui/dsn_configuration_window.cc | 2 +- .../arrow/flight/sql/odbc/flight_sql/utils.h | 6 +- .../flight/sql/odbc/install/install_amd64.cmd | 36 ++ cpp/src/arrow/flight/sql/odbc/odbc.def | 41 +++ cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 330 ++++++++++++++++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 43 +++ .../sql/odbc/odbcabstraction/diagnostics.cc | 6 +- .../sql/odbc/odbcabstraction/encoding.cc | 2 +- .../sql/odbc/odbcabstraction/exceptions.cc | 5 +- .../include/odbcabstraction/diagnostics.h | 4 +- .../include/odbcabstraction/exceptions.h | 2 +- .../odbc_impl/attribute_utils.h | 8 +- .../odbc_impl/encoding_utils.h | 4 +- .../odbc_impl/odbc_connection.h | 4 +- .../odbc_impl/odbc_descriptor.h | 2 +- .../odbcabstraction/odbc_impl/odbc_handle.h | 8 +- .../odbc_impl/odbc_statement.h | 6 +- .../include/odbcabstraction/spi/connection.h | 4 +- .../include/odbcabstraction/spi/driver.h | 4 +- .../include/odbcabstraction/spi/result_set.h | 4 +- .../odbcabstraction/spi/result_set_metadata.h | 2 +- .../include/odbcabstraction/types.h | 2 +- .../include/odbcabstraction/utils.h | 4 +- .../flight/sql/odbc/odbcabstraction/logger.cc | 2 +- .../odbc_impl/odbc_connection.cc | 4 +- .../odbc_impl/odbc_environment.cc | 11 +- .../odbc_impl/odbc_statement.cc | 21 +- .../flight/sql/odbc/tests/CMakeLists.txt | 26 ++ .../flight/sql/odbc/tests/connection_test.cc | 310 ++++++++++++++++ cpp/src/arrow/flight/sql/odbc/visibility.h | 48 +++ 48 files changed, 1116 insertions(+), 83 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/ArrowFlightSqlOdbcConfig.cmake.in create mode 100644 cpp/src/arrow/flight/sql/odbc/arrow-flight-sql-odbc.pc.in create mode 100644 cpp/src/arrow/flight/sql/odbc/entry_points.cc create mode 100644 cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd create mode 100644 cpp/src/arrow/flight/sql/odbc/odbc.def create mode 100644 cpp/src/arrow/flight/sql/odbc/odbc_api.cc create mode 100644 cpp/src/arrow/flight/sql/odbc/odbc_api.h create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc create mode 100644 cpp/src/arrow/flight/sql/odbc/visibility.h diff --git a/cpp/src/arrow/flight/sql/odbc/ArrowFlightSqlOdbcConfig.cmake.in b/cpp/src/arrow/flight/sql/odbc/ArrowFlightSqlOdbcConfig.cmake.in new file mode 100644 index 00000000000..da6d44ebc82 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/ArrowFlightSqlOdbcConfig.cmake.in @@ -0,0 +1,38 @@ +# 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. +# +# This config sets the following variables in your project:: +# +# ArrowFlightSqlOdbc_FOUND - true if Arrow Flight SQL ODBC found on the system +# +# This config sets the following targets in your project:: +# +# ArrowFlightSqlOdbc::arrow_flight_sql_odbc_shared - for linked as shared library if shared library is built +# ArrowFlightSqlOdbc::arrow_flight_sql_odbc_static - for linked as static library if static library is built + +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(ArrowFlightSql) + +include("${CMAKE_CURRENT_LIST_DIR}/ArrowFlightSqlOdbcTargets.cmake") + +arrow_keep_backward_compatibility(ArrowFlightSqlOdbc arrow_flight_sql_odbc) + +check_required_components(ArrowFlightSqlOdbc) + +arrow_show_details(ArrowFlightSqlOdbc ARROW_FLIGHT_SQL_ODBC) diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 80be0dee99f..7be9758626f 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -19,3 +19,53 @@ add_custom_target(arrow_flight_sql_odbc) add_subdirectory(flight_sql) add_subdirectory(odbcabstraction) +add_subdirectory(tests) + +arrow_install_all_headers("arrow/flight/sql/odbc") + +set(ARROW_FLIGHT_SQL_ODBC_SRCS entry_points.cc odbc_api.cc) + +if(WIN32) + list(APPEND ARROW_FLIGHT_SQL_ODBC_SRCS odbc.def) +endif() + +if(WIN32) + if(MSVC_VERSION GREATER_EQUAL 1900) + set(ODBCINST legacy_stdio_definitions odbccp32 shlwapi) + endif() +elseif(APPLE) + set(ODBCINST iodbcinst) +else() + set(ODBCINST odbcinst) +endif() + +add_arrow_lib(arrow_flight_sql_odbc + CMAKE_PACKAGE_NAME + ArrowFlightSqlOdbc + PKG_CONFIG_NAME + arrow-flight-sql-odbc + OUTPUTS + ARROW_FLIGHT_SQL_ODBC_LIBRARIES + SOURCES + ${ARROW_FLIGHT_SQL_ODBC_SRCS} + DEPENDENCIES + arrow_flight_sql + SHARED_LINK_FLAGS + ${ARROW_VERSION_SCRIPT_FLAGS} # Defined in cpp/arrow/CMakeLists.txt + SHARED_LINK_LIBS + arrow_flight_sql_shared + SHARED_INSTALL_INTERFACE_LIBS + ArrowFlight::arrow_flight_sql_shared + STATIC_LINK_LIBS + arrow_flight_sql_static + STATIC_INSTALL_INTERFACE_LIBS + ArrowFlight::arrow_flight_sql_static + SHARED_PRIVATE_LINK_LIBS + ${ODBC_LIBRARIES} + ${ODBCINST} + odbcabstraction + arrow_odbc_spi_impl) + +foreach(LIB_TARGET ${ARROW_FLIGHT_SQL_ODBC_LIBRARIES}) + target_compile_definitions(${LIB_TARGET} PRIVATE ARROW_FLIGHT_SQL_ODBC_EXPORTING) +endforeach() diff --git a/cpp/src/arrow/flight/sql/odbc/arrow-flight-sql-odbc.pc.in b/cpp/src/arrow/flight/sql/odbc/arrow-flight-sql-odbc.pc.in new file mode 100644 index 00000000000..78959034954 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/arrow-flight-sql-odbc.pc.in @@ -0,0 +1,27 @@ +# 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. + +prefix=@CMAKE_INSTALL_PREFIX@ +includedir=@ARROW_PKG_CONFIG_INCLUDEDIR@ +libdir=@ARROW_PKG_CONFIG_LIBDIR@ + +Name: Apache Arrow Flight SQL ODBC +Description: Apache Arrow Flight SQL ODBC extension +Version: @ARROW_VERSION@ +Requires: arrow-flight-sql +Libs: -L${libdir} -larrow_flight_sql_odbc +Cflags.private: -DARROW_FLIGHT_SQL_ODBC_STATIC diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc new file mode 100644 index 00000000000..ce91e88e053 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -0,0 +1,82 @@ +// 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. + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include +#include + +#include "arrow/flight/sql/odbc/odbc_api.h" +#include "arrow/flight/sql/odbc/visibility.h" + +SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { + return arrow::SQLAllocHandle(type, parent, result); +} + +SQLRETURN SQL_API SQLAllocEnv(SQLHENV* env) { + return arrow::SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env); +} + +SQLRETURN SQL_API SQLAllocConnect(SQLHENV env, SQLHDBC* conn) { + return arrow::SQLAllocHandle(SQL_HANDLE_DBC, env, conn); +} + +SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { + return arrow::SQLFreeHandle(type, handle); +} + +SQLRETURN SQL_API SQLFreeEnv(SQLHENV env) { + return arrow::SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +SQLRETURN SQL_API SQLFreeConnect(SQLHDBC conn) { + return arrow::SQLFreeHandle(SQL_HANDLE_DBC, conn); +} + +SQLRETURN SQL_API SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + return arrow::SQLGetDiagFieldW(handleType, handle, recNumber, diagIdentifier, + diagInfoPtr, bufferLength, stringLengthPtr); +} + +SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER bufferLen, SQLINTEGER* strLenPtr) { + return arrow::SQLGetEnvAttr(env, attr, valuePtr, bufferLen, strLenPtr); +} + +SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER strLen) { + return arrow::SQLSetEnvAttr(env, attr, valuePtr, strLen); +} + +SQLRETURN SQL_API SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion) { + // TODO: implement SQLDriverConnect by linking to `odbc_impl` //-AL- TODO: create GitHub + // issue for SQLDriverConnect implementation + return SQL_INVALID_HANDLE; +} diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/primitive_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/primitive_array_accessor_test.cc index 820c0a7bd84..abf18fa9ce8 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/primitive_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/primitive_array_accessor_test.cc @@ -16,7 +16,7 @@ // under the License. #include "arrow/flight/sql/odbc/flight_sql/accessors/primitive_array_accessor.h" -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" #include "arrow/testing/builder.h" #include "gtest/gtest.h" diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc index be92be057da..4eb7d5980c2 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc @@ -38,17 +38,15 @@ std::string ReadDsnString(const std::string& dsn, const std::string_view& key, const std::string& dflt = "") { #define BUFFER_SIZE (1024) std::vector buf(BUFFER_SIZE); - - std::string key_str = std::string(key); int ret = - SQLGetPrivateProfileString(dsn.c_str(), key_str.c_str(), dflt.c_str(), buf.data(), + SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), static_cast(buf.size()), "ODBC.INI"); if (ret > BUFFER_SIZE) { // If there wasn't enough space, try again with the right size buffer. buf.resize(ret + 1); ret = - SQLGetPrivateProfileString(dsn.c_str(), key_str.c_str(), dflt.c_str(), buf.data(), + SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), static_cast(buf.size()), "ODBC.INI"); } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_accessors.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_accessors.h index 3f7d6856083..1d5014140ef 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_accessors.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_accessors.h @@ -17,9 +17,9 @@ #pragma once -#include -#include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" +#include "arrow/type_fwd.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc index f863d4bc489..035390981c8 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc @@ -16,10 +16,10 @@ // under the License. #include "arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.h" -#include -#include +#include "arrow/flight/sql/column_metadata.h" #include "arrow/flight/sql/odbc/flight_sql/utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" +#include "arrow/util/key_value_metadata.h" #include #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_ssl_config.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_ssl_config.h index 76a54f13ce1..2369f0aab4d 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_ssl_config.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_ssl_config.h @@ -17,9 +17,9 @@ #pragma once -#include -#include #include +#include "arrow/flight/types.h" +#include "arrow/status.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h index 4a84bcbede0..864c025d8b3 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h @@ -17,9 +17,9 @@ #pragma once -#include -#include -#include +#include "arrow/flight/client.h" +#include "arrow/flight/sql/client.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/blocking_queue.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h index a54dda2e13b..819b095e6a6 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h @@ -17,12 +17,12 @@ #pragma once -#include -#include #include #include #include #include +#include "arrow/flight/sql/client.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h index 88460cdf5b2..48f2a16416a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h @@ -17,8 +17,8 @@ #pragma once -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/json_converter.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/json_converter.h index de466af4f77..83809265df4 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/json_converter.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/json_converter.h @@ -17,8 +17,8 @@ #pragma once -#include #include +#include "arrow/type_fwd.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/record_batch_transformer.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/record_batch_transformer.h index 261b8c1d7c0..15c482cc631 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/record_batch_transformer.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/record_batch_transformer.h @@ -17,9 +17,9 @@ #pragma once -#include -#include #include +#include "arrow/flight/client.h" +#include "arrow/type.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/scalar_function_reporter.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/scalar_function_reporter.h index 5c2ae06cdba..fd6abf6420e 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/scalar_function_reporter.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/scalar_function_reporter.h @@ -17,7 +17,7 @@ #pragma once -#include +#include "arrow/type.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc index 504b62a81eb..67a4c3db3d3 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc @@ -125,8 +125,7 @@ bool RegisterDsn(const Configuration& config, LPCSTR driver) { continue; } - std::string key_str = std::string(key); - if (!SQLWritePrivateProfileString(dsn.c_str(), key_str.c_str(), it->second.c_str(), + if (!SQLWritePrivateProfileString(dsn.c_str(), key.data(), it->second.c_str(), "ODBC.INI")) { PostLastInstallerError(); return false; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc index 64cc1797f7e..75aa491f781 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc @@ -24,7 +24,7 @@ #include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" #include "ui/custom_window.h" #include "ui/window.h" diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc index 5443ea0ec8d..e79e1221e78 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc @@ -17,7 +17,7 @@ // platform.h includes windows.h, so it needs to be included // before Windowsx.h and commctrl.h -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" #include #include @@ -25,7 +25,7 @@ #include #include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" #include "ui/custom_window.h" namespace driver { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index 42741c5a3e5..c47984ca400 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -20,11 +20,11 @@ #include #include -#include #include #include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h" #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/add_property_window.h" diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.h index 586cfb22a30..8b3e14599a7 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.h @@ -17,13 +17,13 @@ #pragma once -#include -#include -#include #include #include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" +#include "arrow/flight/types.h" namespace driver { namespace flight_sql { diff --git a/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd b/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd new file mode 100644 index 00000000000..fe365d59b90 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd @@ -0,0 +1,36 @@ +@echo off + +set ODBC_AMD64=%1 + +@REM enable delayed variable expansion to make environment variables enclosed with "!" to be evaluated +@REM when the command is executed instead of when the command is parsed +setlocal enableextensions enabledelayedexpansion + +if [%ODBC_AMD64%] == [] ( + echo error: 64-bit driver is not specified. Call format: install_amd64 abs_path_to_64_bit_driver + pause + exit /b 1 +) + +if exist %ODBC_AMD64% ( + for %%i IN (%ODBC_AMD64%) DO IF EXIST %%~si\NUL ( + echo warning: The path you have specified seems to be a directory. Note that you have to specify path to driver file itself instead. + ) + echo Installing 64-bit driver: %ODBC_AMD64% + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\Apache Arrow Flight SQL ODBC Driver" /v DriverODBCVer /t REG_SZ /d "03.80" /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\Apache Arrow Flight SQL ODBC Driver" /v UsageCount /t REG_DWORD /d 00000001 /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\Apache Arrow Flight SQL ODBC Driver" /v Driver /t REG_SZ /d %ODBC_AMD64% /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\Apache Arrow Flight SQL ODBC Driver" /v Setup /t REG_SZ /d %ODBC_AMD64% /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers" /v "Apache Arrow Flight SQL ODBC Driver" /t REG_SZ /d "Installed" /f + + IF !ERRORLEVEL! NEQ 0 ( + echo Error occurred while registering 64-bit driver. Exiting. + echo ERRORLEVEL: !ERRORLEVEL! + exit !ERRORLEVEL! + ) +) else ( + echo 64-bit driver can not be found: %ODBC_AMD64% + echo Call format: install_amd64 abs_path_to_64_bit_driver + pause + exit /b 1 +) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def new file mode 100644 index 00000000000..2c93d183c92 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -0,0 +1,41 @@ + +LIBRARY arrow_flight_sql_odbc +EXPORTS + SQLAllocConnect + SQLAllocEnv + SQLAllocHandle + SQLAllocStmt + SQLBindCol + SQLCancel + SQLCloseCursor + SQLColAttributeW + SQLColumnsW + SQLConnectW + SQLDisconnect + SQLDriverConnectW + SQLErrorW + SQLExecDirectW + SQLExecute + SQLFetch + SQLForeignKeysW + SQLFreeEnv + SQLFreeHandle + SQLFreeStmt + SQLGetConnectAttrW + SQLGetData + SQLGetDiagFieldW + SQLGetDiagRecW + SQLGetEnvAttr + SQLGetInfoW + SQLGetStmtAttrW + SQLGetTypeInfoW + SQLMoreResults + SQLNativeSqlW + SQLNumResultCols + SQLPrepareW + SQLPrimaryKeysW + SQLSetConnectAttrW + SQLSetEnvAttr + SQLSetStmtAttrW + SQLTablesW + diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc new file mode 100644 index 00000000000..a57a371044e --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -0,0 +1,330 @@ +// 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 +#include +#include +#include +#include +#include + +// odbc_api includes windows.h, which needs to be put behind winsock2.h. +// odbc_environment.h includes winsock2.h +#include + +namespace arrow { +SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { + // TODO: implement SQLAllocHandle by linking to `odbc_impl` + *result = nullptr; + + switch (type) { + case SQL_HANDLE_ENV: { + using driver::flight_sql::FlightSqlDriver; + using ODBC::ODBCEnvironment; + + *result = SQL_NULL_HENV; + + try { + static std::shared_ptr odbc_driver = + std::make_shared(); + *result = reinterpret_cast(new ODBCEnvironment(odbc_driver)); + + return SQL_SUCCESS; + } catch (const std::bad_alloc&) { + // allocating environment failed so cannot log diagnostic error here + return SQL_ERROR; + } + } + + case SQL_HANDLE_DBC: { + using ODBC::ODBCConnection; + using ODBC::ODBCEnvironment; + + *result = SQL_NULL_HDBC; + + ODBCEnvironment* environment = reinterpret_cast(parent); + + return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { + std::shared_ptr conn = environment->CreateConnection(); + + if (conn) { + *result = reinterpret_cast(conn.get()); + + return SQL_SUCCESS; + } + + return SQL_ERROR; + }); + } + + case SQL_HANDLE_STMT: { + return SQL_INVALID_HANDLE; + } + + default: + break; + } + + return SQL_ERROR; +} + +SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { + switch (type) { + case SQL_HANDLE_ENV: { + using ODBC::ODBCEnvironment; + + ODBCEnvironment* environment = reinterpret_cast(handle); + + if (!environment) { + return SQL_INVALID_HANDLE; + } + + delete environment; + + return SQL_SUCCESS; + } + + case SQL_HANDLE_DBC: { + using ODBC::ODBCConnection; + + ODBCConnection* conn = reinterpret_cast(handle); + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + conn->releaseConnection(); + + return SQL_SUCCESS; + }); + } + + case SQL_HANDLE_STMT: + return SQL_INVALID_HANDLE; + + case SQL_HANDLE_DESC: + return SQL_INVALID_HANDLE; + + default: + break; + } + + return SQL_ERROR; +} + +SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + using driver::odbcabstraction::Diagnostics; + using ODBC::GetStringAttribute; + using ODBC::ODBCConnection; + using ODBC::ODBCEnvironment; + + if (!handle) { + return SQL_INVALID_HANDLE; + } + + if (!diagInfoPtr) { + return SQL_ERROR; + } + + // Set character type to be Unicode by defualt (not Ansi) + bool isUnicode = true; + Diagnostics* diagnostics = nullptr; + + switch (handleType) { + case SQL_HANDLE_ENV: { + ODBCEnvironment* environment = reinterpret_cast(handle); + diagnostics = &environment->GetDiagnostics(); + break; + } + + case SQL_HANDLE_DBC: { + ODBCConnection* connection = reinterpret_cast(handle); + diagnostics = &connection->GetDiagnostics(); + break; + } + + default: + return SQL_ERROR; + } + + if (!diagnostics) { + return SQL_ERROR; + } + + // Retrieve header level diagnostics if Record 0 specified + if (recNumber == 0) { + switch (diagIdentifier) { + case SQL_DIAG_NUMBER: { + SQLINTEGER count = static_cast(diagnostics->GetRecordCount()); + *static_cast(diagInfoPtr) = count; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); + } + + return SQL_SUCCESS; + } + + case SQL_DIAG_SERVER_NAME: { + const std::string source = diagnostics->GetDataSourceComponent(); + return GetStringAttribute(isUnicode, source, false, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + default: + return SQL_ERROR; + } + } + + // Retrieve record level diagnostics from specified 1 based record + uint32_t recordIndex = static_cast(recNumber - 1); + if (!diagnostics->HasRecord(recordIndex)) { + return SQL_NO_DATA; + } + + // Retrieve record field data + switch (diagIdentifier) { + case SQL_DIAG_MESSAGE_TEXT: { + const std::string message = diagnostics->GetMessageText(recordIndex); + return GetStringAttribute(isUnicode, message, false, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + case SQL_DIAG_NATIVE: { + *static_cast(diagInfoPtr) = diagnostics->GetNativeError(recordIndex); + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); + } + + return SQL_SUCCESS; + } + + case SQL_DIAG_SQLSTATE: { + const std::string state = diagnostics->GetSQLState(recordIndex); + return GetStringAttribute(isUnicode, state, false, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + default: + return SQL_ERROR; + } + + return SQL_ERROR; +} + +SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER bufferLen, SQLINTEGER* strLenPtr) { + using driver::odbcabstraction::DriverException; + using ODBC::ODBCEnvironment; + + ODBCEnvironment* environment = reinterpret_cast(env); + + return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { + switch (attr) { + case SQL_ATTR_ODBC_VERSION: { + if (!valuePtr && !strLenPtr) { + throw DriverException("Invalid null pointer for attribute.", "HY000"); + } + + if (valuePtr) { + SQLINTEGER* value = reinterpret_cast(valuePtr); + *value = static_cast(environment->getODBCVersion()); + } + + if (strLenPtr) { + *strLenPtr = sizeof(SQLINTEGER); + } + + return SQL_SUCCESS; + } + + case SQL_ATTR_OUTPUT_NTS: { + if (!valuePtr && !strLenPtr) { + throw DriverException("Invalid null pointer for attribute.", "HY000"); + } + + if (valuePtr) { + // output nts always returns SQL_TRUE + SQLINTEGER* value = reinterpret_cast(valuePtr); + *value = SQL_TRUE; + } + + if (strLenPtr) { + *strLenPtr = sizeof(SQLINTEGER); + } + + return SQL_SUCCESS; + } + + case SQL_ATTR_CONNECTION_POOLING: + case SQL_ATTR_APP_ROW_DESC: { + throw DriverException("Optional feature not supported.", "HYC00"); + } + + default: { + throw DriverException("Invalid attribute", "HYC00"); + } + } + }); +} + +SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER strLen) { + using driver::odbcabstraction::DriverException; + using ODBC::ODBCEnvironment; + + ODBCEnvironment* environment = reinterpret_cast(env); + + return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { + if (!valuePtr) { + throw DriverException("Invalid null pointer for attribute.", "HY024"); + } + + switch (attr) { + case SQL_ATTR_ODBC_VERSION: { + SQLINTEGER version = + static_cast(reinterpret_cast(valuePtr)); + if (version == SQL_OV_ODBC2 || version == SQL_OV_ODBC3) { + environment->setODBCVersion(version); + + return SQL_SUCCESS; + } else { + throw DriverException("Invalid value for attribute", "HY024"); + } + } + + case SQL_ATTR_OUTPUT_NTS: { + // output nts can not be set to SQL_FALSE, is always SQL_TRUE + SQLINTEGER value = static_cast(reinterpret_cast(valuePtr)); + if (value == SQL_TRUE) { + return SQL_SUCCESS; + } else { + throw DriverException("Invalid value for attribute", "HY024"); + } + } + + case SQL_ATTR_CONNECTION_POOLING: + case SQL_ATTR_APP_ROW_DESC: { + throw DriverException("Optional feature not supported.", "HYC00"); + } + + default: { + throw DriverException("Invalid attribute", "HY092"); + } + } + }); +} +} // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h new file mode 100644 index 00000000000..6c204fe3ae3 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -0,0 +1,43 @@ +// 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. + +#pragma once + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +// @file odbc_api.h +// +// Define internal ODBC API function headers. + +namespace arrow { +SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result); +SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle); +SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr); +SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER bufferLen, SQLINTEGER* strLenPtr); +SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER strLen); +} // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/diagnostics.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/diagnostics.cc index 8c94978ef99..78ca45ea2fe 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/diagnostics.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/diagnostics.cc @@ -15,9 +15,9 @@ // specific language governing permissions and limitations // under the License. -#include -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/encoding.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/encoding.cc index 95dc920da78..00718cdbbe5 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/encoding.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/encoding.cc @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" #if defined(__APPLE__) # include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/exceptions.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/exceptions.cc index fcd8163a500..242c85e5a28 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/exceptions.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/exceptions.cc @@ -15,8 +15,9 @@ // specific language governing permissions and limitations // under the License. -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + #include namespace driver { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h index f1c6efe4982..473411efd4f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h @@ -21,8 +21,8 @@ #include #include -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h index 48a773e4f4d..82ffebedff6 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h @@ -17,10 +17,10 @@ #pragma once -#include #include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/error_codes.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h index 9163e942ceb..7b3b457a35f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h @@ -17,16 +17,16 @@ #pragma once -#include -#include -#include #include #include #include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" namespace ODBC { using driver::odbcabstraction::WcsToUtf8; diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 25619bb5555..01eae6f059a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -16,9 +16,9 @@ // under the License. #pragma once +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" -#include -#include #include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h index 6a01fe128d9..e771f467e6e 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h @@ -17,9 +17,9 @@ #pragma once -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" -#include #include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h index 092483f4719..e7656082c5c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h @@ -17,7 +17,7 @@ #pragma once -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h" #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h index c2428df394d..64257541a87 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h @@ -17,12 +17,14 @@ #pragma once -#include -#include +// platform.h includes windows.h, so it needs to be included first +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + #include #include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" /** * @brief An abstraction over a generic ODBC handle. @@ -47,7 +49,7 @@ class ODBCHandle { rc = function(); } catch (const driver::odbcabstraction::DriverException& ex) { GetDiagnostics().AddError(ex); - } catch (const std::bad_alloc& ex) { + } catch (const std::bad_alloc&) { GetDiagnostics().AddError(driver::odbcabstraction::DriverException( "A memory allocation error occurred.", "HY001")); } catch (const std::exception& ex) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h index bbddfac4185..29efaec8280 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h @@ -17,9 +17,11 @@ #pragma once -#include +// platform.h platform.h includes windows.h so it needs to be included first +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_handle.h" -#include #include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h index 792a52c1fad..64b7e6a724b 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h @@ -25,8 +25,8 @@ #include #include -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h index f13371bf2d5..61d570574c7 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h @@ -19,8 +19,8 @@ #include -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h index 1b3f8eb96d8..4c12a4b5934 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h @@ -20,9 +20,9 @@ #include #include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set_metadata.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set_metadata.h index f625a2598c1..636dce21e4a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set_metadata.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set_metadata.h @@ -17,8 +17,8 @@ #pragma once -#include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h index e5d206a2ca7..8f16000daaa 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h @@ -17,8 +17,8 @@ #pragma once -#include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h index cc848baa0fd..0fa8463b546 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h @@ -17,10 +17,10 @@ #pragma once -#include -#include #include #include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/logger.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/logger.cc index edace64cf6a..8b105a2f0b6 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/logger.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/logger.cc @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" namespace driver { namespace odbcabstraction { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 0143976bb48..4d5d4dc3656 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -85,9 +85,7 @@ void loadPropertiesFromDSN(const std::string& dsn, for (auto& key : keys) { outputBuffer.clear(); outputBuffer.resize(BUFFER_SIZE, '\0'); - - std::string key_str = std::string(key); - SQLGetPrivateProfileString(dsn.c_str(), key_str.c_str(), "", &outputBuffer[0], + SQLGetPrivateProfileString(dsn.c_str(), key.data(), "", &outputBuffer[0], BUFFER_SIZE, "odbc.ini"); std::string value = std::string(&outputBuffer[0]); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_environment.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_environment.cc index 7781235688f..9d7a8223591 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_environment.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_environment.cc @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_environment.h" + +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/driver.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" -#include -#include -#include -#include #include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index a5db0cc25dd..f6c06060d67 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -15,16 +15,17 @@ // specific language governing permissions and limitations // under the License. -#include - -#include -#include -#include -#include -#include -#include -#include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" + +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set_metadata.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/statement.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" + #include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt new file mode 100644 index 00000000000..161669b41df --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -0,0 +1,26 @@ +# 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. + +add_custom_target(tests) + +include_directories(${ODBC_INCLUDE_DIRS}) + +add_arrow_test(connection_test + SOURCES + connection_test.cc + EXTRA_LINK_LIBS + ${ODBC_LIBRARIES}) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc new file mode 100644 index 00000000000..7991ec40263 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -0,0 +1,310 @@ +// 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. + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include +#include "gtest/gtest.h" + +namespace arrow { +namespace flight { +namespace odbc { +namespace integration_tests { + +TEST(SQLAllocHandle, TestSQLAllocHandleEnv) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); + + EXPECT_TRUE(env != NULL); +} + +TEST(SQLAllocEnv, TestSQLAllocEnv) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_value = SQLAllocEnv(&env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); +} + +TEST(SQLAllocHandle, TestSQLAllocHandleConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN return_value = SQLAllocEnv(&env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); + + // Allocate a connection using alloc handle + SQLRETURN return_alloc_handle = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(return_alloc_handle == SQL_SUCCESS); +} + +TEST(SQLAllocConnect, TestSQLAllocHandleConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN return_value = SQLAllocEnv(&env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); + + // Allocate a connection using alloc handle + SQLRETURN return_alloc_connect = SQLAllocConnect(env, &conn); + + EXPECT_TRUE(return_alloc_connect == SQL_SUCCESS); +} + +TEST(SQLFreeHandle, TestSQLFreeHandleEnv) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); + + // Free an environment handle + SQLRETURN return_value = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); +} + +TEST(SQLFreeEnv, TestSQLFreeEnv) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); + + // Free an environment handle + SQLRETURN return_value = SQLFreeEnv(env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); +} + +TEST(SQLFreeHandle, TestSQLFreeHandleConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN return_value = SQLAllocEnv(&env); + + EXPECT_TRUE(return_value == SQL_SUCCESS); + + // Allocate a connection using alloc handle + SQLRETURN return_alloc_handle = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(return_alloc_handle == SQL_SUCCESS); + + // Free the created connection using free handle + SQLRETURN return_free_handle = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(return_free_handle == SQL_SUCCESS); +} + +TEST(SQLFreeConnect, TestSQLFreeConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Allocate a connection using alloc handle + SQLRETURN return_alloc_handle = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(return_alloc_handle == SQL_SUCCESS); + + // Free the created connection using free connect + SQLRETURN return_free_connect = SQLFreeConnect(conn); + + EXPECT_TRUE(return_free_connect == SQL_SUCCESS); +} + +TEST(SQLGetEnvAttr, TestSQLGetEnvAttrODBCVersion) { + // ODBC Environment + SQLHENV env; + + SQLINTEGER version; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, &version, 0, 0); + + EXPECT_TRUE(return_get == SQL_SUCCESS); + + EXPECT_EQ(version, SQL_OV_ODBC2); +} + +TEST(SQLSetEnvAttr, TestSQLSetEnvAttrODBCVersionValid) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Attempt to set to unsupported version + SQLRETURN return_set = + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC2), 0); + + EXPECT_TRUE(return_set == SQL_SUCCESS); +} + +TEST(SQLSetEnvAttr, TestSQLSetEnvAttrODBCVersionInvalid) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Attempt to set to unsupported version + SQLRETURN return_set = + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(1), 0); + + EXPECT_TRUE(return_set == SQL_ERROR); +} + +TEST(SQLGetEnvAttr, TestSQLGetEnvAttrOutputNTS) { + // ODBC Environment + SQLHENV env; + + SQLINTEGER output_nts; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, &output_nts, 0, 0); + + EXPECT_TRUE(return_get == SQL_SUCCESS); + + EXPECT_EQ(output_nts, SQL_TRUE); +} + +TEST(SQLGetEnvAttr, TestSQLGetEnvAttrGetLength) { + GTEST_SKIP(); + // ODBC Environment + SQLHENV env; + + SQLINTEGER length; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, &length); + + EXPECT_TRUE(return_get == SQL_SUCCESS); + + EXPECT_EQ(length, sizeof(SQLINTEGER)); +} + +TEST(SQLGetEnvAttr, TestSQLGetEnvAttrNullValuePointer) { + GTEST_SKIP(); + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, nullptr); + + EXPECT_TRUE(return_get == SQL_ERROR); +} + +TEST(SQLSetEnvAttr, TestSQLSetEnvAttrOutputNTSValid) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Attempt to set to output nts to supported version + SQLRETURN return_set = + SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, reinterpret_cast(SQL_TRUE), 0); + + EXPECT_TRUE(return_set == SQL_SUCCESS); +} + +TEST(SQLSetEnvAttr, TestSQLSetEnvAttrOutputNTSInvalid) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Attempt to set to output nts to unsupported false + SQLRETURN return_set = + SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, reinterpret_cast(SQL_FALSE), 0); + + EXPECT_TRUE(return_set == SQL_ERROR); +} + +TEST(SQLSetEnvAttr, TestSQLSetEnvAttrNullValuePointer) { + // ODBC Environment + SQLHENV env; + + // Allocate an environment handle + SQLRETURN return_env = SQLAllocEnv(&env); + + EXPECT_TRUE(return_env == SQL_SUCCESS); + + // Attempt to set using bad data pointer + SQLRETURN return_set = + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0); + + EXPECT_TRUE(return_set == SQL_ERROR); +} + +} // namespace integration_tests +} // namespace odbc +} // namespace flight +} // namespace arrow + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/cpp/src/arrow/flight/sql/odbc/visibility.h b/cpp/src/arrow/flight/sql/odbc/visibility.h new file mode 100644 index 00000000000..416dfecc864 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/visibility.h @@ -0,0 +1,48 @@ +// 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. + +#pragma once + +#if defined(_WIN32) || defined(__CYGWIN__) +# if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4251) +# else +# pragma GCC diagnostic ignored "-Wattributes" +# endif + +# ifdef ARROW_FLIGHT_SQL_ODBC_STATIC +# define ARROW_FLIGHT_SQL_ODBC_EXPORT +# elif defined(ARROW_FLIGHT_SQL_ODBC_EXPORTING) +# define ARROW_FLIGHT_SQL_ODBC_EXPORT __declspec(dllexport) +# else +# define ARROW_FLIGHT_SQL_ODBC_EXPORT __declspec(dllimport) +# endif + +# define ARROW_FLIGHT_SQL_ODBC_NO_EXPORT +#else // Not Windows +# ifndef ARROW_FLIGHT_SQL_ODBC_EXPORT +# define ARROW_FLIGHT_SQL_ODBC_EXPORT __attribute__((visibility("default"))) +# endif +# ifndef ARROW_FLIGHT_SQL_ODBC_NO_EXPORT +# define ARROW_FLIGHT_SQL_ODBC_NO_EXPORT __attribute__((visibility("hidden"))) +# endif +#endif // Non-Windows + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif From c75eae82f3223e9cfe584bc6e9ba10e2b566e526 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 12 May 2025 16:28:12 -0700 Subject: [PATCH 02/74] SQLDriverConnect, SQLConnect and SQLDisconnect Implement stubs for SQLGetInfo, SQLGetDiagField and SQLGetDiagRec Separate RegisterDsn and UnregisterDsn from windows build Update code to save driver value from connection string Add ReadMes for ODBC and tests Fix test issues with string_view Address code reviews Update entry_points.cc to fix build issue Remove Dremio references Use emplace properly Address comment from Rob and add SQLDisconnect test case --- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 20 +- cpp/src/arrow/flight/sql/odbc/README | 19 + cpp/src/arrow/flight/sql/odbc/entry_points.cc | 49 +- .../flight/sql/odbc/flight_sql/CMakeLists.txt | 4 +- .../odbc/flight_sql/config/configuration.cc | 27 +- .../odbc/flight_sql/flight_sql_auth_method.cc | 12 +- .../odbc/flight_sql/flight_sql_connection.cc | 35 +- .../odbc/flight_sql/flight_sql_connection.h | 7 + .../flight_sql/flight_sql_connection_test.cc | 36 +- .../sql/odbc/flight_sql/flight_sql_driver.cc | 12 +- .../flight_sql_get_tables_reader.cc | 7 +- .../flight_sql_statement_get_columns.cc | 8 +- .../include/flight_sql/config/configuration.h | 9 +- .../arrow/flight/sql/odbc/flight_sql/main.cc | 14 +- .../flight/sql/odbc/flight_sql/system_dsn.cc | 91 +--- .../flight/sql/odbc/flight_sql/system_dsn.h | 51 ++ .../sql/odbc/flight_sql/win_system_dsn.cc | 119 +++++ cpp/src/arrow/flight/sql/odbc/odbc.def | 3 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 137 ++++- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 14 +- .../odbc_impl/attribute_utils.h | 17 +- .../odbc_impl/encoding_utils.h | 43 +- .../odbc_impl/odbc_connection.h | 8 + .../include/odbcabstraction/spi/connection.h | 4 +- .../odbc_impl/odbc_connection.cc | 7 +- .../flight/sql/odbc/odbcabstraction/utils.cc | 4 +- .../flight/sql/odbc/tests/CMakeLists.txt | 9 +- cpp/src/arrow/flight/sql/odbc/tests/README | 4 + .../flight/sql/odbc/tests/connection_test.cc | 502 +++++++++++++++++- .../flight/sql/odbc/tests/odbc_test_suite.cc | 172 ++++++ .../flight/sql/odbc/tests/odbc_test_suite.h | 90 ++++ 31 files changed, 1290 insertions(+), 244 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/README create mode 100644 cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h create mode 100644 cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/README create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 7be9758626f..1f7c5d1f61f 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -17,6 +17,16 @@ add_custom_target(arrow_flight_sql_odbc) +if(WIN32) + if(MSVC_VERSION GREATER_EQUAL 1900) + set(ODBCINST legacy_stdio_definitions odbccp32 shlwapi) + endif() +elseif(APPLE) + set(ODBCINST iodbcinst) +else() + set(ODBCINST odbcinst) +endif() + add_subdirectory(flight_sql) add_subdirectory(odbcabstraction) add_subdirectory(tests) @@ -29,16 +39,6 @@ if(WIN32) list(APPEND ARROW_FLIGHT_SQL_ODBC_SRCS odbc.def) endif() -if(WIN32) - if(MSVC_VERSION GREATER_EQUAL 1900) - set(ODBCINST legacy_stdio_definitions odbccp32 shlwapi) - endif() -elseif(APPLE) - set(ODBCINST iodbcinst) -else() - set(ODBCINST odbcinst) -endif() - add_arrow_lib(arrow_flight_sql_odbc CMAKE_PACKAGE_NAME ArrowFlightSqlOdbc diff --git a/cpp/src/arrow/flight/sql/odbc/README b/cpp/src/arrow/flight/sql/odbc/README new file mode 100644 index 00000000000..04749d9b859 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/README @@ -0,0 +1,19 @@ +Steps to Register the 64-bit Apache Arrow ODBC driver on Windows + +After the build succeeds, the ODBC DLL will be located in +`build\debug\Debug` for a debug build and `build\release\Release` for a release build. + +1. Open Power Shell as administrator. + +2. Register your ODBC DLL: + Need to replace with actual path to repository in the commands. + + i. `cd to repo.` + ii. `cd ` + iii. Run script to register your ODBC DLL as Apache Arrow Flight SQL ODBC Driver + `.\cpp\src\arrow\flight\sql\odbc\install\install_amd64.cmd \cpp\build\< release | debug >\< Release | Debug>\arrow_flight_sql_odbc.dll` + Example command for reference: + `.\cpp\src\arrow\flight\sql\odbc\install\install_amd64.cmd C:\path\to\arrow\cpp\build\release\Release\arrow_flight_sql_odbc.dll` + +If the registration is successful, then Apache Arrow Flight SQL ODBC Driver +should show as an available ODBC driver in the x64 ODBC Driver Manager. diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index ce91e88e053..0fc55720938 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -69,14 +69,43 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePt return arrow::SQLSetEnvAttr(env, attr, valuePtr, strLen); } -SQLRETURN SQL_API SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, - SQLWCHAR* inConnectionString, - SQLSMALLINT inConnectionStringLen, - SQLWCHAR* outConnectionString, - SQLSMALLINT outConnectionStringBufferLen, - SQLSMALLINT* outConnectionStringLen, - SQLUSMALLINT driverCompletion) { - // TODO: implement SQLDriverConnect by linking to `odbc_impl` //-AL- TODO: create GitHub - // issue for SQLDriverConnect implementation - return SQL_INVALID_HANDLE; +SQLRETURN SQL_API SQLSetConnectAttrW(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, + SQLINTEGER valueLen) { + // TODO implement SQLSetConnectAttr + return SQL_ERROR; } + +SQLRETURN SQL_API SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, SQLSMALLINT bufLen, + SQLSMALLINT* length) { + return arrow::SQLGetInfoW(conn, infoType, infoValuePtr, bufLen, length); +} + +SQLRETURN SQL_API SQLGetDiagRecW(SQLSMALLINT type, SQLHANDLE handle, SQLSMALLINT recNum, + SQLWCHAR* sqlState, SQLINTEGER* nativeError, + SQLWCHAR* msgBuffer, SQLSMALLINT msgBufferLen, + SQLSMALLINT* msgLen) { + // TODO implement SQLGetDiagRecW + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion) { + return arrow::SQLDriverConnectW( + conn, windowHandle, inConnectionString, inConnectionStringLen, outConnectionString, + outConnectionStringBufferLen, outConnectionStringLen, driverCompletion); +} + +SQLRETURN SQL_API SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, + SQLWCHAR* password, SQLSMALLINT passwordLen) { + return arrow::SQLConnectW(conn, dsnName, dsnNameLen, userName, userNameLen, password, + passwordLen); +} + +SQLRETURN SQL_API SQLDisconnect(SQLHDBC conn) { return arrow::SQLDisconnect(conn); } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt index 56aabb54dbf..6985f781b9a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt @@ -76,6 +76,8 @@ add_library(arrow_odbc_spi_impl scalar_function_reporter.h system_trust_store.cc system_trust_store.h + system_dsn.cc + system_dsn.h utils.cc) target_include_directories(arrow_odbc_spi_impl PUBLIC include include/flight_sql @@ -96,7 +98,7 @@ if(WIN32) ui/window.cc ui/dsn_configuration_window.cc ui/add_property_window.cc - system_dsn.cc) + win_system_dsn.cc) endif() target_link_libraries(arrow_odbc_spi_impl PUBLIC odbcabstraction arrow_flight_sql_shared) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc index 4eb7d5980c2..bfd050e724b 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc @@ -38,16 +38,14 @@ std::string ReadDsnString(const std::string& dsn, const std::string_view& key, const std::string& dflt = "") { #define BUFFER_SIZE (1024) std::vector buf(BUFFER_SIZE); - int ret = - SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), - static_cast(buf.size()), "ODBC.INI"); + int ret = SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), + static_cast(buf.size()), "ODBC.INI"); if (ret > BUFFER_SIZE) { // If there wasn't enough space, try again with the right size buffer. buf.resize(ret + 1); - ret = - SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), - static_cast(buf.size()), "ODBC.INI"); + ret = SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), + static_cast(buf.size()), "ODBC.INI"); } return std::string(buf.data(), ret); @@ -140,11 +138,11 @@ void Configuration::LoadDsn(const std::string& dsn) { void Configuration::Clear() { this->properties.clear(); } bool Configuration::IsSet(const std::string_view& key) const { - return 0 != this->properties.count(key); + return 0 != this->properties.count(std::string(key)); } const std::string& Configuration::Get(const std::string_view& key) const { - const auto itr = this->properties.find(key); + const auto itr = this->properties.find(std::string(key)); if (itr == this->properties.cend()) { static const std::string empty(""); return empty; @@ -155,7 +153,15 @@ const std::string& Configuration::Get(const std::string_view& key) const { void Configuration::Set(const std::string_view& key, const std::string& value) { const std::string copy = boost::trim_copy(value); if (!copy.empty()) { - this->properties[key] = value; + this->properties[std::string(key)] = value; + } +} + +void Configuration::Emplace(const std::string_view& key, std::string&& value) { + const std::string copy = boost::trim_copy(value); + if (!copy.empty()) { + this->properties.emplace( + std::make_pair(std::move(std::string(key)), std::move(value))); } } @@ -167,13 +173,12 @@ const driver::odbcabstraction::Connection::ConnPropertyMap& Configuration::GetPr std::vector Configuration::GetCustomKeys() const { driver::odbcabstraction::Connection::ConnPropertyMap copyProps(properties); for (auto& key : FlightSqlConnection::ALL_KEYS) { - copyProps.erase(key); + copyProps.erase(std::string(key)); } std::vector keys; boost::copy(copyProps | boost::adaptors::map_keys, std::back_inserter(keys)); return keys; } - } // namespace config } // namespace flight_sql } // namespace driver diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc index fcf951270e6..3fcc3a87162 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc @@ -153,22 +153,22 @@ std::unique_ptr FlightSqlAuthMethod::FromProperties( const std::unique_ptr& client, const Connection::ConnPropertyMap& properties) { // Check if should use user-password authentication - auto it_user = properties.find(FlightSqlConnection::USER); + auto it_user = properties.find(std::string(FlightSqlConnection::USER)); if (it_user == properties.end()) { // The Microsoft OLE DB to ODBC bridge provider (MSDASQL) will write // "User ID" and "Password" properties instead of mapping // to ODBC compliant UID/PWD keys. - it_user = properties.find(FlightSqlConnection::USER_ID); + it_user = properties.find(std::string(FlightSqlConnection::USER_ID)); } - auto it_password = properties.find(FlightSqlConnection::PASSWORD); - auto it_token = properties.find(FlightSqlConnection::TOKEN); + auto it_password = properties.find(std::string(FlightSqlConnection::PASSWORD)); + auto it_token = properties.find(std::string(FlightSqlConnection::TOKEN)); if (it_user == properties.end() || it_password == properties.end()) { // Accept UID/PWD as aliases for User/Password. These are suggested as // standard properties in the documentation for SQLDriverConnect. - it_user = properties.find(FlightSqlConnection::UID); - it_password = properties.find(FlightSqlConnection::PWD); + it_user = properties.find(std::string(FlightSqlConnection::UID)); + it_password = properties.find(std::string(FlightSqlConnection::PWD)); } if (it_user != properties.end() || it_password != properties.end()) { const std::string& user = it_user != properties.end() ? it_user->second : ""; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc index 09764e5c18b..708ac2f81a4 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc @@ -111,26 +111,26 @@ inline std::string GetCerts() { return ""; } #endif -const std::set - BUILT_IN_PROPERTIES = {FlightSqlConnection::HOST, - FlightSqlConnection::PORT, - FlightSqlConnection::USER, - FlightSqlConnection::USER_ID, - FlightSqlConnection::UID, - FlightSqlConnection::PASSWORD, - FlightSqlConnection::PWD, - FlightSqlConnection::TOKEN, - FlightSqlConnection::USE_ENCRYPTION, - FlightSqlConnection::DISABLE_CERTIFICATE_VERIFICATION, - FlightSqlConnection::TRUSTED_CERTS, - FlightSqlConnection::USE_SYSTEM_TRUST_STORE, - FlightSqlConnection::STRING_COLUMN_LENGTH, - FlightSqlConnection::USE_WIDE_CHAR}; +const std::set BUILT_IN_PROPERTIES = { + FlightSqlConnection::HOST, + FlightSqlConnection::PORT, + FlightSqlConnection::USER, + FlightSqlConnection::USER_ID, + FlightSqlConnection::UID, + FlightSqlConnection::PASSWORD, + FlightSqlConnection::PWD, + FlightSqlConnection::TOKEN, + FlightSqlConnection::USE_ENCRYPTION, + FlightSqlConnection::DISABLE_CERTIFICATE_VERIFICATION, + FlightSqlConnection::TRUSTED_CERTS, + FlightSqlConnection::USE_SYSTEM_TRUST_STORE, + FlightSqlConnection::STRING_COLUMN_LENGTH, + FlightSqlConnection::USE_WIDE_CHAR}; Connection::ConnPropertyMap::const_iterator TrackMissingRequiredProperty( const std::string_view& property, const Connection::ConnPropertyMap& properties, std::vector& missing_attr) { - auto prop_iter = properties.find(property); + auto prop_iter = properties.find(std::string(property)); if (properties.end() == prop_iter) { missing_attr.push_back(property); } @@ -149,7 +149,8 @@ std::shared_ptr LoadFlightSslConfigs( AsBool(connPropertyMap, FlightSqlConnection::USE_SYSTEM_TRUST_STORE) .value_or(SYSTEM_TRUST_STORE_DEFAULT); - auto trusted_certs_iterator = connPropertyMap.find(FlightSqlConnection::TRUSTED_CERTS); + auto trusted_certs_iterator = + connPropertyMap.find(std::string(FlightSqlConnection::TRUSTED_CERTS)); auto trusted_certs = trusted_certs_iterator != connPropertyMap.end() ? trusted_certs_iterator->second : ""; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h index 0ee6d5d5391..0a4b213229f 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h @@ -29,6 +29,13 @@ namespace driver { namespace flight_sql { +/// \brief Case insensitive comparator that takes string_view +struct CaseInsensitiveComparatorStrView { + bool operator()(const std::string_view& s1, const std::string_view& s2) const { + return boost::lexicographical_compare(s1, s2, boost::is_iless()); + } +}; + class FlightSqlSslConfig; /// \brief Create an instance of the FlightSqlSslConfig class, from the properties passed diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection_test.cc index 6a519138b63..a7a0fc10c29 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection_test.cc @@ -69,10 +69,12 @@ TEST(MetadataSettingsTest, StringColumnLengthTest) { const int32_t expected_string_column_length = 100000; const Connection::ConnPropertyMap properties = { - {FlightSqlConnection::HOST, std::string("localhost")}, // expect not used - {FlightSqlConnection::PORT, std::string("32010")}, // expect not used - {FlightSqlConnection::USE_ENCRYPTION, std::string("false")}, // expect not used - {FlightSqlConnection::STRING_COLUMN_LENGTH, + {std::string(FlightSqlConnection::HOST), + std::string("localhost")}, // expect not used + {std::string(FlightSqlConnection::PORT), std::string("32010")}, // expect not used + {std::string(FlightSqlConnection::USE_ENCRYPTION), + std::string("false")}, // expect not used + {std::string(FlightSqlConnection::STRING_COLUMN_LENGTH), std::to_string(expected_string_column_length)}, }; @@ -90,10 +92,10 @@ TEST(MetadataSettingsTest, UseWideCharTest) { connection.SetClosed(false); const Connection::ConnPropertyMap properties1 = { - {FlightSqlConnection::USE_WIDE_CHAR, std::string("true")}, + {std::string(FlightSqlConnection::USE_WIDE_CHAR), std::string("true")}, }; const Connection::ConnPropertyMap properties2 = { - {FlightSqlConnection::USE_WIDE_CHAR, std::string("false")}, + {std::string(FlightSqlConnection::USE_WIDE_CHAR), std::string("false")}, }; EXPECT_EQ(true, connection.GetUseWideChar(properties1)); @@ -105,9 +107,9 @@ TEST(MetadataSettingsTest, UseWideCharTest) { TEST(BuildLocationTests, ForTcp) { std::vector missing_attr; Connection::ConnPropertyMap properties = { - {FlightSqlConnection::HOST, std::string("localhost")}, - {FlightSqlConnection::PORT, std::string("32010")}, - {FlightSqlConnection::USE_ENCRYPTION, std::string("false")}, + {std::string(FlightSqlConnection::HOST), std::string("localhost")}, + {std::string(FlightSqlConnection::PORT), std::string("32010")}, + {std::string(FlightSqlConnection::USE_ENCRYPTION), std::string("false")}, }; const std::shared_ptr& ssl_config = @@ -117,8 +119,8 @@ TEST(BuildLocationTests, ForTcp) { FlightSqlConnection::BuildLocation(properties, missing_attr, ssl_config); const Location& actual_location2 = FlightSqlConnection::BuildLocation( { - {FlightSqlConnection::HOST, std::string("localhost")}, - {FlightSqlConnection::PORT, std::string("32011")}, + {std::string(FlightSqlConnection::HOST), std::string("localhost")}, + {std::string(FlightSqlConnection::PORT), std::string("32011")}, }, missing_attr, ssl_config); @@ -131,9 +133,9 @@ TEST(BuildLocationTests, ForTcp) { TEST(BuildLocationTests, ForTls) { std::vector missing_attr; Connection::ConnPropertyMap properties = { - {FlightSqlConnection::HOST, std::string("localhost")}, - {FlightSqlConnection::PORT, std::string("32010")}, - {FlightSqlConnection::USE_ENCRYPTION, std::string("1")}, + {std::string(FlightSqlConnection::HOST), std::string("localhost")}, + {std::string(FlightSqlConnection::PORT), std::string("32010")}, + {std::string(FlightSqlConnection::USE_ENCRYPTION), std::string("1")}, }; const std::shared_ptr& ssl_config = @@ -143,9 +145,9 @@ TEST(BuildLocationTests, ForTls) { FlightSqlConnection::BuildLocation(properties, missing_attr, ssl_config); Connection::ConnPropertyMap second_properties = { - {FlightSqlConnection::HOST, std::string("localhost")}, - {FlightSqlConnection::PORT, std::string("32011")}, - {FlightSqlConnection::USE_ENCRYPTION, std::string("1")}, + {std::string(FlightSqlConnection::HOST), std::string("localhost")}, + {std::string(FlightSqlConnection::PORT), std::string("32011")}, + {std::string(FlightSqlConnection::USE_ENCRYPTION), std::string("1")}, }; const std::shared_ptr& second_ssl_config = diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc index 1949d2f15ad..61a11252380 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc @@ -66,7 +66,7 @@ void FlightSqlDriver::RegisterLog() { odbcabstraction::PropertyMap propertyMap; driver::odbcabstraction::ReadConfigFile(propertyMap, CONFIG_FILE_NAME); - auto log_enable_iterator = propertyMap.find(SPDLogger::LOG_ENABLED); + auto log_enable_iterator = propertyMap.find(std::string(SPDLogger::LOG_ENABLED)); auto log_enabled = log_enable_iterator != propertyMap.end() ? odbcabstraction::AsBool(log_enable_iterator->second) : false; @@ -74,13 +74,13 @@ void FlightSqlDriver::RegisterLog() { return; } - auto log_path_iterator = propertyMap.find(SPDLogger::LOG_PATH); + auto log_path_iterator = propertyMap.find(std::string(SPDLogger::LOG_PATH)); auto log_path = log_path_iterator != propertyMap.end() ? log_path_iterator->second : ""; if (log_path.empty()) { return; } - auto log_level_iterator = propertyMap.find(SPDLogger::LOG_LEVEL); + auto log_level_iterator = propertyMap.find(std::string(SPDLogger::LOG_LEVEL)); auto log_level = ToLogLevel(log_level_iterator != propertyMap.end() ? std::stoi(log_level_iterator->second) : 1); @@ -88,12 +88,14 @@ void FlightSqlDriver::RegisterLog() { return; } - auto maximum_file_size_iterator = propertyMap.find(SPDLogger::MAXIMUM_FILE_SIZE); + auto maximum_file_size_iterator = + propertyMap.find(std::string(SPDLogger::MAXIMUM_FILE_SIZE)); auto maximum_file_size = maximum_file_size_iterator != propertyMap.end() ? std::stoi(maximum_file_size_iterator->second) : DEFAULT_MAXIMUM_FILE_SIZE; - auto maximum_file_quantity_iterator = propertyMap.find(SPDLogger::FILE_QUANTITY); + auto maximum_file_quantity_iterator = + propertyMap.find(std::string(SPDLogger::FILE_QUANTITY)); auto maximum_file_quantity = maximum_file_quantity_iterator != propertyMap.end() ? std::stoi(maximum_file_quantity_iterator->second) : 1; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_get_tables_reader.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_get_tables_reader.cc index ccd6058f8cd..b048d1984c5 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_get_tables_reader.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_get_tables_reader.cc @@ -80,9 +80,10 @@ std::shared_ptr GetTablesReader::GetSchema() { const arrow::Result>& result = arrow::ipc::ReadSchema(&dataset_schema_reader, &in_memo); if (!result.ok()) { - // TODO: Ignoring this error until we fix the problem on Dremio server - // The problem is that complex types columns are being returned without the children - // types. + // TODO: Test and build the driver against a server that returns + // complex types columns with the children + // types and handle the failure properly + // https://github.com/apache/arrow/issues/46561 return nullptr; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc index 0e250d1af9b..d3250401193 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc @@ -98,10 +98,10 @@ Result> Transform_inner( const auto& table_name = reader.GetTableName(); const std::shared_ptr& schema = reader.GetSchema(); if (schema == nullptr) { - // TODO: Remove this if after fixing TODO on GetTablesReader::GetSchema() - // This is because of a problem on Dremio server, where complex types columns - // are being returned without the children types, so we are simply ignoring - // it by now. + // TODO: Test and build the driver against a server that returns + // complex types columns with the children + // types and handle the failure properly. + // https://github.com/apache/arrow/issues/46561 continue; } for (int i = 0; i < schema->num_fields(); ++i) { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h index 69fa8a8696c..c7c9cc5b894 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h @@ -46,13 +46,6 @@ class Configuration { */ ~Configuration(); - /** - * Convert configure to connect string. - * - * @return Connect string. - */ - std::string ToConnectString() const; - void LoadDefaults(); void LoadDsn(const std::string& dsn); @@ -60,7 +53,7 @@ class Configuration { bool IsSet(const std::string_view& key) const; const std::string& Get(const std::string_view& key) const; void Set(const std::string_view& key, const std::string& value); - + void Emplace(const std::string_view& key, std::string&& value); /** * Get properties map. */ diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/main.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/main.cc index e112fdf67c0..aaf267cc268 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/main.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/main.cc @@ -43,7 +43,7 @@ using driver::odbcabstraction::Statement; void TestBindColumn(const std::shared_ptr& connection) { const std::shared_ptr& statement = connection->CreateStatement(); - statement->Execute("SELECT IncidntNum, Category FROM \"@dremio\".Test LIMIT 10"); + statement->Execute("SELECT IncidntNum, Category FROM \"@apache\".Test LIMIT 10"); const std::shared_ptr& result_set = statement->GetResultSet(); @@ -105,7 +105,7 @@ void TestBindColumnBigInt(const std::shared_ptr& connection) { " SELECT CONVERT_TO_INTEGER(IncidntNum, 1, 1, 0) AS IncidntNum, " "Category\n" " FROM (\n" - " SELECT IncidntNum, Category FROM \"@dremio\".Test LIMIT 10\n" + " SELECT IncidntNum, Category FROM \"@apache\".Test LIMIT 10\n" " ) nested_0\n" ") nested_0"); @@ -202,11 +202,11 @@ int main() { driver.CreateConnection(driver::odbcabstraction::V_3); Connection::ConnPropertyMap properties = { - {FlightSqlConnection::HOST, std::string("automaster.drem.io")}, - {FlightSqlConnection::PORT, std::string("32010")}, - {FlightSqlConnection::USER, std::string("dremio")}, - {FlightSqlConnection::PASSWORD, std::string("dremio123")}, - {FlightSqlConnection::USE_ENCRYPTION, std::string("false")}, + {std::string(FlightSqlConnection::HOST), std::string("automaster.apache")}, + {std::string(FlightSqlConnection::PORT), std::string("32010")}, + {std::string(FlightSqlConnection::USER), std::string("apache")}, + {std::string(FlightSqlConnection::PASSWORD), std::string("apache123")}, + {std::string(FlightSqlConnection::USE_ENCRYPTION), std::string("false")}, }; std::vector missing_attr; connection->Connect(properties, missing_attr); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc index 67a4c3db3d3..95b47bdb1e2 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc @@ -15,63 +15,16 @@ // specific language governing permissions and limitations // under the License. -// platform.h includes windows.h, so it needs to be included -// before winuser.h -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" +#include "arrow/flight/sql/odbc/flight_sql/system_dsn.h" -#include -#include #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" -#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/connection_string_parser.h" -#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/dsn_configuration_window.h" -#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h" -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" #include -#include -#include #include using driver::flight_sql::FlightSqlConnection; using driver::flight_sql::config::Configuration; -using driver::flight_sql::config::ConnectionStringParser; -using driver::flight_sql::config::DsnConfigurationWindow; -using driver::flight_sql::config::Result; -using driver::flight_sql::config::Window; - -BOOL CALLBACK ConfigDriver(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, - LPCSTR lpszArgs, LPSTR lpszMsg, WORD cbMsgMax, - WORD* pcbMsgOut) { - return false; -} - -bool DisplayConnectionWindow(void* windowParent, Configuration& config) { - HWND hwndParent = (HWND)windowParent; - - if (!hwndParent) return true; - - try { - Window parent(hwndParent); - DsnConfigurationWindow window(&parent, config); - - window.Create(); - - window.Show(); - window.Update(); - - return ProcessMessages(window) == Result::OK; - } catch (driver::odbcabstraction::DriverException& err) { - std::stringstream buf; - buf << "Message: " << err.GetMessageText() << ", Code: " << err.GetNativeError(); - std::string message = buf.str(); - MessageBox(NULL, message.c_str(), "Error!", MB_ICONEXCLAMATION | MB_OK); - - SQLPostInstallerError(err.GetNativeError(), err.GetMessageText().c_str()); - } - - return false; -} void PostLastInstallerError() { #define BUFFER_SIZE (1024) @@ -134,45 +87,3 @@ bool RegisterDsn(const Configuration& config, LPCSTR driver) { return true; } - -BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attributes) { - Configuration config; - ConnectionStringParser parser(config); - parser.ParseConfigAttributes(attributes); - - switch (req) { - case ODBC_ADD_DSN: { - config.LoadDefaults(); - if (!DisplayConnectionWindow(hwndParent, config) || !RegisterDsn(config, driver)) - return FALSE; - - break; - } - - case ODBC_CONFIG_DSN: { - const std::string& dsn = config.Get(FlightSqlConnection::DSN); - if (!SQLValidDSN(dsn.c_str())) return FALSE; - - Configuration loaded(config); - loaded.LoadDsn(dsn); - - if (!DisplayConnectionWindow(hwndParent, loaded) || !UnregisterDsn(dsn.c_str()) || - !RegisterDsn(loaded, driver)) - return FALSE; - - break; - } - - case ODBC_REMOVE_DSN: { - const std::string& dsn = config.Get(FlightSqlConnection::DSN); - if (!SQLValidDSN(dsn.c_str()) || !UnregisterDsn(dsn)) return FALSE; - - break; - } - - default: - return FALSE; - } - - return TRUE; -} diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h new file mode 100644 index 00000000000..535a063269e --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h @@ -0,0 +1,51 @@ +// 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. + +// platform.h includes windows.h, so it needs to be included first +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" + +using driver::flight_sql::config::Configuration; + +#if defined _WIN32 || defined _WIN64 +/** + * Display connection window for user to configure connection parameters. + * + * @param windowParent Parent window handle. + * @param config Output configuration. + * @return True on success and false on fail. + */ +bool DisplayConnectionWindow(void* windowParent, Configuration& config); +#endif + +/** + * Register DSN with specified configuration. + * + * @param config Configuration. + * @param driver Driver. + * @return True on success and false on fail. + */ +bool RegisterDsn(const Configuration& config, LPCSTR driver); + +/** + * Unregister specified DSN. + * + * @param dsn DSN name. + * @return True on success and false on fail. + */ +bool UnregisterDsn(const std::string& dsn); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc new file mode 100644 index 00000000000..a2bf2565610 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc @@ -0,0 +1,119 @@ +// 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. + +// platform.h includes windows.h, so it needs to be included +// before winuser.h +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + +#include +#include + +#include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/connection_string_parser.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/dsn_configuration_window.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h" +#include "arrow/flight/sql/odbc/flight_sql/system_dsn.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" + +#include +#include +#include +#include + +using driver::flight_sql::FlightSqlConnection; +using driver::flight_sql::config::Configuration; +using driver::flight_sql::config::ConnectionStringParser; +using driver::flight_sql::config::DsnConfigurationWindow; +using driver::flight_sql::config::Result; +using driver::flight_sql::config::Window; + +BOOL CALLBACK ConfigDriver(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, + LPCSTR lpszArgs, LPSTR lpszMsg, WORD cbMsgMax, + WORD* pcbMsgOut) { + return false; +} + +bool DisplayConnectionWindow(void* windowParent, Configuration& config) { + HWND hwndParent = (HWND)windowParent; + + if (!hwndParent) return true; + + try { + Window parent(hwndParent); + DsnConfigurationWindow window(&parent, config); + + window.Create(); + + window.Show(); + window.Update(); + + return ProcessMessages(window) == Result::OK; + } catch (const driver::odbcabstraction::DriverException& err) { + std::stringstream buf; + buf << "SQL State: " << err.GetSqlState() << ", Message: " << err.GetMessageText() + << ", Code: " << err.GetNativeError(); + std::string message = buf.str(); + MessageBox(NULL, message.c_str(), "Error!", MB_ICONEXCLAMATION | MB_OK); + + SQLPostInstallerError(err.GetNativeError(), err.GetMessageText().c_str()); + } + + return false; +} + +BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attributes) { + Configuration config; + ConnectionStringParser parser(config); + parser.ParseConfigAttributes(attributes); + + switch (req) { + case ODBC_ADD_DSN: { + config.LoadDefaults(); + if (!DisplayConnectionWindow(hwndParent, config) || !RegisterDsn(config, driver)) + return FALSE; + + break; + } + + case ODBC_CONFIG_DSN: { + const std::string& dsn = config.Get(FlightSqlConnection::DSN); + if (!SQLValidDSN(dsn.c_str())) return FALSE; + + Configuration loaded(config); + loaded.LoadDsn(dsn); + + if (!DisplayConnectionWindow(hwndParent, loaded) || !UnregisterDsn(dsn.c_str()) || + !RegisterDsn(loaded, driver)) + return FALSE; + + break; + } + + case ODBC_REMOVE_DSN: { + const std::string& dsn = config.Get(FlightSqlConnection::DSN); + if (!SQLValidDSN(dsn.c_str()) || !UnregisterDsn(dsn)) return FALSE; + + break; + } + + default: + return FALSE; + } + + return TRUE; +} diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index 2c93d183c92..dba68425f49 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -1,6 +1,6 @@ - LIBRARY arrow_flight_sql_odbc EXPORTS + ConfigDSN SQLAllocConnect SQLAllocEnv SQLAllocHandle @@ -38,4 +38,3 @@ EXPORTS SQLSetEnvAttr SQLSetStmtAttrW SQLTablesW - diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index a57a371044e..9d2df40c05a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -15,20 +15,30 @@ // specific language governing permissions and limitations // under the License. +// flight_sql_connection.h needs to be included first due to conflicts with windows.h +#include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" + #include #include -#include +#include #include #include #include +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" + +#if defined _WIN32 || defined _WIN64 +// For displaying DSN Window +# include "arrow/flight/sql/odbc/flight_sql/system_dsn.h" +#endif + // odbc_api includes windows.h, which needs to be put behind winsock2.h. // odbc_environment.h includes winsock2.h #include namespace arrow { SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { - // TODO: implement SQLAllocHandle by linking to `odbc_impl` *result = nullptr; switch (type) { @@ -103,11 +113,13 @@ SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { ODBCConnection* conn = reinterpret_cast(handle); - return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { - conn->releaseConnection(); + if (!conn) { + return SQL_INVALID_HANDLE; + } - return SQL_SUCCESS; - }); + conn->releaseConnection(); + + return SQL_SUCCESS; } case SQL_HANDLE_STMT: @@ -327,4 +339,117 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, } }); } + +SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion) { + // TODO: Implement FILEDSN and SAVEFILE keywords according to the spec + // https://github.com/apache/arrow/issues/46449 + + // TODO: Copy connection string properly in SQLDriverConnectW according to the + // spec https://github.com/apache/arrow/issues/46560 + + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + ODBCConnection* connection = reinterpret_cast(conn); + std::string connection_string = + ODBC::SqlWcharToString(inConnectionString, inConnectionStringLen); + Connection::ConnPropertyMap properties; + std::string dsn = + ODBCConnection::getPropertiesFromConnString(connection_string, properties); + + std::vector missing_properties; + + // TODO: Implement SQL_DRIVER_COMPLETE_REQUIRED in SQLDriverConnectW according to the + // spec https://github.com/apache/arrow/issues/46448 +#if defined _WIN32 || defined _WIN64 + if (driverCompletion == SQL_DRIVER_PROMPT || + ((driverCompletion == SQL_DRIVER_COMPLETE || + driverCompletion == SQL_DRIVER_COMPLETE_REQUIRED) && + !missing_properties.empty())) { + // TODO: implement driverCompletion behavior to display connection window. + } +#endif + + connection->connect(dsn, properties, missing_properties); + + // Copy connection string to outConnectionString after connection attempt + return ODBC::GetStringAttribute(true, connection_string, true, outConnectionString, + outConnectionStringBufferLen, outConnectionStringLen, + connection->GetDiagnostics()); + }); +} + +SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, + SQLSMALLINT passwordLen) { + using driver::flight_sql::FlightSqlConnection; + using driver::flight_sql::config::Configuration; + using ODBC::ODBCConnection; + + using ODBC::SqlWcharToString; + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + ODBCConnection* connection = reinterpret_cast(conn); + std::string dsn = SqlWcharToString(dsnName, dsnNameLen); + + Configuration config; + config.LoadDsn(dsn); + + if (userName) { + std::string uid = SqlWcharToString(userName, userNameLen); + config.Emplace(FlightSqlConnection::UID, std::move(uid)); + } + + if (password) { + std::string pwd = SqlWcharToString(password, passwordLen); + config.Emplace(FlightSqlConnection::PWD, std::move(pwd)); + } + + std::vector missing_properties; + + connection->connect(dsn, config.GetProperties(), missing_properties); + + return SQL_SUCCESS; + }); +} + +SQLRETURN SQLDisconnect(SQLHDBC conn) { + using ODBC::ODBCConnection; + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + ODBCConnection* connection = reinterpret_cast(conn); + + connection->disconnect(); + + return SQL_SUCCESS; + }); +} + +SQLRETURN SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, + SQLSMALLINT bufLen, SQLSMALLINT* length) { + // TODO: complete implementation of SQLGetInfoW and write tests + using ODBC::ODBCConnection; + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + ODBCConnection* connection = reinterpret_cast(conn); + + // Partially stubbed implementation of SQLGetInfoW + if (infoType == SQL_DRIVER_ODBC_VER) { + std::string_view ver("03.80"); + + return ODBC::GetStringAttribute(true, ver, true, infoValuePtr, bufLen, length, + connection->GetDiagnostics()); + } + + return static_cast(SQL_ERROR); + }); +} + } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 6c204fe3ae3..14bbddaa3ce 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -28,7 +28,6 @@ // @file odbc_api.h // // Define internal ODBC API function headers. - namespace arrow { SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result); SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle); @@ -40,4 +39,17 @@ SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr); SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER strLen); +SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion); +SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, + SQLSMALLINT passwordLen); +SQLRETURN SQLDisconnect(SQLHDBC conn); +SQLRETURN SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, + SQLSMALLINT bufLen, SQLSMALLINT* length); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h index 7b3b457a35f..7cf52b6cc1d 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h @@ -45,12 +45,12 @@ inline void GetAttribute(T attributeValue, SQLPOINTER output, O outputSize, } template -inline SQLRETURN GetAttributeUTF8(const std::string& attributeValue, SQLPOINTER output, - O outputSize, O* outputLenPtr) { +inline SQLRETURN GetAttributeUTF8(const std::string_view& attributeValue, + SQLPOINTER output, O outputSize, O* outputLenPtr) { if (output) { size_t outputLenBeforeNul = std::min(static_cast(attributeValue.size()), static_cast(outputSize - 1)); - memcpy(output, attributeValue.c_str(), outputLenBeforeNul); + memcpy(output, attributeValue.data(), outputLenBeforeNul); reinterpret_cast(output)[outputLenBeforeNul] = '\0'; } @@ -65,8 +65,8 @@ inline SQLRETURN GetAttributeUTF8(const std::string& attributeValue, SQLPOINTER } template -inline SQLRETURN GetAttributeUTF8(const std::string& attributeValue, SQLPOINTER output, - O outputSize, O* outputLenPtr, +inline SQLRETURN GetAttributeUTF8(const std::string_view& attributeValue, + SQLPOINTER output, O outputSize, O* outputLenPtr, driver::odbcabstraction::Diagnostics& diagnostics) { SQLRETURN result = GetAttributeUTF8(attributeValue, output, outputSize, outputLenPtr); if (SQL_SUCCESS_WITH_INFO == result) { @@ -76,7 +76,7 @@ inline SQLRETURN GetAttributeUTF8(const std::string& attributeValue, SQLPOINTER } template -inline SQLRETURN GetAttributeSQLWCHAR(const std::string& attributeValue, +inline SQLRETURN GetAttributeSQLWCHAR(const std::string_view& attributeValue, bool isLengthInBytes, SQLPOINTER output, O outputSize, O* outputLenPtr) { size_t result = @@ -95,7 +95,7 @@ inline SQLRETURN GetAttributeSQLWCHAR(const std::string& attributeValue, } template -inline SQLRETURN GetAttributeSQLWCHAR(const std::string& attributeValue, +inline SQLRETURN GetAttributeSQLWCHAR(const std::string_view& attributeValue, bool isLengthInBytes, SQLPOINTER output, O outputSize, O* outputLenPtr, driver::odbcabstraction::Diagnostics& diagnostics) { @@ -108,7 +108,8 @@ inline SQLRETURN GetAttributeSQLWCHAR(const std::string& attributeValue, } template -inline SQLRETURN GetStringAttribute(bool isUnicode, const std::string& attributeValue, +inline SQLRETURN GetStringAttribute(bool isUnicode, + const std::string_view& attributeValue, bool isLengthInBytes, SQLPOINTER output, O outputSize, O* outputLenPtr, driver::odbcabstraction::Diagnostics& diagnostics) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 01eae6f059a..23d9ca3e6d4 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -19,6 +19,9 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + #include #include #include @@ -34,10 +37,11 @@ namespace ODBC { using driver::odbcabstraction::DriverException; using driver::odbcabstraction::GetSqlWCharSize; using driver::odbcabstraction::Utf8ToWcs; +using driver::odbcabstraction::WcsToUtf8; // Return the number of bytes required for the conversion. template -inline size_t ConvertToSqlWChar(const std::string& str, SQLWCHAR* buffer, +inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, SQLLEN bufferSizeInBytes) { thread_local std::vector wstr; Utf8ToWcs(str.data(), str.size(), &wstr); @@ -63,7 +67,7 @@ inline size_t ConvertToSqlWChar(const std::string& str, SQLWCHAR* buffer, return valueLengthInBytes; } -inline size_t ConvertToSqlWChar(const std::string& str, SQLWCHAR* buffer, +inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, SQLLEN bufferSizeInBytes) { switch (GetSqlWCharSize()) { case sizeof(char16_t): @@ -77,4 +81,39 @@ inline size_t ConvertToSqlWChar(const std::string& str, SQLWCHAR* buffer, } } +/// \brief Convert buffer of SqlWchar to standard string +/// \param[in] wchar_msg SqlWchar to convert +/// \param[in] msg_len Number of characters in wchar_msg +/// \return wchar_msg in std::string format +inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLSMALLINT msg_len = SQL_NTS) { + if (wchar_msg == nullptr) { + return std::string(); + } + + thread_local std::vector utf8_str; + + if (msg_len == SQL_NTS) { + WcsToUtf8((void*)wchar_msg, &utf8_str); + } else { + WcsToUtf8((void*)wchar_msg, msg_len, &utf8_str); + } + + return std::string(utf8_str.begin(), utf8_str.end()); +} + +inline std::string SqlStringToString(const unsigned char* sqlStr, + int32_t sqlStrLen = SQL_NTS) { + std::string res; + + const char* sqlStrC = reinterpret_cast(sqlStr); + + if (!sqlStr) return res; + + if (sqlStrLen == SQL_NTS) + res.assign(sqlStrC); + else if (sqlStrLen > 0) + res.assign(sqlStrC, sqlStrLen); + + return res; +} } // namespace ODBC diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h index e771f467e6e..966110502f8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h @@ -41,6 +41,9 @@ class ODBCConnection : public ODBCHandle { ODBCConnection(const ODBCConnection&) = delete; ODBCConnection& operator=(const ODBCConnection&) = delete; + /// \brief Constructor for ODBCConnection. + /// \param[in] environment the parent environment. + /// \param[in] spiConnection the underlying spi connection. ODBCConnection(ODBCEnvironment& environment, std::shared_ptr spiConnection); @@ -48,6 +51,11 @@ class ODBCConnection : public ODBCHandle { const std::string& GetDSN() const; bool isConnected() const; + + /// \brief Connect to Arrow Flight SQL server. + /// \param[in] dsn the dsn name. + /// \param[in] properties the connection property map extracted from connection string. + /// \param[out] missing_properties report the properties that are missing void connect(std::string dsn, const driver::odbcabstraction::Connection::ConnPropertyMap& properties, std::vector& missing_properties); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h index 64b7e6a724b..ce86882c952 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h @@ -33,13 +33,13 @@ namespace odbcabstraction { /// \brief Case insensitive comparator struct CaseInsensitiveComparator { - bool operator()(const std::string_view& s1, const std::string_view& s2) const { + bool operator()(const std::string& s1, const std::string& s2) const { return boost::lexicographical_compare(s1, s2, boost::is_iless()); } }; // PropertyMap is case-insensitive for keys. -typedef std::map PropertyMap; +typedef std::map PropertyMap; class Statement; diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 4d5d4dc3656..f28ee1789b0 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -85,11 +85,11 @@ void loadPropertiesFromDSN(const std::string& dsn, for (auto& key : keys) { outputBuffer.clear(); outputBuffer.resize(BUFFER_SIZE, '\0'); - SQLGetPrivateProfileString(dsn.c_str(), key.data(), "", &outputBuffer[0], - BUFFER_SIZE, "odbc.ini"); + SQLGetPrivateProfileString(dsn.c_str(), key.data(), "", &outputBuffer[0], BUFFER_SIZE, + "odbc.ini"); std::string value = std::string(&outputBuffer[0]); - auto propIter = properties.find(key); + auto propIter = properties.find(std::string(key)); if (propIter == properties.end()) { properties.emplace(std::make_pair(std::move(key), std::move(value))); } @@ -759,7 +759,6 @@ std::string ODBCConnection::getPropertiesFromConnString( if (!isDsnFirst) { isDriverFirst = true; } - continue; } // Strip wrapping curly braces. diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc index f1d2d14744d..45dfbcf2e42 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc @@ -42,7 +42,7 @@ boost::optional AsBool(const std::string& value) { boost::optional AsBool(const Connection::ConnPropertyMap& connPropertyMap, const std::string_view& property_name) { - auto extracted_property = connPropertyMap.find(property_name); + auto extracted_property = connPropertyMap.find(std::string(property_name)); if (extracted_property != connPropertyMap.end()) { return AsBool(extracted_property->second); @@ -54,7 +54,7 @@ boost::optional AsBool(const Connection::ConnPropertyMap& connPropertyMap, boost::optional AsInt32(int32_t min_value, const Connection::ConnPropertyMap& connPropertyMap, const std::string_view& property_name) { - auto extracted_property = connPropertyMap.find(property_name); + auto extracted_property = connPropertyMap.find(std::string(property_name)); if (extracted_property != connPropertyMap.end()) { const int32_t stringColumnLength = std::stoi(extracted_property->second); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 161669b41df..1d0dce0bec4 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -19,8 +19,15 @@ add_custom_target(tests) include_directories(${ODBC_INCLUDE_DIRS}) +add_definitions(-DUNICODE=1) + add_arrow_test(connection_test SOURCES connection_test.cc + odbc_test_suite.cc + odbc_test_suite.h EXTRA_LINK_LIBS - ${ODBC_LIBRARIES}) + ${ODBC_LIBRARIES} + ${ODBCINST} + arrow_odbc_spi_impl + odbcabstraction) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/README b/cpp/src/arrow/flight/sql/odbc/tests/README new file mode 100644 index 00000000000..8e43296edff --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/README @@ -0,0 +1,4 @@ +Prior to running the tests, set environment variable `ARROW_FLIGHT_SQL_ODBC_CONN` +to a valid connection string. +A valid connection string looks like: +driver={Apache Arrow Flight SQL ODBC Driver};HOST=localhost;port=32010;pwd=myPassword;uid=myName;useEncryption=false; diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 7991ec40263..c6a39ccc361 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -14,6 +14,7 @@ // 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" #ifdef _WIN32 # include @@ -22,13 +23,13 @@ #include #include #include + #include "gtest/gtest.h" namespace arrow { namespace flight { namespace odbc { namespace integration_tests { - TEST(SQLAllocHandle, TestSQLAllocHandleEnv) { // ODBC Environment SQLHENV env; @@ -199,56 +200,49 @@ TEST(SQLSetEnvAttr, TestSQLSetEnvAttrODBCVersionInvalid) { EXPECT_TRUE(return_set == SQL_ERROR); } -TEST(SQLGetEnvAttr, TestSQLGetEnvAttrOutputNTS) { - // ODBC Environment - SQLHENV env; +TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrOutputNTS) { + connect(); SQLINTEGER output_nts; - // Allocate an environment handle - SQLRETURN return_env = SQLAllocEnv(&env); - - EXPECT_TRUE(return_env == SQL_SUCCESS); - SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, &output_nts, 0, 0); EXPECT_TRUE(return_get == SQL_SUCCESS); EXPECT_EQ(output_nts, SQL_TRUE); + + disconnect(); } -TEST(SQLGetEnvAttr, TestSQLGetEnvAttrGetLength) { +TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrGetLength) { + // Test is disabled because call to SQLGetEnvAttr is handled by the driver manager on + // Windows. This test case can be potentionally used on macOS/Linux GTEST_SKIP(); - // ODBC Environment - SQLHENV env; - - SQLINTEGER length; - // Allocate an environment handle - SQLRETURN return_env = SQLAllocEnv(&env); + connect(); - EXPECT_TRUE(return_env == SQL_SUCCESS); + SQLINTEGER length; SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, &length); EXPECT_TRUE(return_get == SQL_SUCCESS); EXPECT_EQ(length, sizeof(SQLINTEGER)); + + disconnect(); } -TEST(SQLGetEnvAttr, TestSQLGetEnvAttrNullValuePointer) { +TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrNullValuePointer) { + // Test is disabled because call to SQLGetEnvAttr is handled by the driver manager on + // Windows. This test case can be potentionally used on macOS/Linux GTEST_SKIP(); - // ODBC Environment - SQLHENV env; - - // Allocate an environment handle - SQLRETURN return_env = SQLAllocEnv(&env); - - EXPECT_TRUE(return_env == SQL_SUCCESS); + connect(); SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, nullptr); EXPECT_TRUE(return_get == SQL_ERROR); + + disconnect(); } TEST(SQLSetEnvAttr, TestSQLSetEnvAttrOutputNTSValid) { @@ -293,12 +287,466 @@ TEST(SQLSetEnvAttr, TestSQLSetEnvAttrNullValuePointer) { EXPECT_TRUE(return_env == SQL_SUCCESS); // Attempt to set using bad data pointer - SQLRETURN return_set = - SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0); + SQLRETURN return_set = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0); EXPECT_TRUE(return_set == SQL_ERROR); } +TEST(SQLDriverConnect, TestSQLDriverConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE] = L""; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Check that outstr has same content as connect_str + std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); + Connection::ConnPropertyMap out_properties; + Connection::ConnPropertyMap in_properties; + ODBC::ODBCConnection::getPropertiesFromConnString(out_connection_string, + out_properties); + ODBC::ODBCConnection::getPropertiesFromConnString(connect_str, in_properties); + EXPECT_TRUE(compareConnPropertyMap(out_properties, in_properties)); + + // Disconnect from ODBC + ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLDriverConnect, TestSQLDriverConnectInvalidUid) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + // TODO uncomment this check when SQLGetDiagRec is implemented + // VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + + // TODO: Check that outstr remains empty after SqlWcharToString + // is fixed to handle empty `outstr` + // std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); + // EXPECT_TRUE(out_connection_string.empty()); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLConnect, TestSQLConnect) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + + // Write connection string content into a DSN, + // must succeed before continuing + std::string uid(""), pwd(""); + ASSERT_TRUE(writeDSN(connect_str)); + + std::string dsn(TEST_DSN); + ASSERT_OK_AND_ASSIGN(std::wstring wdsn, arrow::util::UTF8ToWideString(dsn)); + ASSERT_OK_AND_ASSIGN(std::wstring wuid, arrow::util::UTF8ToWideString(uid)); + ASSERT_OK_AND_ASSIGN(std::wstring wpwd, arrow::util::UTF8ToWideString(pwd)); + std::vector dsn0(wdsn.begin(), wdsn.end()); + std::vector uid0(wuid.begin(), wuid.end()); + std::vector pwd0(wpwd.begin(), wpwd.end()); + + // Connecting to ODBC server. + ret = SQLConnect(conn, dsn0.data(), static_cast(dsn0.size()), uid0.data(), + static_cast(uid0.size()), pwd0.data(), + static_cast(pwd0.size())); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Remove DSN + EXPECT_TRUE(UnregisterDsn(dsn)); + + // Disconnect from ODBC + ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLConnect, TestSQLConnectInputUidPwd) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + + // Retrieve valid uid and pwd + Connection::ConnPropertyMap properties; + ODBC::ODBCConnection::getPropertiesFromConnString(connect_str, properties); + std::string uid_key("uid"); + std::string pwd_key("pwd"); + std::string uid = properties[uid_key]; + std::string pwd = properties[pwd_key]; + + // Write connection string content without uid and pwd into a DSN, + // must succeed before continuing + properties.erase(uid_key); + properties.erase(pwd_key); + ASSERT_TRUE(writeDSN(properties)); + + std::string dsn(TEST_DSN); + ASSERT_OK_AND_ASSIGN(std::wstring wdsn, arrow::util::UTF8ToWideString(dsn)); + ASSERT_OK_AND_ASSIGN(std::wstring wuid, arrow::util::UTF8ToWideString(uid)); + ASSERT_OK_AND_ASSIGN(std::wstring wpwd, arrow::util::UTF8ToWideString(pwd)); + std::vector dsn0(wdsn.begin(), wdsn.end()); + std::vector uid0(wuid.begin(), wuid.end()); + std::vector pwd0(wpwd.begin(), wpwd.end()); + + // Connecting to ODBC server. + ret = SQLConnect(conn, dsn0.data(), static_cast(dsn0.size()), uid0.data(), + static_cast(uid0.size()), pwd0.data(), + static_cast(pwd0.size())); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Remove DSN + EXPECT_TRUE(UnregisterDsn(dsn)); + + // Disconnect from ODBC + ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLConnect, TestSQLConnectInvalidUid) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + + // Retrieve valid uid and pwd + Connection::ConnPropertyMap properties; + ODBC::ODBCConnection::getPropertiesFromConnString(connect_str, properties); + std::string uid = properties[std::string("uid")]; + std::string pwd = properties[std::string("pwd")]; + + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + + // Write connection string content into a DSN, + // must succeed before continuing + ASSERT_TRUE(writeDSN(connect_str)); + + std::string dsn(TEST_DSN); + ASSERT_OK_AND_ASSIGN(std::wstring wdsn, arrow::util::UTF8ToWideString(dsn)); + ASSERT_OK_AND_ASSIGN(std::wstring wuid, arrow::util::UTF8ToWideString(uid)); + ASSERT_OK_AND_ASSIGN(std::wstring wpwd, arrow::util::UTF8ToWideString(pwd)); + std::vector dsn0(wdsn.begin(), wdsn.end()); + std::vector uid0(wuid.begin(), wuid.end()); + std::vector pwd0(wpwd.begin(), wpwd.end()); + + // Connecting to ODBC server. + ret = SQLConnect(conn, dsn0.data(), static_cast(dsn0.size()), uid0.data(), + static_cast(uid0.size()), pwd0.data(), + static_cast(pwd0.size())); + + // UID specified in DSN will take precedence, + // so connection still fails despite passing valid uid in SQLConnect call + EXPECT_TRUE(ret == SQL_ERROR); + + // TODO uncomment this check when SQLGetDiagRec is implemented + // VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + + // Remove DSN + EXPECT_TRUE(UnregisterDsn(dsn)); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLConnect, TestSQLConnectDSNPrecedence) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + + // Write connection string content into a DSN, + // must succeed before continuing + + // Pass incorrect uid and password to SQLConnect, they will be ignored + std::string uid("non_existent_id"), pwd("non_existent_password"); + ASSERT_TRUE(writeDSN(connect_str)); + + std::string dsn(TEST_DSN); + ASSERT_OK_AND_ASSIGN(std::wstring wdsn, arrow::util::UTF8ToWideString(dsn)); + ASSERT_OK_AND_ASSIGN(std::wstring wuid, arrow::util::UTF8ToWideString(uid)); + ASSERT_OK_AND_ASSIGN(std::wstring wpwd, arrow::util::UTF8ToWideString(pwd)); + std::vector dsn0(wdsn.begin(), wdsn.end()); + std::vector uid0(wuid.begin(), wuid.end()); + std::vector pwd0(wpwd.begin(), wpwd.end()); + + // Connecting to ODBC server. + ret = SQLConnect(conn, dsn0.data(), static_cast(dsn0.size()), uid0.data(), + static_cast(uid0.size()), pwd0.data(), + static_cast(pwd0.size())); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Remove DSN + EXPECT_TRUE(UnregisterDsn(dsn)); + + // Disconnect from ODBC + ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Attempt to disconnect without a connection, expect to fail + ret = SQLDisconnect(conn); + + EXPECT_TRUE(ret == SQL_ERROR); + + // Expect ODBC driver manager to return error state + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("08003")); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} } // namespace integration_tests } // namespace odbc } // namespace flight diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc new file mode 100644 index 00000000000..d60bcea19e4 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -0,0 +1,172 @@ +// 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. + +// For DSN registration. flight_sql_connection.h needs to included first due to conflicts +// with windows.h +#include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" + +#include "arrow/flight/sql/odbc/tests/odbc_test_suite.h" + +// For DSN registration +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" + +namespace arrow { +namespace flight { +namespace odbc { +namespace integration_tests { +void FlightSQLODBCTestBase::connect() { + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::vector connect_str0(connect_str.begin(), connect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + // Assert connection is successful before we continue + ASSERT_TRUE(ret == SQL_SUCCESS); +} + +void FlightSQLODBCTestBase::disconnect() { + // Disconnect from ODBC + SQLRETURN ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +bool compareConnPropertyMap(Connection::ConnPropertyMap map1, + Connection::ConnPropertyMap map2) { + if (map1.size() != map2.size()) return false; + + for (const auto& [key, value] : map1) { + if (value != map2[key]) return false; + } + + return true; +} + +void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, + std::string expected_state) { + using ODBC::SqlWcharToString; + + SQLWCHAR sql_state[7] = {}; + SQLINTEGER native_code; + + SQLWCHAR message[ODBC_BUFFER_SIZE] = {}; + SQLSMALLINT reallen = 0; + + // On Windows, reallen is in bytes. On Linux, reallen is in chars. + // So, not using reallen + SQLGetDiagRec(handle_type, handle, 1, sql_state, &native_code, message, + ODBC_BUFFER_SIZE, &reallen); + + std::string res = SqlWcharToString(sql_state); + + EXPECT_EQ(res, expected_state); +} + +std::string GetOdbcErrorMessage(SQLSMALLINT handle_type, SQLHANDLE handle) { + using ODBC::SqlWcharToString; + + SQLWCHAR sql_state[7] = {}; + SQLINTEGER native_code; + + SQLWCHAR message[ODBC_BUFFER_SIZE] = {}; + SQLSMALLINT reallen = 0; + + // On Windows, reallen is in bytes. On Linux, reallen is in chars. + // So, not using reallen + SQLGetDiagRec(handle_type, handle, 1, sql_state, &native_code, message, + ODBC_BUFFER_SIZE, &reallen); + + std::string res = SqlWcharToString(sql_state); + + if (res.empty() || !message[0]) { + res = "Cannot find ODBC error message"; + } else { + res.append(": ").append(SqlWcharToString(message)); + } + + return res; +} + +bool writeDSN(std::string connection_str) { + Connection::ConnPropertyMap properties; + + ODBC::ODBCConnection::getPropertiesFromConnString(connection_str, properties); + return writeDSN(properties); +} + +bool writeDSN(Connection::ConnPropertyMap properties) { + using driver::flight_sql::FlightSqlConnection; + using driver::flight_sql::config::Configuration; + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + + Configuration config; + config.Set(FlightSqlConnection::DSN, std::string(TEST_DSN)); + + for (const auto& [key, value] : properties) { + config.Set(key, value); + } + + std::string driver = config.Get(FlightSqlConnection::DRIVER); + + return RegisterDsn(config, driver.c_str()); +} +} // namespace integration_tests +} // namespace odbc +} // namespace flight +} // namespace arrow 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 new file mode 100644 index 00000000000..49ab2e20f44 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.h @@ -0,0 +1,90 @@ +// 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/testing/gtest_util.h" +#include "arrow/util/io_util.h" +#include "arrow/util/utf8.h" + +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" + +// For DSN registration +#include "arrow/flight/sql/odbc/flight_sql/system_dsn.h" + +#define TEST_CONNECT_STR "ARROW_FLIGHT_SQL_ODBC_CONN" +#define TEST_DSN "Apache Arrow Flight SQL Test DSN" + +namespace arrow { +namespace flight { +namespace odbc { +namespace integration_tests { +using driver::odbcabstraction::Connection; + +class FlightSQLODBCTestBase : public ::testing::Test { + public: + /// \brief Connect to Arrow Flight SQL server using connection string defined in + /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" + void connect(); + /// \brief Disconnect from server + void disconnect(); + + /** ODBC Environment. */ + SQLHENV env; + + /** ODBC Connect. */ + SQLHDBC conn; + + /** ODBC Statement. */ + SQLHSTMT stmt; +}; + +/** ODBC read buffer size. */ +enum { ODBC_BUFFER_SIZE = 1024 }; + +/// Compare ConnPropertyMap, key value is case-insensitive +bool compareConnPropertyMap(Connection::ConnPropertyMap map1, + Connection::ConnPropertyMap map2); + +/// Get error message from ODBC driver using SQLGetDiagRec +std::string GetOdbcErrorMessage(SQLSMALLINT handle_type, SQLHANDLE handle); + +/// Verify ODBC Error State +void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, + std::string expected_state); + +/// \brief Write connection string into DSN +/// \param[in] connection_str the connection string. +/// \return true on success +bool writeDSN(std::string connection_str); + +/// \brief Write properties map into DSN +/// \param[in] properties map. +/// \return true on success +bool writeDSN(Connection::ConnPropertyMap properties); +} // namespace integration_tests +} // namespace odbc +} // namespace flight +} // namespace arrow From 770cba60035f9e67121c170ecf4e723fa7dda130 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 22 May 2025 16:26:21 -0700 Subject: [PATCH 03/74] Add odbc.def and cmd file to rat_exclude --- dev/release/rat_exclude_files.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index 51c01516e7c..c8f45e89940 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -13,6 +13,8 @@ ci/vcpkg/*.patch CHANGELOG.md cpp/CHANGELOG_PARQUET.md cpp/src/arrow/c/dlpack_abi.h +cpp/src/arrow/flight/sql/odbc/odbc.def +cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd cpp/src/arrow/io/mman.h cpp/src/arrow/util/random.h cpp/src/arrow/status.cc From e9940576f1af7ca485a906ff7e7faa3389ed055a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 23 May 2025 16:45:00 -0700 Subject: [PATCH 04/74] Nit - remove duplicate lines Accidentally committed the change during git rebase --- .../include/odbcabstraction/odbc_impl/encoding_utils.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 23d9ca3e6d4..2d898066583 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -19,9 +19,6 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/encoding.h" -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" - #include #include #include From 2cff904bcf5a7c6f1258a99c88f2bb656ad3c9de Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 23 May 2025 16:59:39 -0700 Subject: [PATCH 05/74] Nit - remove usage of nullptr --- .../include/odbcabstraction/odbc_impl/encoding_utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 2d898066583..45ed8713626 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -83,7 +83,7 @@ inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, /// \param[in] msg_len Number of characters in wchar_msg /// \return wchar_msg in std::string format inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLSMALLINT msg_len = SQL_NTS) { - if (wchar_msg == nullptr) { + if (!wchar_msg) { return std::string(); } From 132ea6026d9a3730c4ded235e8ebf6420275f451 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 22 May 2025 16:46:48 -0700 Subject: [PATCH 06/74] DSN window implementation --- .../flight/sql/odbc/flight_sql/system_dsn.h | 14 +++++++ .../sql/odbc/flight_sql/win_system_dsn.cc | 18 +++++++- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 42 +++++++++++++++---- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h index 535a063269e..f5470693eea 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h @@ -21,6 +21,7 @@ #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" using driver::flight_sql::config::Configuration; +using driver::odbcabstraction::Connection; #if defined _WIN32 || defined _WIN64 /** @@ -31,6 +32,19 @@ using driver::flight_sql::config::Configuration; * @return True on success and false on fail. */ bool DisplayConnectionWindow(void* windowParent, Configuration& config); + +/** + * For SQLDriverConnect. + * Display connection window for user to configure connection parameters. + * + * @param windowParent Parent window handle. + * @param config Output configuration, presumed to be empty, it will be using values from + * properties. + * @param config Output properties. + * @return True on success and false on fail. + */ +bool DisplayConnectionWindow(void* windowParent, Configuration& config, + Connection::ConnPropertyMap& properties); #endif /** diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc index a2bf2565610..5c8d116a7bc 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc @@ -41,6 +41,7 @@ using driver::flight_sql::config::ConnectionStringParser; using driver::flight_sql::config::DsnConfigurationWindow; using driver::flight_sql::config::Result; using driver::flight_sql::config::Window; +using driver::odbcabstraction::DriverException; BOOL CALLBACK ConfigDriver(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, LPCSTR lpszArgs, LPSTR lpszMsg, WORD cbMsgMax, @@ -63,7 +64,7 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config) { window.Update(); return ProcessMessages(window) == Result::OK; - } catch (const driver::odbcabstraction::DriverException& err) { + } catch (const DriverException& err) { std::stringstream buf; buf << "SQL State: " << err.GetSqlState() << ", Message: " << err.GetMessageText() << ", Code: " << err.GetNativeError(); @@ -76,6 +77,21 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config) { return false; } +bool DisplayConnectionWindow(void* windowParent, Configuration& config, + Connection::ConnPropertyMap& properties) { + for (const auto& [key, value] : properties) { + config.Set(key, value); + } + + if (DisplayConnectionWindow(windowParent, config)) { + properties = config.GetProperties(); + return true; + } else { + // TODO: log cancelled dialog after logging is enabled. + return false; + } +} + BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attributes) { Configuration config; ConnectionStringParser parser(config); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 9d2df40c05a..9368d6b9b30 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -354,6 +354,7 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, // spec https://github.com/apache/arrow/issues/46560 using driver::odbcabstraction::Connection; + using driver::odbcabstraction::DriverException; using ODBC::ODBCConnection; return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { @@ -369,16 +370,41 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, // TODO: Implement SQL_DRIVER_COMPLETE_REQUIRED in SQLDriverConnectW according to the // spec https://github.com/apache/arrow/issues/46448 #if defined _WIN32 || defined _WIN64 - if (driverCompletion == SQL_DRIVER_PROMPT || - ((driverCompletion == SQL_DRIVER_COMPLETE || - driverCompletion == SQL_DRIVER_COMPLETE_REQUIRED) && - !missing_properties.empty())) { - // TODO: implement driverCompletion behavior to display connection window. + // Load the DSN window according to driverCompletion + if (driverCompletion == SQL_DRIVER_PROMPT) { + // Load DSN window before first attempt to connect + driver::flight_sql::config::Configuration config; + if (!DisplayConnectionWindow(windowHandle, config, properties)) { + return static_cast(SQL_NO_DATA); + } + connection->connect(dsn, properties, missing_properties); + } else if (driverCompletion == SQL_DRIVER_COMPLETE || + driverCompletion == SQL_DRIVER_COMPLETE_REQUIRED) { + try { + connection->connect(dsn, properties, missing_properties); + } catch (const DriverException&) { + // If first connection fails due to missing attributes, load + // the DSN window and try to connect again + if (!missing_properties.empty()) { + driver::flight_sql::config::Configuration config; + missing_properties.clear(); + + if (!DisplayConnectionWindow(windowHandle, config, properties)) { + return static_cast(SQL_NO_DATA); + } + connection->connect(dsn, properties, missing_properties); + } else { + throw; + } + } + } else { + // Default case: attempt connection without showing DSN window + connection->connect(dsn, properties, missing_properties); } -#endif - +#else + // Attempt connection without loading DSN window on macOS/Linux connection->connect(dsn, properties, missing_properties); - +#endif // Copy connection string to outConnectionString after connection attempt return ODBC::GetStringAttribute(true, connection_string, true, outConnectionString, outConnectionStringBufferLen, outConnectionStringLen, From 18fc2e50d71a295f2b652ef9303600f648755de9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 26 May 2025 16:07:29 -0700 Subject: [PATCH 07/74] Add licenses to `.cmd` and `.def` files --- .../flight/sql/odbc/install/install_amd64.cmd | 17 +++++++++++++++++ cpp/src/arrow/flight/sql/odbc/odbc.def | 17 +++++++++++++++++ dev/release/rat_exclude_files.txt | 2 -- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd b/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd index fe365d59b90..b1fd85d578e 100644 --- a/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd +++ b/cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd @@ -1,3 +1,20 @@ +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. + @echo off set ODBC_AMD64=%1 diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index dba68425f49..c90c181d7c1 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -1,3 +1,20 @@ +; 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. + LIBRARY arrow_flight_sql_odbc EXPORTS ConfigDSN diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index c8f45e89940..51c01516e7c 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -13,8 +13,6 @@ ci/vcpkg/*.patch CHANGELOG.md cpp/CHANGELOG_PARQUET.md cpp/src/arrow/c/dlpack_abi.h -cpp/src/arrow/flight/sql/odbc/odbc.def -cpp/src/arrow/flight/sql/odbc/install/install_amd64.cmd cpp/src/arrow/io/mman.h cpp/src/arrow/util/random.h cpp/src/arrow/status.cc From ab2bae4c0643b867b10e0737913ddfa40e66c497 Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 27 May 2025 16:19:21 -0700 Subject: [PATCH 08/74] Implement SQLGetDiagRec --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 16 ++-- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 84 ++++++++++++++++++- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 4 + .../flight/sql/odbc/tests/connection_test.cc | 82 +++++++++++++++++- 4 files changed, 172 insertions(+), 14 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 0fc55720938..46536b84e9b 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -59,6 +59,14 @@ SQLRETURN SQL_API SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, diagInfoPtr, bufferLength, stringLengthPtr); } +SQLRETURN SQL_API SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLWCHAR* sqlState, + SQLINTEGER* nativeErrorPtr, SQLWCHAR* messageText, + SQLSMALLINT bufferLength, SQLSMALLINT* textLengthPtr) { + return arrow::SQLGetDiagRecW(handleType, handle, recNumber, sqlState, nativeErrorPtr, + messageText, bufferLength, textLengthPtr); +} + SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr) { return arrow::SQLGetEnvAttr(env, attr, valuePtr, bufferLen, strLenPtr); @@ -81,14 +89,6 @@ SQLRETURN SQL_API SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, return arrow::SQLGetInfoW(conn, infoType, infoValuePtr, bufLen, length); } -SQLRETURN SQL_API SQLGetDiagRecW(SQLSMALLINT type, SQLHANDLE handle, SQLSMALLINT recNum, - SQLWCHAR* sqlState, SQLINTEGER* nativeError, - SQLWCHAR* msgBuffer, SQLSMALLINT msgBufferLen, - SQLSMALLINT* msgLen) { - // TODO implement SQLGetDiagRecW - return SQL_ERROR; -} - SQLRETURN SQL_API SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, SQLWCHAR* inConnectionString, SQLSMALLINT inConnectionStringLen, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 9368d6b9b30..bf484510ccf 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -230,13 +230,93 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, stringLengthPtr, *diagnostics); } - default: - return SQL_ERROR; + default: { + // TODO Return correct dummy values + return SQL_SUCCESS; + } } return SQL_ERROR; } +SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, + SQLWCHAR* messageText, SQLSMALLINT bufferLength, + SQLSMALLINT* textLengthPtr) { + using driver::odbcabstraction::Diagnostics; + using ODBC::ConvertToSqlWChar; + using ODBC::GetStringAttribute; + using ODBC::ODBCConnection; + using ODBC::ODBCEnvironment; + + if (!handle) { + return SQL_INVALID_HANDLE; + } + + // Record number must be greater or equal to 1 + if (recNumber < 1 || bufferLength < 0) { + return SQL_ERROR; + } + + // Set character type to be Unicode by default + const bool isUnicode = true; + Diagnostics* diagnostics = nullptr; + + switch (handleType) { + case SQL_HANDLE_ENV: { + auto* environment = ODBCEnvironment::of(handle); + diagnostics = &environment->GetDiagnostics(); + break; + } + + case SQL_HANDLE_DBC: { + auto* connection = ODBCConnection::of(handle); + diagnostics = &connection->GetDiagnostics(); + break; + } + + case SQL_HANDLE_DESC: { + return SQL_ERROR; + } + + case SQL_HANDLE_STMT: { + return SQL_ERROR; + } + + default: + return SQL_INVALID_HANDLE; + } + + if (!diagnostics) { + return SQL_ERROR; + } + + // Convert from ODBC 1 based record number to internal diagnostics 0 indexed storage + const size_t recordIndex = static_cast(recNumber - 1); + if (!diagnostics->HasRecord(recordIndex)) { + return SQL_NO_DATA; + } + + if (sqlState) { + // The length of the sql state is always 5 characters plus null + SQLSMALLINT size = 6; + const std::string& state = diagnostics->GetSQLState(recordIndex); + GetStringAttribute(isUnicode, state, false, sqlState, size, &size, *diagnostics); + } + + if (nativeErrorPtr) { + *nativeErrorPtr = diagnostics->GetNativeError(recordIndex); + } + + if (messageText || textLengthPtr) { + const std::string& message = diagnostics->GetMessageText(recordIndex); + return GetStringAttribute(isUnicode, message, false, messageText, bufferLength, + textLengthPtr, *diagnostics); + } + + return SQL_SUCCESS; +} + SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr) { using driver::odbcabstraction::DriverException; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 14bbddaa3ce..d5c392bcf59 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -35,6 +35,10 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr); +SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, + SQLWCHAR* messageText, SQLSMALLINT bufferLength, + SQLSMALLINT* textLengthPtr); SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr); SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index c6a39ccc361..039a5fd074e 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -400,8 +400,7 @@ TEST(SQLDriverConnect, TestSQLDriverConnectInvalidUid) { EXPECT_TRUE(ret == SQL_ERROR); - // TODO uncomment this check when SQLGetDiagRec is implemented - // VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); // TODO: Check that outstr remains empty after SqlWcharToString // is fixed to handle empty `outstr` @@ -621,8 +620,7 @@ TEST(SQLConnect, TestSQLConnectInvalidUid) { // so connection still fails despite passing valid uid in SQLConnect call EXPECT_TRUE(ret == SQL_ERROR); - // TODO uncomment this check when SQLGetDiagRec is implemented - // VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); // Remove DSN EXPECT_TRUE(UnregisterDsn(dsn)); @@ -747,6 +745,82 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { EXPECT_TRUE(ret == SQL_SUCCESS); } + +TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + SQLWCHAR sql_state[6]; + SQLINTEGER native_error; + SQLWCHAR message[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_length; + + ret = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, sql_state, &native_error, message, + ODBC_BUFFER_SIZE, &message_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + EXPECT_TRUE(message_length > 200); + + EXPECT_TRUE(native_error == 200); + + // 28000 + EXPECT_TRUE(sql_state[0] == '2'); + EXPECT_TRUE(sql_state[1] == '8'); + EXPECT_TRUE(sql_state[2] == '0'); + EXPECT_TRUE(sql_state[3] == '0'); + EXPECT_TRUE(sql_state[4] == '0'); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + } // namespace integration_tests } // namespace odbc } // namespace flight From 822f5c02d25779de0cf6c4b11f6541785654fe61 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 26 May 2025 16:20:18 -0700 Subject: [PATCH 09/74] Build ODBC in Windows Workflow Tests are skipped for now. --- ci/scripts/cpp_build.sh | 2 ++ ci/scripts/cpp_test.sh | 2 ++ cpp/cmake_modules/ThirdpartyToolchain.cmake | 2 +- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 5 +++++ cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt | 8 +++++--- .../flight_sql/accessors/string_array_accessor_test.cc | 1 - cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h | 1 + .../arrow/flight/sql/odbc/flight_sql/system_trust_store.h | 3 +++ .../arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc | 5 +++-- .../arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt | 3 --- 10 files changed, 22 insertions(+), 10 deletions(-) diff --git a/ci/scripts/cpp_build.sh b/ci/scripts/cpp_build.sh index 8df5ec2b2d0..3197426eaba 100755 --- a/ci/scripts/cpp_build.sh +++ b/ci/scripts/cpp_build.sh @@ -64,6 +64,7 @@ if [ "${ARROW_ENABLE_THREADING:-ON}" = "OFF" ]; then ARROW_AZURE=OFF ARROW_FLIGHT=OFF ARROW_FLIGHT_SQL=OFF + ARROW_FLIGHT_SQL_ODBC=OFF ARROW_GCS=OFF ARROW_JEMALLOC=OFF ARROW_MIMALLOC=OFF @@ -206,6 +207,7 @@ else -DARROW_FILESYSTEM=${ARROW_FILESYSTEM:-ON} \ -DARROW_FLIGHT=${ARROW_FLIGHT:-OFF} \ -DARROW_FLIGHT_SQL=${ARROW_FLIGHT_SQL:-OFF} \ + -DARROW_FLIGHT_SQL_ODBC=${ARROW_FLIGHT_SQL_ODBC:-OFF} \ -DARROW_FUZZING=${ARROW_FUZZING:-OFF} \ -DARROW_GANDIVA_PC_CXX_FLAGS=${ARROW_GANDIVA_PC_CXX_FLAGS:-} \ -DARROW_GANDIVA=${ARROW_GANDIVA:-OFF} \ diff --git a/ci/scripts/cpp_test.sh b/ci/scripts/cpp_test.sh index 88c06849c8b..d761491bdd1 100755 --- a/ci/scripts/cpp_test.sh +++ b/ci/scripts/cpp_test.sh @@ -73,6 +73,8 @@ case "$(uname)" in exclude_tests="${exclude_tests}|gandiva-precompiled-test" exclude_tests="${exclude_tests}|gandiva-projector-test" exclude_tests="${exclude_tests}|gandiva-utf8-test" + # TODO: Enable ODBC tests + exclude_tests="${exclude_tests}|arrow-connection-test" ctest_options+=(--exclude-regex "${exclude_tests}") ;; *) diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake b/cpp/cmake_modules/ThirdpartyToolchain.cmake index 6e7544a707d..8a319b02c57 100644 --- a/cpp/cmake_modules/ThirdpartyToolchain.cmake +++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake @@ -1269,7 +1269,7 @@ if(ARROW_USE_BOOST) endif() if(ARROW_BOOST_REQUIRE_LIBRARY) set(ARROW_BOOST_COMPONENTS filesystem system) - if(ARROW_FLIGHT_SQL_ODBC AND MSVC) + if(ARROW_FLIGHT_SQL_ODBC) list(APPEND ARROW_BOOST_COMPONENTS locale) endif() if(ARROW_ENABLE_THREADING) diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 1f7c5d1f61f..449db0fedf4 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -17,9 +17,14 @@ add_custom_target(arrow_flight_sql_odbc) +# Ensure fmt is loaded as header only +add_compile_definitions(FMT_HEADER_ONLY) + if(WIN32) if(MSVC_VERSION GREATER_EQUAL 1900) set(ODBCINST legacy_stdio_definitions odbccp32 shlwapi) + elseif(MINGW) + set(ODBCINST odbccp32 shlwapi) endif() elseif(APPLE) set(ODBCINST iodbcinst) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt index 6985f781b9a..e9f282e91f9 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt @@ -101,10 +101,12 @@ if(WIN32) win_system_dsn.cc) endif() -target_link_libraries(arrow_odbc_spi_impl PUBLIC odbcabstraction arrow_flight_sql_shared) +target_link_libraries(arrow_odbc_spi_impl PUBLIC odbcabstraction arrow_flight_sql_shared + Boost::locale) -if(MSVC) - target_link_libraries(arrow_odbc_spi_impl PUBLIC Boost::locale) +# Link libraries on MINGW64 only +if(MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_libraries(arrow_odbc_spi_impl PUBLIC ${ODBCINST}) endif() set_target_properties(arrow_odbc_spi_impl diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/string_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/string_array_accessor_test.cc index 8b568bbffcf..587e7d5eb1c 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/string_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/string_array_accessor_test.cc @@ -134,7 +134,6 @@ TEST(StringArrayAccessor, Test_CDataType_WCHAR_Truncation) { ColumnBinding binding(odbcabstraction::CDataType_WCHAR, 0, 0, buffer.data(), max_strlen, strlen_buffer.data()); - std::basic_stringstream ss; int64_t value_offset = 0; // Construct the whole string by concatenating smaller chunks from diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h index 312d5689a98..2609abd6af7 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h @@ -21,6 +21,7 @@ #include #include +#include #if !_WIN32 # include #endif diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h index 71175b09709..f8e02fea526 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h @@ -24,6 +24,9 @@ # include # include + +# include + # include # include diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc index e79e1221e78..8f660a21329 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc @@ -23,6 +23,7 @@ #include #include +#include #include #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" @@ -53,7 +54,7 @@ LRESULT CALLBACK CustomWindow::WndProc(HWND hwnd, UINT msg, WPARAM wParam, switch (msg) { case WM_NCCREATE: { - _ASSERT(lParam != NULL); + assert(lParam != NULL); CREATESTRUCT* createStruct = reinterpret_cast(lParam); @@ -65,7 +66,7 @@ LRESULT CALLBACK CustomWindow::WndProc(HWND hwnd, UINT msg, WPARAM wParam, } case WM_CREATE: { - _ASSERT(window != NULL); + assert(window != NULL); window->SetHandle(hwnd); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt index c9614b88a5b..e1e52492648 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt @@ -17,9 +17,6 @@ include_directories(include) -# Ensure fmt is loaded as header only -add_compile_definitions(FMT_HEADER_ONLY) - add_library(odbcabstraction include/odbcabstraction/calendar_utils.h include/odbcabstraction/diagnostics.h From 4512ac53c374a083c542817cfedc8e1ffbf088c1 Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 4 Jun 2025 13:17:35 -0700 Subject: [PATCH 10/74] Updates for SQLGetDiagFieldW --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 180 +++++++++++++--- .../odbc_impl/attribute_utils.h | 10 +- .../flight/sql/odbc/tests/connection_test.cc | 201 +++++++++++++++++- 3 files changed, 347 insertions(+), 44 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index bf484510ccf..5f54a5d714a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -135,10 +135,26 @@ SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { return SQL_ERROR; } +inline bool IsValidStringFieldArgs(SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr, bool isUnicode) { + const SQLSMALLINT charSize = isUnicode ? GetSqlWCharSize() : sizeof(char); + const bool hasValidBuffer = + diagInfoPtr && bufferLength >= 0 && bufferLength % charSize == 0; + + // regardless of capacity return false if invalid + if (diagInfoPtr && !hasValidBuffer) { + return false; + } + + return hasValidBuffer || stringLengthPtr; +} + SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { + // TODO: Implement additional fields types + // https://github.com/apache/arrow/issues/46573 using driver::odbcabstraction::Diagnostics; using ODBC::GetStringAttribute; using ODBC::ODBCConnection; @@ -148,12 +164,18 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, return SQL_INVALID_HANDLE; } - if (!diagInfoPtr) { + if (!diagInfoPtr && !stringLengthPtr) { return SQL_ERROR; } - // Set character type to be Unicode by defualt (not Ansi) - bool isUnicode = true; + // If buffer length derived from null terminated string + if (diagInfoPtr && bufferLength == SQL_NTS) { + const wchar_t* str = reinterpret_cast(diagInfoPtr); + bufferLength = wcslen(str) * driver::odbcabstraction::GetSqlWCharSize(); + } + + // Set character type to be Unicode by default + const bool isUnicode = true; Diagnostics* diagnostics = nullptr; switch (handleType) { @@ -169,6 +191,14 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, break; } + case SQL_HANDLE_DESC: { + return SQL_ERROR; + } + + case SQL_HANDLE_STMT: { + return SQL_ERROR; + } + default: return SQL_ERROR; } @@ -177,32 +207,46 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, return SQL_ERROR; } - // Retrieve header level diagnostics if Record 0 specified - if (recNumber == 0) { - switch (diagIdentifier) { - case SQL_DIAG_NUMBER: { - SQLINTEGER count = static_cast(diagnostics->GetRecordCount()); - *static_cast(diagInfoPtr) = count; - if (stringLengthPtr) { - *stringLengthPtr = sizeof(SQLINTEGER); - } + // Retrieve and return if header level diagnostics + switch (diagIdentifier) { + case SQL_DIAG_NUMBER: { + if (diagInfoPtr) { + *static_cast(diagInfoPtr) = + static_cast(diagnostics->GetRecordCount()); + } - return SQL_SUCCESS; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); } - case SQL_DIAG_SERVER_NAME: { - const std::string source = diagnostics->GetDataSourceComponent(); - return GetStringAttribute(isUnicode, source, false, diagInfoPtr, bufferLength, - stringLengthPtr, *diagnostics); + return SQL_SUCCESS; + } + + // TODO implement return code function + case SQL_DIAG_RETURNCODE: { + return SQL_SUCCESS; + } + + // TODO Implement statement header functions + case SQL_DIAG_CURSOR_ROW_COUNT: + case SQL_DIAG_DYNAMIC_FUNCTION: + case SQL_DIAG_DYNAMIC_FUNCTION_CODE: + case SQL_DIAG_ROW_COUNT: { + if (handleType == SQL_HANDLE_STMT) { + return SQL_SUCCESS; } - default: - return SQL_ERROR; + return SQL_ERROR; } } + // If not a diagnostic header field then the record number must be 1 or greater + if (recNumber < 1) { + return SQL_ERROR; + } + // Retrieve record level diagnostics from specified 1 based record - uint32_t recordIndex = static_cast(recNumber - 1); + const uint32_t recordIndex = static_cast(recNumber - 1); if (!diagnostics->HasRecord(recordIndex)) { return SQL_NO_DATA; } @@ -210,13 +254,20 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, // Retrieve record field data switch (diagIdentifier) { case SQL_DIAG_MESSAGE_TEXT: { - const std::string message = diagnostics->GetMessageText(recordIndex); - return GetStringAttribute(isUnicode, message, false, diagInfoPtr, bufferLength, - stringLengthPtr, *diagnostics); + if (IsValidStringFieldArgs(diagInfoPtr, bufferLength, stringLengthPtr, isUnicode)) { + const std::string& message = diagnostics->GetMessageText(recordIndex); + return GetStringAttribute(isUnicode, message, true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + return SQL_ERROR; } case SQL_DIAG_NATIVE: { - *static_cast(diagInfoPtr) = diagnostics->GetNativeError(recordIndex); + if (diagInfoPtr) { + *static_cast(diagInfoPtr) = diagnostics->GetNativeError(recordIndex); + } + if (stringLengthPtr) { *stringLengthPtr = sizeof(SQLINTEGER); } @@ -224,16 +275,85 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, return SQL_SUCCESS; } + case SQL_DIAG_SERVER_NAME: { + if (IsValidStringFieldArgs(diagInfoPtr, bufferLength, stringLengthPtr, isUnicode)) { + switch (handleType) { + case SQL_HANDLE_DBC: { + ODBCConnection* connection = reinterpret_cast(handle); + std::string dsn = connection->GetDSN(); + return GetStringAttribute(isUnicode, dsn, true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + case SQL_HANDLE_DESC: { + // TODO Implement for case of descriptor + return SQL_ERROR; + } + + case SQL_HANDLE_STMT: { + // TODO Implement for case of statement + return SQL_ERROR; + } + + default: + return SQL_ERROR; + } + } + + return SQL_ERROR; + } + case SQL_DIAG_SQLSTATE: { - const std::string state = diagnostics->GetSQLState(recordIndex); - return GetStringAttribute(isUnicode, state, false, diagInfoPtr, bufferLength, - stringLengthPtr, *diagnostics); + if (IsValidStringFieldArgs(diagInfoPtr, bufferLength, stringLengthPtr, isUnicode)) { + const std::string& state = diagnostics->GetSQLState(recordIndex); + return GetStringAttribute(isUnicode, state, true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + return SQL_ERROR; } - default: { - // TODO Return correct dummy values + // Return valid dummy variable for unimplemented field + case SQL_DIAG_COLUMN_NUMBER: { + if (diagInfoPtr) { + *static_cast(diagInfoPtr) = SQL_NO_COLUMN_NUMBER; + } + + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); + } + + return SQL_SUCCESS; + } + + // Return empty string dummy variable for unimplemented fields + case SQL_DIAG_CLASS_ORIGIN: + case SQL_DIAG_CONNECTION_NAME: + case SQL_DIAG_SUBCLASS_ORIGIN: { + if (IsValidStringFieldArgs(diagInfoPtr, bufferLength, stringLengthPtr, isUnicode)) { + return GetStringAttribute(isUnicode, "", true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + } + + return SQL_ERROR; + } + + // Return valid dummy variable for unimplemented field + case SQL_DIAG_ROW_NUMBER: { + if (diagInfoPtr) { + *static_cast(diagInfoPtr) = SQL_NO_ROW_NUMBER; + } + + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLLEN); + } + return SQL_SUCCESS; } + + default: { + return SQL_ERROR; + } } return SQL_ERROR; @@ -486,7 +606,7 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, connection->connect(dsn, properties, missing_properties); #endif // Copy connection string to outConnectionString after connection attempt - return ODBC::GetStringAttribute(true, connection_string, true, outConnectionString, + return ODBC::GetStringAttribute(true, connection_string, false, outConnectionString, outConnectionStringBufferLen, outConnectionStringLen, connection->GetDiagnostics()); }); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h index 7cf52b6cc1d..d194ace237f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h @@ -79,16 +79,20 @@ template inline SQLRETURN GetAttributeSQLWCHAR(const std::string_view& attributeValue, bool isLengthInBytes, SQLPOINTER output, O outputSize, O* outputLenPtr) { - size_t result = + size_t length = ConvertToSqlWChar(attributeValue, reinterpret_cast(output), isLengthInBytes ? outputSize : outputSize * GetSqlWCharSize()); + if (!isLengthInBytes) { + length = length / GetSqlWCharSize(); + } + if (outputLenPtr) { - *outputLenPtr = static_cast(isLengthInBytes ? result : result / GetSqlWCharSize()); + *outputLenPtr = static_cast(length); } if (output && - outputSize < static_cast(result + (isLengthInBytes ? GetSqlWCharSize() : 1))) { + outputSize < static_cast(length + (isLengthInBytes ? GetSqlWCharSize() : 1))) { return SQL_SUCCESS_WITH_INFO; } return SQL_SUCCESS; diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 039a5fd074e..b2866b3e5a7 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -746,7 +746,7 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { +TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailure) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -785,9 +785,188 @@ TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { EXPECT_TRUE(ret == SQL_ERROR); - if (ret != SQL_SUCCESS) { - std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; - } + // Retrieve all supported header level and record level data + SQLSMALLINT HEADER_LEVEL = 0; + SQLSMALLINT RECORD_1 = 1; + + // SQL_DIAG_NUMBER + SQLINTEGER diag_number; + SQLSMALLINT diag_number_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, HEADER_LEVEL, SQL_DIAG_NUMBER, &diag_number, + sizeof(SQLINTEGER), &diag_number_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + EXPECT_EQ(diag_number, 1); + + // SQL_DIAG_SERVER_NAME + SQLWCHAR server_name[ODBC_BUFFER_SIZE]; + SQLSMALLINT server_name_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SERVER_NAME, server_name, + ODBC_BUFFER_SIZE, &server_name_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // SQL_DIAG_MESSAGE_TEXT + SQLWCHAR message_text[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_text_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, + message_text, ODBC_BUFFER_SIZE, &message_text_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + EXPECT_GT(message_text_length, 100); + + // SQL_DIAG_NATIVE + SQLINTEGER diag_native; + SQLSMALLINT diag_native_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_NATIVE, &diag_native, + sizeof(diag_native), &diag_native_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + EXPECT_EQ(diag_native, 200); + + // SQL_DIAG_SQLSTATE + const SQLSMALLINT sql_state_size = 6; + SQLWCHAR sql_state[sql_state_size]; + SQLSMALLINT sql_state_length; + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SQLSTATE, sql_state, + sql_state_size * driver::odbcabstraction::GetSqlWCharSize(), + &sql_state_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // 28000 + EXPECT_EQ(sql_state[0], '2'); + EXPECT_EQ(sql_state[1], '8'); + EXPECT_EQ(sql_state[2], '0'); + EXPECT_EQ(sql_state[3], '0'); + EXPECT_EQ(sql_state[4], '0'); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailureNTS) { + // Test is disabled because driver manager on Windows does not pass through SQL_NTS + // This test case can be potentionally used on macOS/Linux + GTEST_SKIP(); + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + // Retrieve all supported header level and record level data + SQLSMALLINT HEADER_LEVEL = 0; + SQLSMALLINT RECORD_1 = 1; + + // SQL_DIAG_MESSAGE_TEXT SQL_NTS + SQLWCHAR message_text[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_text_length; + + message_text[ODBC_BUFFER_SIZE - 1] = '\0'; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, + message_text, SQL_NTS, &message_text_length); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + EXPECT_GT(message_text_length, 100); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + +TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + ASSERT_OK_AND_ASSIGN(std::string connect_str, + arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); SQLWCHAR sql_state[6]; SQLINTEGER native_error; @@ -799,16 +978,16 @@ TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); - EXPECT_TRUE(message_length > 200); + EXPECT_GT(message_length, 200); - EXPECT_TRUE(native_error == 200); + EXPECT_EQ(native_error, 200); // 28000 - EXPECT_TRUE(sql_state[0] == '2'); - EXPECT_TRUE(sql_state[1] == '8'); - EXPECT_TRUE(sql_state[2] == '0'); - EXPECT_TRUE(sql_state[3] == '0'); - EXPECT_TRUE(sql_state[4] == '0'); + EXPECT_EQ(sql_state[0], '2'); + EXPECT_EQ(sql_state[1], '8'); + EXPECT_EQ(sql_state[2], '0'); + EXPECT_EQ(sql_state[3], '0'); + EXPECT_EQ(sql_state[4], '0'); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); From e7b60c3b9fd629d147f7909fb0da71c0633f24dd Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 30 May 2025 11:41:18 -0700 Subject: [PATCH 11/74] Enable Driver Logging Add todo to update logging system later Add logs --- .../flight/sql/odbc/flight_sql/address_info.h | 3 +- .../sql/odbc/flight_sql/flight_sql_driver.cc | 15 ++++- .../sql/odbc/flight_sql/win_system_dsn.cc | 3 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 67 ++++++++++++++++--- .../include/odbcabstraction/logger.h | 5 +- .../include/odbcabstraction/utils.h | 3 +- .../flight/sql/odbc/odbcabstraction/utils.cc | 5 +- 7 files changed, 82 insertions(+), 19 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h index 2609abd6af7..91f5a7175d7 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/address_info.h @@ -19,7 +19,8 @@ #include -#include +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + #include #include #if !_WIN32 diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc index 61a11252380..cb0e5c5ae5c 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc @@ -20,9 +20,11 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spd_logger.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h" +#include "arrow/util/io_util.h" #define DEFAULT_MAXIMUM_FILE_SIZE 16777216 #define CONFIG_FILE_NAME "arrow-odbc.ini" +#define CONFIG_FILE_PATH "CONFIG_FILE_PATH" namespace driver { namespace flight_sql { @@ -52,7 +54,9 @@ LogLevel ToLogLevel(int64_t level) { } // namespace FlightSqlDriver::FlightSqlDriver() - : diagnostics_("Apache Arrow", "Flight SQL", OdbcVersion::V_3), version_("0.9.0.0") {} + : diagnostics_("Apache Arrow", "Flight SQL", OdbcVersion::V_3), version_("0.9.0.0") { + RegisterLog(); +} std::shared_ptr FlightSqlDriver::CreateConnection(OdbcVersion odbc_version) { return std::make_shared(odbc_version, version_); @@ -63,14 +67,19 @@ odbcabstraction::Diagnostics& FlightSqlDriver::GetDiagnostics() { return diagnos void FlightSqlDriver::SetVersion(std::string version) { version_ = std::move(version); } void FlightSqlDriver::RegisterLog() { + std::string config_path = arrow::internal::GetEnvVar(CONFIG_FILE_PATH).ValueOr(""); + if (config_path.empty()) { + return; + } + odbcabstraction::PropertyMap propertyMap; - driver::odbcabstraction::ReadConfigFile(propertyMap, CONFIG_FILE_NAME); + driver::odbcabstraction::ReadConfigFile(propertyMap, config_path, CONFIG_FILE_NAME); auto log_enable_iterator = propertyMap.find(std::string(SPDLogger::LOG_ENABLED)); auto log_enabled = log_enable_iterator != propertyMap.end() ? odbcabstraction::AsBool(log_enable_iterator->second) : false; - if (!log_enabled) { + if (!log_enabled.get()) { return; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc index 5c8d116a7bc..18b5c399c2c 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc @@ -29,6 +29,7 @@ #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h" #include "arrow/flight/sql/odbc/flight_sql/system_dsn.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" #include #include @@ -87,7 +88,7 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config, properties = config.GetProperties(); return true; } else { - // TODO: log cancelled dialog after logging is enabled. + LOG_INFO("Dialog is cancelled by user"); return false; } } diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 5f54a5d714a..b3deb11bbb7 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -18,15 +18,15 @@ // flight_sql_connection.h needs to be included first due to conflicts with windows.h #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" -#include -#include -#include -#include -#include -#include - #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/diagnostics.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_environment.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" #if defined _WIN32 || defined _WIN64 // For displaying DSN Window @@ -35,10 +35,13 @@ // odbc_api includes windows.h, which needs to be put behind winsock2.h. // odbc_environment.h includes winsock2.h -#include +#include "arrow/flight/sql/odbc/odbc_api.h" namespace arrow { SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { + LOG_DEBUG("SQLAllocHandle called with type: {}, parent: {}, result: {}", type, parent, + fmt::ptr(result)); + *result = nullptr; switch (type) { @@ -93,6 +96,8 @@ SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) } SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { + LOG_DEBUG("SQLFreeHandle called with type: {}, handle: {}", type, handle); + switch (type) { case SQL_HANDLE_ENV: { using ODBC::ODBCEnvironment; @@ -160,6 +165,12 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, using ODBC::ODBCConnection; using ODBC::ODBCEnvironment; + LOG_DEBUG( + "SQLGetDiagFieldW called with handleType: {}, handle: {}, recNumber: {}, " + "diagIdentifier: {}, diagInfoPtr: {}, bufferLength: {}, stringLengthPtr: {}", + handleType, handle, recNumber, diagIdentifier, diagInfoPtr, bufferLength, + fmt::ptr(stringLengthPtr)); + if (!handle) { return SQL_INVALID_HANDLE; } @@ -369,6 +380,13 @@ SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT r using ODBC::ODBCConnection; using ODBC::ODBCEnvironment; + LOG_DEBUG( + "SQLGetDiagRecW called with handleType: {}, handle: {}, recNumber: {}, " + "sqlState: {}, nativeErrorPtr: {}, messageText: {}, bufferLength: {}, " + "textLengthPtr: {}", + handleType, handle, recNumber, fmt::ptr(sqlState), fmt::ptr(nativeErrorPtr), + fmt::ptr(messageText), bufferLength, fmt::ptr(textLengthPtr)); + if (!handle) { return SQL_INVALID_HANDLE; } @@ -438,10 +456,15 @@ SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT r } SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, - SQLINTEGER bufferLen, SQLINTEGER* strLenPtr) { + SQLINTEGER bufferLength, SQLINTEGER* strLenPtr) { using driver::odbcabstraction::DriverException; using ODBC::ODBCEnvironment; + LOG_DEBUG( + "SQLGetEnvAttr called with env: {}, attr: {}, valuePtr: {}, " + "bufferLength: {}, strLenPtr: {}", + env, attr, valuePtr, bufferLength, fmt::ptr(strLenPtr)); + ODBCEnvironment* environment = reinterpret_cast(env); return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { @@ -498,6 +521,11 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, using driver::odbcabstraction::DriverException; using ODBC::ODBCEnvironment; + LOG_DEBUG( + "SQLSetEnvAttr called with env: {}, attr: {}, valuePtr: {}, " + "strLen: {}", + env, attr, valuePtr, strLen); + ODBCEnvironment* environment = reinterpret_cast(env); return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { @@ -557,6 +585,14 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, using driver::odbcabstraction::DriverException; using ODBC::ODBCConnection; + LOG_DEBUG( + "SQLDriverConnectW called with conn: {}, windowHandle: {}, inConnectionString: {}, " + "inConnectionStringLen: {}, outConnectionString: {}, outConnectionStringBufferLen: " + "{}, outConnectionStringLen: {}, driverCompletion: {}", + conn, fmt::ptr(windowHandle), fmt::ptr(inConnectionString), inConnectionStringLen, + fmt::ptr(outConnectionString), outConnectionStringBufferLen, + fmt::ptr(outConnectionStringLen), driverCompletion); + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); std::string connection_string = @@ -621,6 +657,12 @@ SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, using ODBC::SqlWcharToString; + LOG_DEBUG( + "SQLConnectW called with conn: {}, dsnName: {}, dsnNameLen: {}, userName: {}, " + "userNameLen: {}, password: {}, passwordLen: {}", + conn, fmt::ptr(dsnName), dsnNameLen, fmt::ptr(userName), userNameLen, + fmt::ptr(password), passwordLen); + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); std::string dsn = SqlWcharToString(dsnName, dsnNameLen); @@ -649,6 +691,8 @@ SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, SQLRETURN SQLDisconnect(SQLHDBC conn) { using ODBC::ODBCConnection; + LOG_DEBUG("SQLDisconnect called with conn: {}", conn); + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); @@ -663,6 +707,11 @@ SQLRETURN SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValueP // TODO: complete implementation of SQLGetInfoW and write tests using ODBC::ODBCConnection; + LOG_DEBUG( + "SQLGetInfoW called with conn: {}, infoType: {}, infoValuePtr: {}, bufLen: {}, " + "length: {}", + conn, infoType, infoValuePtr, bufLen, fmt::ptr(length)); + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h index 5f8619cbb92..4ea3261cbed 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h @@ -20,7 +20,10 @@ #include #include -#include +#include + +// The logger using spdlog is deprecated and will be replaced. +// TODO: mirgate logging to use Arrow's internal logging system #define __LAZY_LOG(LEVEL, ...) \ do { \ diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h index 0fa8463b546..6e1fe5739be 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h @@ -52,7 +52,8 @@ boost::optional AsInt32(int32_t min_value, const Connection::ConnPropertyMap& connPropertyMap, const std::string_view& property_name); -void ReadConfigFile(PropertyMap& properties, const std::string& configFileName); +void ReadConfigFile(PropertyMap& properties, const std::string& configPath, + const std::string& configFileName); } // namespace odbcabstraction } // namespace driver diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc index 45dfbcf2e42..6feb7ff3be2 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/utils.cc @@ -81,9 +81,8 @@ std::string GetModulePath() { return std::string(path.begin(), path.begin() + dirname_length); } -void ReadConfigFile(PropertyMap& properties, const std::string& config_file_name) { - auto config_path = GetModulePath(); - +void ReadConfigFile(PropertyMap& properties, const std::string& config_path, + const std::string& config_file_name) { std::ifstream config_file; auto config_file_path = config_path + "/" + config_file_name; config_file.open(config_file_path); From e68b605385d8553af087a4fa3cf3db37efaad478 Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 11 Jun 2025 09:47:05 -0700 Subject: [PATCH 12/74] Implement ODBC API with debug logging --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 219 +++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 46536b84e9b..912a544e22d 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -27,6 +27,8 @@ #include "arrow/flight/sql/odbc/odbc_api.h" #include "arrow/flight/sql/odbc/visibility.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" + SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { return arrow::SQLAllocHandle(type, parent, result); } @@ -79,7 +81,8 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePt SQLRETURN SQL_API SQLSetConnectAttrW(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, SQLINTEGER valueLen) { - // TODO implement SQLSetConnectAttr + LOG_DEBUG("SQLSetConnectAttrW called with conn: {}, attr: {}, value: {}, valueLen: {}", + conn, attr, value, valueLen); return SQL_ERROR; } @@ -109,3 +112,217 @@ SQLRETURN SQL_API SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNa } SQLRETURN SQL_API SQLDisconnect(SQLHDBC conn) { return arrow::SQLDisconnect(conn); } + +SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLSMALLINT targetType, SQLPOINTER targetValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { + LOG_DEBUG( + "SQLBindCol called with statementHandle: {}, columnNumber: {}, targetType: {}, " + "targetValuePtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", + statementHandle, columnNumber, targetType, targetValuePtr, bufferLength, + fmt::ptr(strLen_or_IndPtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLCancel(SQLHSTMT statementHandle) { + LOG_DEBUG("SQLCancel called with statementHandle: {}", statementHandle); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { + LOG_DEBUG("SQLCloseCursor called with statementHandle: {}", statementHandle); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLColAttributeW(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, + SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, + SQLLEN* numericAttributePtr) { + LOG_DEBUG( + "SQLColAttributeW called with statementHandle: {}, columnNumber: {}, " + "fieldIdentifier: {}, characterAttributePtr: {}, bufferLength: {}, " + "stringLengthPtr: {}, numericAttributePtr: {}", + statementHandle, columnNumber, fieldIdentifier, characterAttributePtr, bufferLength, + fmt::ptr(stringLengthPtr), fmt::ptr(numericAttributePtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLColumnsW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength, SQLWCHAR* columnName, + SQLSMALLINT columnNameLength) { + LOG_DEBUG( + "SQLColumnsW called with statementHandle: {}, catalogName: {}, catalogNameLength: " + "{}, " + "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " + "columnName: {}, " + "columnNameLength: {}", + statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(columnName), + columnNameLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLErrorW(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, + SQLWCHAR FAR* szSqlState, SQLINTEGER FAR* pfNativeError, + SQLWCHAR FAR* szErrorMsg, SQLSMALLINT cbErrorMsgMax, + SQLSMALLINT FAR* pcbErrorMsg) { + LOG_DEBUG( + "SQLErrorW called with handleType: {}, handle: {}, hstmt: {}, szSqlState: {}, " + "pfNativeError: {}, szErrorMsg: {}, cbErrorMsgMax: {}, pcbErrorMsg: {}", + handleType, handle, hstmt, fmt::ptr(szSqlState), fmt::ptr(pfNativeError), + fmt::ptr(szErrorMsg), cbErrorMsgMax, fmt::ptr(pcbErrorMsg)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLExecDirectW(SQLHSTMT statementHandle, SQLWCHAR* statementText, + SQLINTEGER textLength) { + LOG_DEBUG( + "SQLExecDirectW called with statementHandle: {}, statementText: {}, textLength: {}", + statementHandle, fmt::ptr(statementText), textLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { + LOG_DEBUG("SQLExecute called with statementHandle: {}", statementHandle); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { + LOG_DEBUG("SQLFetch called with statementHandle: {}", statementHandle); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLForeignKeysW(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, + SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, + SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, + SQLSMALLINT pKTableNameLength, SQLWCHAR* fKCatalogName, + SQLSMALLINT fKCatalogNameLength, SQLWCHAR* fKSchemaName, + SQLSMALLINT fKSchemaNameLength, SQLWCHAR* fKTableName, + SQLSMALLINT fKTableNameLength) { + LOG_DEBUG( + "SQLForeignKeysW called with statementHandle: {}, pKCatalogName: {}, " + "pKCatalogNameLength: " + "{}, pKSchemaName: {}, pKSchemaNameLength: {}, pKTableName: {}, pKTableNameLength: " + "{}, " + "fKCatalogName: {}, fKCatalogNameLength: {}, fKSchemaName: {}, fKSchemaNameLength: " + "{}, " + "fKTableName: {}, fKTableNameLength : {}", + statementHandle, fmt::ptr(pKCatalogName), pKCatalogNameLength, + fmt::ptr(pKSchemaName), pKSchemaNameLength, fmt::ptr(pKTableName), + pKTableNameLength, fmt::ptr(fKCatalogName), fKCatalogNameLength, + fmt::ptr(fKSchemaName), fKSchemaNameLength, fmt::ptr(fKTableName), + fKTableNameLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLGetConnectAttrW(SQLHDBC connectionHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + LOG_DEBUG( + "SQLGetConnectAttrW called with connectionHandle: {}, attribute: {}, valuePtr: {}, " + "bufferLength: {}, stringLengthPtr: {}", + connectionHandle, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLUSMALLINT col_or_Param_Num, + SQLSMALLINT targetType, SQLPOINTER targetValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { + LOG_DEBUG( + "SQLGetData called with statementHandle: {}, col_or_Param_Num: {}, targetType: {}, " + "targetValuePtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", + statementHandle, col_or_Param_Num, targetType, targetValuePtr, bufferLength, + fmt::ptr(strLen_or_IndPtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLGetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + LOG_DEBUG( + "SQLGetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " + "bufferLength: {}, stringLengthPtr: {}", + statementHandle, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLGetTypeInfoW(SQLHSTMT statementHandle, SQLSMALLINT dataType) { + LOG_DEBUG("SQLGetTypeInfoW called with statementHandle: {} dataType: {}", + statementHandle, dataType); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLMoreResults(SQLHSTMT statementHandle) { + LOG_DEBUG("SQLMoreResults called with statementHandle: {}", statementHandle); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLNativeSqlW(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, + SQLINTEGER inStatementTextLength, + SQLWCHAR* outStatementText, SQLINTEGER bufferLength, + SQLINTEGER* outStatementTextLength) { + LOG_DEBUG( + "SQLNativeSqlW called with connectionHandle: {}, inStatementText: {}, " + "inStatementTextLength: " + "{}, outStatementText: {}, bufferLength: {}, outStatementTextLength: {}", + connectionHandle, fmt::ptr(inStatementText), inStatementTextLength, + fmt::ptr(outStatementText), bufferLength, fmt::ptr(outStatementTextLength)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, + SQLSMALLINT* columnCountPtr) { + LOG_DEBUG("SQLNumResultCols called with statementHandle: {}, columnCountPtr: {}", + statementHandle, fmt::ptr(columnCountPtr)); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLPrepareW(SQLHSTMT statementHandle, SQLWCHAR* statementText, + SQLINTEGER textLength) { + LOG_DEBUG( + "SQLPrepareW called with statementHandle: {}, statementText: {}, textLength: {}", + statementHandle, fmt::ptr(statementText), textLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLPrimaryKeysW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength) { + LOG_DEBUG( + "SQLPrimaryKeysW called with statementHandle: {}, catalogName: {}, " + "catalogNameLength: " + "{}, schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}", + statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + schemaNameLength, fmt::ptr(tableName), tableNameLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLSetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER stringLength) { + LOG_DEBUG( + "SQLSetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " + "stringLength: {}", + statementHandle, attribute, valuePtr, stringLength); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLTablesW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength, SQLWCHAR* tableType, + SQLSMALLINT tableTypeLength) { + LOG_DEBUG( + "SQLTablesW called with statementHandle: {}, catalogName: {}, catalogNameLength: " + "{}, " + "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " + "tableType: {}, " + "tableTypeLength: {}", + statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(tableType), + tableTypeLength); + return SQL_ERROR; +} From fd6c8a4c7016b9e6a063bd956fec0d60275c3c47 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:35:40 -0700 Subject: [PATCH 13/74] Enable mock test (#42) * Add todo for noauth validation * mock server with token auth Add tests * run same test with both modes * Enable ODBC tests in workflow * Switch current test cases to use FlightSQLODBCTestBase So the tests can be skipped when `TEST_CONNECT_STR` is not set. * Change tests to run on both mock and remote modes Wrap usage of TEST_CONNECT_STR where possible * Rename test fixtures and make connection string functions virtual * Fix lint issue * Attempt to enable ODBC build on Windows platforms * Attempt to fix clang64 and MinGW errors * Attempt to register ODBC * Address James' comments Use constant string for token * use ServerMiddleware to validate token --- .github/workflows/cpp.yml | 4 + ci/scripts/cpp_test.sh | 2 - cpp/cmake_modules/DefineOptions.cmake | 2 +- .../odbc/flight_sql/flight_sql_auth_method.cc | 4 + .../flight/sql/odbc/flight_sql/system_dsn.h | 2 +- .../flight/sql/odbc/tests/CMakeLists.txt | 12 ++ .../flight/sql/odbc/tests/connection_test.cc | 111 ++++++++---------- .../flight/sql/odbc/tests/odbc_test_suite.cc | 97 ++++++++++++++- .../flight/sql/odbc/tests/odbc_test_suite.h | 83 ++++++++++++- 9 files changed, 248 insertions(+), 69 deletions(-) diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 0961e66ac8a..cc62faef2ad 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -476,6 +476,10 @@ jobs: PIPX_BASE_PYTHON: ${{ steps.python-install.outputs.python-path }} run: | ci/scripts/install_gcs_testbench.sh default + - name: Register Flight SQL ODBC Driver + shell: cmd + run: | + call "cpp\src\arrow\flight\sql\odbc\install\install_amd64.cmd" ${{github.workspace}}\build\cpp\%ARROW_BUILD_TYPE%\libarrow_flight_sql_odbc.dll - name: Test shell: msys2 {0} run: | diff --git a/ci/scripts/cpp_test.sh b/ci/scripts/cpp_test.sh index d761491bdd1..88c06849c8b 100755 --- a/ci/scripts/cpp_test.sh +++ b/ci/scripts/cpp_test.sh @@ -73,8 +73,6 @@ case "$(uname)" in exclude_tests="${exclude_tests}|gandiva-precompiled-test" exclude_tests="${exclude_tests}|gandiva-projector-test" exclude_tests="${exclude_tests}|gandiva-utf8-test" - # TODO: Enable ODBC tests - exclude_tests="${exclude_tests}|arrow-connection-test" ctest_options+=(--exclude-regex "${exclude_tests}") ;; *) diff --git a/cpp/cmake_modules/DefineOptions.cmake b/cpp/cmake_modules/DefineOptions.cmake index faac95c4004..1b28f70f4ce 100644 --- a/cpp/cmake_modules/DefineOptions.cmake +++ b/cpp/cmake_modules/DefineOptions.cmake @@ -108,7 +108,7 @@ endmacro() macro(resolve_option_dependencies) # Arrow Flight SQL ODBC is available only for Windows for now. - if(NOT MSVC_TOOLCHAIN) + if(NOT WIN32) set(ARROW_FLIGHT_SQL_ODBC OFF) endif() if(MSVC_TOOLCHAIN) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc index 3fcc3a87162..4b66c30dab0 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc @@ -45,6 +45,10 @@ class NoOpAuthMethod : public FlightSqlAuthMethod { void Authenticate(FlightSqlConnection& connection, FlightCallOptions& call_options) override { // Do nothing + + // TODO: implement NoOpAuthMethod to validate server address. + // Can use NoOpClientAuthHandler. + // https://github.com/apache/arrow/issues/46733 } }; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h index f5470693eea..f3744d3428a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h @@ -40,7 +40,7 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config); * @param windowParent Parent window handle. * @param config Output configuration, presumed to be empty, it will be using values from * properties. - * @param config Output properties. + * @param properties Output properties. * @return True on success and false on fail. */ bool DisplayConnectionWindow(void* windowParent, Configuration& config, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 1d0dce0bec4..41e51182275 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -21,13 +21,25 @@ include_directories(${ODBC_INCLUDE_DIRS}) add_definitions(-DUNICODE=1) +find_package(SQLite3Alt REQUIRED) + +set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS + ../../example/sqlite_sql_info.cc + ../../example/sqlite_type_info.cc + ../../example/sqlite_statement.cc + ../../example/sqlite_statement_batch_reader.cc + ../../example/sqlite_server.cc + ../../example/sqlite_tables_schema_batch_reader.cc) + add_arrow_test(connection_test SOURCES connection_test.cc odbc_test_suite.cc odbc_test_suite.h + ${ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS} EXTRA_LINK_LIBS ${ODBC_LIBRARIES} ${ODBCINST} + ${SQLite3_LIBRARIES} arrow_odbc_spi_impl odbcabstraction) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index b2866b3e5a7..8461ead6cf9 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -200,49 +200,51 @@ TEST(SQLSetEnvAttr, TestSQLSetEnvAttrODBCVersionInvalid) { EXPECT_TRUE(return_set == SQL_ERROR); } -TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrOutputNTS) { - connect(); +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetEnvAttrOutputNTS) { + this->connect(); SQLINTEGER output_nts; - SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, &output_nts, 0, 0); + SQLRETURN return_get = SQLGetEnvAttr(this->env, SQL_ATTR_OUTPUT_NTS, &output_nts, 0, 0); EXPECT_TRUE(return_get == SQL_SUCCESS); EXPECT_EQ(output_nts, SQL_TRUE); - disconnect(); + this->disconnect(); } -TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrGetLength) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetEnvAttrGetLength) { // Test is disabled because call to SQLGetEnvAttr is handled by the driver manager on - // Windows. This test case can be potentionally used on macOS/Linux + // Windows. This test case can be potentially used on macOS/Linux GTEST_SKIP(); - connect(); + this->connect(); SQLINTEGER length; - SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, &length); + SQLRETURN return_get = + SQLGetEnvAttr(this->env, SQL_ATTR_ODBC_VERSION, nullptr, 0, &length); EXPECT_TRUE(return_get == SQL_SUCCESS); EXPECT_EQ(length, sizeof(SQLINTEGER)); - disconnect(); + this->disconnect(); } -TEST_F(FlightSQLODBCTestBase, TestSQLGetEnvAttrNullValuePointer) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetEnvAttrNullValuePointer) { // Test is disabled because call to SQLGetEnvAttr is handled by the driver manager on - // Windows. This test case can be potentionally used on macOS/Linux + // Windows. This test case can be potentially used on macOS/Linux GTEST_SKIP(); - connect(); + this->connect(); - SQLRETURN return_get = SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0, nullptr); + SQLRETURN return_get = + SQLGetEnvAttr(this->env, SQL_ATTR_ODBC_VERSION, nullptr, 0, nullptr); EXPECT_TRUE(return_get == SQL_ERROR); - disconnect(); + this->disconnect(); } TEST(SQLSetEnvAttr, TestSQLSetEnvAttrOutputNTSValid) { @@ -292,7 +294,7 @@ TEST(SQLSetEnvAttr, TestSQLSetEnvAttrNullValuePointer) { EXPECT_TRUE(return_set == SQL_ERROR); } -TEST(SQLDriverConnect, TestSQLDriverConnect) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnect) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -312,8 +314,7 @@ TEST(SQLDriverConnect, TestSQLDriverConnect) { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::string connect_str = this->getConnectionString(); ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, arrow::util::UTF8ToWideString(connect_str)); std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); @@ -361,7 +362,7 @@ TEST(SQLDriverConnect, TestSQLDriverConnect) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLDriverConnect, TestSQLDriverConnectInvalidUid) { +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -380,11 +381,8 @@ TEST(SQLDriverConnect, TestSQLDriverConnectInvalidUid) { EXPECT_TRUE(ret == SQL_SUCCESS); - // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); - // Append invalid uid to connection string - connect_str += std::string("uid=non_existent_id;"); + // Invalid connect string + std::string connect_str = getInvalidConnectionString(); ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, arrow::util::UTF8ToWideString(connect_str)); @@ -418,7 +416,7 @@ TEST(SQLDriverConnect, TestSQLDriverConnectInvalidUid) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLConnect, TestSQLConnect) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -438,8 +436,7 @@ TEST(SQLConnect, TestSQLConnect) { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::string connect_str = this->getConnectionString(); // Write connection string content into a DSN, // must succeed before continuing @@ -454,7 +451,7 @@ TEST(SQLConnect, TestSQLConnect) { std::vector uid0(wuid.begin(), wuid.end()); std::vector pwd0(wpwd.begin(), wpwd.end()); - // Connecting to ODBC server. + // Connecting to ODBC server. Empty uid and pwd should be ignored. ret = SQLConnect(conn, dsn0.data(), static_cast(dsn0.size()), uid0.data(), static_cast(uid0.size()), pwd0.data(), static_cast(pwd0.size())); @@ -488,7 +485,7 @@ TEST(SQLConnect, TestSQLConnect) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLConnect, TestSQLConnectInputUidPwd) { +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -508,10 +505,9 @@ TEST(SQLConnect, TestSQLConnectInputUidPwd) { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::string connect_str = getConnectionString(); - // Retrieve valid uid and pwd + // Retrieve valid uid and pwd, assumes TEST_CONNECT_STR contains uid and pwd Connection::ConnPropertyMap properties; ODBC::ODBCConnection::getPropertiesFromConnString(connect_str, properties); std::string uid_key("uid"); @@ -567,7 +563,7 @@ TEST(SQLConnect, TestSQLConnectInputUidPwd) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLConnect, TestSQLConnectInvalidUid) { +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -587,10 +583,9 @@ TEST(SQLConnect, TestSQLConnectInvalidUid) { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::string connect_str = getConnectionString(); - // Retrieve valid uid and pwd + // Retrieve valid uid and pwd, assumes TEST_CONNECT_STR contains uid and pwd Connection::ConnPropertyMap properties; ODBC::ODBCConnection::getPropertiesFromConnString(connect_str, properties); std::string uid = properties[std::string("uid")]; @@ -636,7 +631,7 @@ TEST(SQLConnect, TestSQLConnectInvalidUid) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLConnect, TestSQLConnectDSNPrecedence) { +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -656,13 +651,13 @@ TEST(SQLConnect, TestSQLConnectDSNPrecedence) { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); + std::string connect_str = getConnectionString(); // Write connection string content into a DSN, // must succeed before continuing - // Pass incorrect uid and password to SQLConnect, they will be ignored + // Pass incorrect uid and password to SQLConnect, they will be ignored. + // Assumes TEST_CONNECT_STR contains uid and pwd std::string uid("non_existent_id"), pwd("non_existent_password"); ASSERT_TRUE(writeDSN(connect_str)); @@ -746,7 +741,7 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailure) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -765,11 +760,8 @@ TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); - // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); - // Append invalid uid to connection string - connect_str += std::string("uid=non_existent_id;"); + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, arrow::util::UTF8ToWideString(connect_str)); @@ -859,9 +851,9 @@ TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailureNTS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { // Test is disabled because driver manager on Windows does not pass through SQL_NTS - // This test case can be potentionally used on macOS/Linux + // This test case can be potentially used on macOS/Linux GTEST_SKIP(); // ODBC Environment SQLHENV env; @@ -881,11 +873,8 @@ TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailureNTS) { EXPECT_TRUE(ret == SQL_SUCCESS); - // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); - // Append invalid uid to connection string - connect_str += std::string("uid=non_existent_id;"); + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, arrow::util::UTF8ToWideString(connect_str)); @@ -902,7 +891,6 @@ TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailureNTS) { EXPECT_TRUE(ret == SQL_ERROR); // Retrieve all supported header level and record level data - SQLSMALLINT HEADER_LEVEL = 0; SQLSMALLINT RECORD_1 = 1; // SQL_DIAG_MESSAGE_TEXT SQL_NTS @@ -929,7 +917,7 @@ TEST(SQLGetDiagFieldW, TestSQLGetDiagFieldWForConnectFailureNTS) { EXPECT_TRUE(ret == SQL_SUCCESS); } -TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { // ODBC Environment SQLHENV env; SQLHDBC conn; @@ -948,11 +936,8 @@ TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); - // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); - // Append invalid uid to connection string - connect_str += std::string("uid=non_existent_id;"); + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, arrow::util::UTF8ToWideString(connect_str)); @@ -978,7 +963,7 @@ TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); - EXPECT_GT(message_length, 200); + EXPECT_GT(message_length, 120); EXPECT_EQ(native_error, 200); @@ -1000,6 +985,12 @@ TEST(SQLGetDiagRec, TestSQLGetDiagRecForConnectFailure) { EXPECT_TRUE(ret == SQL_SUCCESS); } +TYPED_TEST(FlightSQLODBCTestBase, TestConnect) { + // Verifies connect and disconnect works on its own + this->connect(); + this->disconnect(); +} + } // namespace integration_tests } // namespace odbc } // namespace flight diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index d60bcea19e4..656c221a1d1 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -29,7 +29,11 @@ namespace arrow { namespace flight { namespace odbc { namespace integration_tests { -void FlightSQLODBCTestBase::connect() { +void FlightSQLODBCRemoteTestBase::connect() { + std::string connect_str = getConnectionString(); + connectWithString(connect_str); +} +void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); @@ -45,8 +49,6 @@ void FlightSQLODBCTestBase::connect() { EXPECT_TRUE(ret == SQL_SUCCESS); // Connect string - ASSERT_OK_AND_ASSIGN(std::string connect_str, - arrow::internal::GetEnvVar(TEST_CONNECT_STR)); std::vector connect_str0(connect_str.begin(), connect_str.end()); SQLWCHAR outstr[ODBC_BUFFER_SIZE]; @@ -65,7 +67,7 @@ void FlightSQLODBCTestBase::connect() { ASSERT_TRUE(ret == SQL_SUCCESS); } -void FlightSQLODBCTestBase::disconnect() { +void FlightSQLODBCRemoteTestBase::disconnect() { // Disconnect from ODBC SQLRETURN ret = SQLDisconnect(conn); @@ -86,6 +88,93 @@ void FlightSQLODBCTestBase::disconnect() { EXPECT_TRUE(ret == SQL_SUCCESS); } +std::string FlightSQLODBCRemoteTestBase::getConnectionString() { + std::string connect_str = arrow::internal::GetEnvVar(TEST_CONNECT_STR).ValueOrDie(); + return connect_str; +} + +std::string FlightSQLODBCRemoteTestBase::getInvalidConnectionString() { + std::string connect_str = getConnectionString(); + // Append invalid uid to connection string + connect_str += std::string("uid=non_existent_id;"); + return connect_str; +} + +void FlightSQLODBCRemoteTestBase::SetUp() { + if (arrow::internal::GetEnvVar(TEST_CONNECT_STR).ValueOr("").empty()) { + GTEST_SKIP() << "Skipping FlightSQLODBCRemoteTestBase test: TEST_CONNECT_STR not set"; + } +} + +std::string FindTokenInCallHeaders(const CallHeaders& incoming_headers) { + // Lambda function to compare characters without case sensitivity. + auto char_compare = [](const char& char1, const char& char2) { + return (::toupper(char1) == ::toupper(char2)); + }; + + const std::string auth_val(incoming_headers.find(kAuthHeader)->second); + std::string bearer_token(""); + if (auth_val.size() > kBearerPrefix.length()) { + if (std::equal(auth_val.begin(), auth_val.begin() + kBearerPrefix.length(), + kBearerPrefix.begin(), char_compare)) { + bearer_token = auth_val.substr(kBearerPrefix.length()); + } + } + return bearer_token; +} + +void MockServerMiddleware::SendingHeaders(AddCallHeaders* outgoing_headers) { + std::string bearer_token = FindTokenInCallHeaders(incoming_headers_); + *isValid_ = (bearer_token == std::string(test_token)); +} + +Status MockServerMiddlewareFactory::StartCall( + const CallInfo& info, const ServerCallContext& context, + std::shared_ptr* middleware) { + std::string bearer_token = FindTokenInCallHeaders(context.incoming_headers()); + if (bearer_token == std::string(test_token)) { + *middleware = + std::make_shared(context.incoming_headers(), &isValid_); + } else { + return MakeFlightError(FlightStatusCode::Unauthenticated, + "Invalid token for mock server"); + } + + return Status::OK(); +} + +std::string FlightSQLODBCMockTestBase::getConnectionString() { + std::string connect_str( + "driver={Apache Arrow Flight SQL ODBC Driver};HOST=localhost;port=" + + std::to_string(port) + ";token=" + std::string(test_token) + + ";useEncryption=false;"); + return connect_str; +} + +std::string FlightSQLODBCMockTestBase::getInvalidConnectionString() { + std::string connect_str = getConnectionString(); + // Append invalid token to connection string + connect_str += std::string("token=invalid_token;"); + return connect_str; +} + +void FlightSQLODBCMockTestBase::SetUp() { + ASSERT_OK_AND_ASSIGN(auto location, Location::ForGrpcTcp("0.0.0.0", 0)); + arrow::flight::FlightServerOptions options(location); + options.auth_handler = std::make_unique(); + options.middleware.push_back( + {"bearer-auth-server", std::make_shared()}); + ASSERT_OK_AND_ASSIGN(server, + arrow::flight::sql::example::SQLiteFlightSqlServer::Create()); + ASSERT_OK(server->Init(options)); + + port = server->port(); + ASSERT_OK_AND_ASSIGN(location, Location::ForGrpcTcp("localhost", port)); + ASSERT_OK_AND_ASSIGN(auto client, arrow::flight::FlightClient::Connect(location)); +} + +void FlightSQLODBCMockTestBase::TearDown() { ASSERT_OK(server->Shutdown()); } + bool compareConnPropertyMap(Connection::ConnPropertyMap map1, Connection::ConnPropertyMap map2) { if (map1.size() != map2.size()) return false; 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 49ab2e20f44..168204e8d0b 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 @@ -19,6 +19,9 @@ #include "arrow/util/io_util.h" #include "arrow/util/utf8.h" +#include "arrow/flight/server_middleware.h" +#include "arrow/flight/sql/client.h" +#include "arrow/flight/sql/example/sqlite_server.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" #ifdef _WIN32 @@ -29,6 +32,8 @@ #include #include +#include + #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" // For DSN registration @@ -43,13 +48,20 @@ namespace odbc { namespace integration_tests { using driver::odbcabstraction::Connection; -class FlightSQLODBCTestBase : public ::testing::Test { +class FlightSQLODBCRemoteTestBase : public ::testing::Test { public: /// \brief Connect to Arrow Flight SQL server using connection string defined in /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" void connect(); + /// \brief Connect to Arrow Flight SQL server using connection string + void connectWithString(std::string connection_str); /// \brief Disconnect from server void disconnect(); + /// \brief Get connection string from environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" + std::string virtual getConnectionString(); + /// \brief Get invalid connection string based on connection string defined in + /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" + std::string virtual getInvalidConnectionString(); /** ODBC Environment. */ SQLHENV env; @@ -59,8 +71,77 @@ class FlightSQLODBCTestBase : public ::testing::Test { /** ODBC Statement. */ SQLHSTMT stmt; + + protected: + void SetUp() override; +}; + +static constexpr std::string_view kAuthHeader = "authorization"; +static constexpr std::string_view kBearerPrefix = "Bearer "; +static constexpr std::string_view test_token = "t0k3n"; + +std::string FindTokenInCallHeaders(const CallHeaders& incoming_headers); + +// A server middleware for validating incoming bearer header authentication. +class MockServerMiddleware : public ServerMiddleware { + public: + explicit MockServerMiddleware(const CallHeaders& incoming_headers, bool* isValid) + : isValid_(isValid) { + incoming_headers_ = incoming_headers; + } + + void SendingHeaders(AddCallHeaders* outgoing_headers) override; + + void CallCompleted(const Status& status) override {} + + std::string name() const override { return "MockServerMiddleware"; } + + private: + CallHeaders incoming_headers_; + bool* isValid_; +}; + +// Factory for base64 header authentication testing. +class MockServerMiddlewareFactory : public ServerMiddlewareFactory { + public: + MockServerMiddlewareFactory() : isValid_(false) {} + + Status StartCall(const CallInfo& info, const ServerCallContext& context, + std::shared_ptr* middleware) override; + + private: + bool isValid_; +}; + +class FlightSQLODBCMockTestBase : public FlightSQLODBCRemoteTestBase { + // Sets up a mock server for each test case + public: + /// \brief Get connection string for mock server + std::string getConnectionString() override; + /// \brief Get invalid connection string for mock server + std::string getInvalidConnectionString() override; + + int port; + + protected: + void SetUp() override; + + void TearDown() override; + + private: + std::shared_ptr server; }; +template +class FlightSQLODBCTestBase : public T { + public: + using List = std::list; +}; + +using TestTypes = + ::testing::Types; +TYPED_TEST_SUITE(FlightSQLODBCTestBase, TestTypes); + /** ODBC read buffer size. */ enum { ODBC_BUFFER_SIZE = 1024 }; From 2c41e04fb943635d0f84f147a61017a3e862f338 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 12 Jun 2025 13:11:15 -0700 Subject: [PATCH 14/74] Fix connection issues to DBT Labs PopulateCallOptions before making a connection Fix dsn window bug with advance properties Fix seg fault issue from empty string --- .../arrow/flight/sql/odbc/flight_sql/config/configuration.cc | 4 ++-- .../arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc | 3 ++- .../odbc/flight_sql/include/flight_sql/config/configuration.h | 2 +- .../flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc | 2 +- .../include/odbcabstraction/odbc_impl/encoding_utils.h | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc index bfd050e724b..7cebebe56eb 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc @@ -170,12 +170,12 @@ const driver::odbcabstraction::Connection::ConnPropertyMap& Configuration::GetPr return this->properties; } -std::vector Configuration::GetCustomKeys() const { +std::vector Configuration::GetCustomKeys() const { driver::odbcabstraction::Connection::ConnPropertyMap copyProps(properties); for (auto& key : FlightSqlConnection::ALL_KEYS) { copyProps.erase(std::string(key)); } - std::vector keys; + std::vector keys; boost::copy(copyProps | boost::adaptors::map_keys, std::back_inserter(keys)); return keys; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc index 708ac2f81a4..54e712feffb 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc @@ -175,6 +175,8 @@ void FlightSqlConnection::Connect(const ConnPropertyMap& properties, std::unique_ptr flight_client; ThrowIfNotOK(FlightClient::Connect(location, client_options).Value(&flight_client)); + PopulateCallOptions(properties); + std::unique_ptr auth_method = FlightSqlAuthMethod::FromProperties(flight_client, properties); auth_method->Authenticate(*this, call_options_); @@ -190,7 +192,6 @@ void FlightSqlConnection::Connect(const ConnPropertyMap& properties, attribute_[CONNECTION_DEAD] = static_cast(SQL_FALSE); PopulateMetadataSettings(properties); - PopulateCallOptions(properties); } catch (...) { attribute_[CONNECTION_DEAD] = static_cast(SQL_TRUE); sql_client_.reset(); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h index c7c9cc5b894..8c4d6865505 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h @@ -59,7 +59,7 @@ class Configuration { */ const driver::odbcabstraction::Connection::ConnPropertyMap& GetProperties() const; - std::vector GetCustomKeys() const; + std::vector GetCustomKeys() const; private: driver::odbcabstraction::Connection::ConnPropertyMap properties; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index c47984ca400..e469b6b067f 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -306,7 +306,7 @@ int DsnConfigurationWindow::CreatePropertiesGroup(int posX, int posY, int sizeX) const auto keys = config.GetCustomKeys(); for (const auto& key : keys) { - propertyList->ListAddItem({std::string(key), config.Get(key)}); + propertyList->ListAddItem({key, config.Get(key)}); } SendMessage(propertyList->GetHandle(), LVM_SETEXTENDEDLISTVIEWSTYLE, diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 45ed8713626..3d6a80f835d 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -83,7 +83,7 @@ inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, /// \param[in] msg_len Number of characters in wchar_msg /// \return wchar_msg in std::string format inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLSMALLINT msg_len = SQL_NTS) { - if (!wchar_msg) { + if (!wchar_msg || wchar_msg[0] == 0) { return std::string(); } From 252d8b07cb7e30b9ccc0b097e9fb553c1db92055 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 12 Jun 2025 15:44:23 -0700 Subject: [PATCH 15/74] Implement SQLAllocStmt --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 8 ++ cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 76 +++++++++++++++- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 1 + .../flight/sql/odbc/tests/connection_test.cc | 87 +++++++++++++++++++ .../flight/sql/odbc/tests/odbc_test_suite.cc | 12 ++- 5 files changed, 180 insertions(+), 4 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 912a544e22d..ef6aa2e5fd0 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -41,6 +41,10 @@ SQLRETURN SQL_API SQLAllocConnect(SQLHENV env, SQLHDBC* conn) { return arrow::SQLAllocHandle(SQL_HANDLE_DBC, env, conn); } +SQLRETURN SQL_API SQLAllocStmt(SQLHDBC conn, SQLHSTMT* stmt) { + return arrow::SQLAllocHandle(SQL_HANDLE_STMT, conn, stmt); +} + SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { return arrow::SQLFreeHandle(type, handle); } @@ -53,6 +57,10 @@ SQLRETURN SQL_API SQLFreeConnect(SQLHDBC conn) { return arrow::SQLFreeHandle(SQL_HANDLE_DBC, conn); } +SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT stmt, SQLUSMALLINT option) { + return arrow::SQLFreeStmt(stmt, option); +} + SQLRETURN SQL_API SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index b3deb11bbb7..9c734e072a9 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -26,6 +26,7 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_environment.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" #if defined _WIN32 || defined _WIN64 @@ -85,9 +86,30 @@ SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) } case SQL_HANDLE_STMT: { - return SQL_INVALID_HANDLE; + using ODBC::ODBCConnection; + using ODBC::ODBCStatement; + + *result = SQL_NULL_HSTMT; + + ODBCConnection* connection = reinterpret_cast(parent); + + return ODBCConnection::ExecuteWithDiagnostics(connection, SQL_ERROR, [=]() { + std::shared_ptr statement = connection->createStatement(); + + if (statement) { + *result = reinterpret_cast(statement.get()); + + return SQL_SUCCESS; + } + + return SQL_ERROR; + }); } + // TODO Implement for case of descriptor + case SQL_HANDLE_DESC: + return SQL_INVALID_HANDLE; + default: break; } @@ -127,8 +149,19 @@ SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { return SQL_SUCCESS; } - case SQL_HANDLE_STMT: - return SQL_INVALID_HANDLE; + case SQL_HANDLE_STMT: { + using ODBC::ODBCStatement; + + ODBCStatement* statement = reinterpret_cast(handle); + + if (!statement) { + return SQL_INVALID_HANDLE; + } + + statement->releaseStatement(); + + return SQL_SUCCESS; + } case SQL_HANDLE_DESC: return SQL_INVALID_HANDLE; @@ -140,6 +173,43 @@ SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { return SQL_ERROR; } +SQLRETURN SQLFreeStmt(SQLHSTMT handle, SQLUSMALLINT option) { + switch (option) { + case SQL_CLOSE: { + using ODBC::ODBCStatement; + + ODBCStatement* statement = reinterpret_cast(handle); + + return ODBCStatement::ExecuteWithDiagnostics(statement, SQL_ERROR, [=]() { + if (!statement) { + return SQL_INVALID_HANDLE; + } + + // Close cursor with suppressErrors set to true + statement->closeCursor(true); + + return SQL_SUCCESS; + }); + } + + case SQL_DROP: { + return SQLFreeHandle(SQL_HANDLE_STMT, handle); + } + + // TODO Implement SQLBindCol + case SQL_UNBIND: { + return SQL_SUCCESS; + } + + // SQLBindParameter is not supported + case SQL_RESET_PARAMS: { + return SQL_SUCCESS; + } + } + + return SQL_ERROR; +} + inline bool IsValidStringFieldArgs(SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, bool isUnicode) { const SQLSMALLINT charSize = isUnicode ? GetSqlWCharSize() : sizeof(char); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index d5c392bcf59..9350cead384 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -31,6 +31,7 @@ namespace arrow { SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result); SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle); +SQLRETURN SQLFreeStmt(SQLHSTMT stmt, SQLUSMALLINT option); SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 8461ead6cf9..176129ba627 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -991,6 +991,93 @@ TYPED_TEST(FlightSQLODBCTestBase, TestConnect) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLAllocFreeStmt) { + this->connect(); + SQLHSTMT statement; + + // Allocate a statement using alloc statement + SQLRETURN ret = SQLAllocStmt(this->conn, &statement); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // TODO Uncomment once SQLExecDirect is implemented + // SQLWCHAR sql_buffer[ODBC_BUFFER_SIZE] = L"SELECT 1"; + // ret = SQLExecDirect(statement, sql_buffer, SQL_NTS); + + // EXPECT_TRUE(ret == SQL_SUCCESS); + + // ret = SQLFreeStmt(statement, SQL_CLOSE); + + // EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free statement handle + ret = SQLFreeStmt(statement, SQL_DROP); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { + // Test is disabled as disconnecting without closing statement fails on Windows. + // This test case can be potentially used on macOS/Linux. + GTEST_SKIP(); + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + SQLHSTMT statement; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Connect string + std::string connect_str = this->getConnectionString(); + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE] = L""; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Allocate a statement using alloc statement + ret = SQLAllocStmt(conn, &statement); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Disconnect from ODBC without closing the statement first + ret = SQLDisconnect(conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_TRUE(ret == SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_TRUE(ret == SQL_SUCCESS); +} + } // namespace integration_tests } // namespace odbc } // namespace flight diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index 656c221a1d1..c079c3c175c 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -65,11 +65,21 @@ void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { // Assert connection is successful before we continue ASSERT_TRUE(ret == SQL_SUCCESS); + + // Allocate a statement using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_STMT, conn, &stmt); + + ASSERT_TRUE(ret == SQL_SUCCESS); } void FlightSQLODBCRemoteTestBase::disconnect() { + // Close statement + SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); + + EXPECT_TRUE(ret == SQL_SUCCESS); + // Disconnect from ODBC - SQLRETURN ret = SQLDisconnect(conn); + ret = SQLDisconnect(conn); if (ret != SQL_SUCCESS) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; From 6475502eb9988344625789d90292b2b380335d3a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 13 Jun 2025 10:39:51 -0700 Subject: [PATCH 16/74] Follow-up DBT Labs connection fix --- .../flight/sql/odbc/flight_sql/flight_sql_auth_method.cc | 9 ++++++--- .../flight/sql/odbc/flight_sql/flight_sql_connection.cc | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc index 4b66c30dab0..6c265673837 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc @@ -107,7 +107,9 @@ class UserPasswordAuthMethod : public FlightSqlAuthMethod { throw odbcabstraction::DriverException(bearer_result.status().message()); } - call_options.headers.push_back(bearer_result.ValueOrDie()); + // call_options may have already been populated with data from the connection string + // or DSN. Ensure auth-generated headers are placed at the front of the header list. + call_options.headers.insert(call_options.headers.begin(), bearer_result.ValueOrDie()); } std::string GetUser() override { return user_; } @@ -129,10 +131,11 @@ class TokenAuthMethod : public FlightSqlAuthMethod { void Authenticate(FlightSqlConnection& connection, FlightCallOptions& call_options) override { - // add the token to the headers + // add the token to the front of the headers. For consistency auth headers should be + // at the front. const std::pair token_header("authorization", "Bearer " + token_); - call_options.headers.push_back(token_header); + call_options.headers.insert(call_options.headers.begin(), token_header); const arrow::Status status = client_.Authenticate( call_options, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc index 54e712feffb..422316a9f8b 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc @@ -175,6 +175,7 @@ void FlightSqlConnection::Connect(const ConnPropertyMap& properties, std::unique_ptr flight_client; ThrowIfNotOK(FlightClient::Connect(location, client_options).Value(&flight_client)); + PopulateMetadataSettings(properties); PopulateCallOptions(properties); std::unique_ptr auth_method = @@ -190,8 +191,6 @@ void FlightSqlConnection::Connect(const ConnPropertyMap& properties, info_.SetProperty(SQL_USER_NAME, auth_method->GetUser()); attribute_[CONNECTION_DEAD] = static_cast(SQL_FALSE); - - PopulateMetadataSettings(properties); } catch (...) { attribute_[CONNECTION_DEAD] = static_cast(SQL_TRUE); sql_client_.reset(); From 787157f14ad262aa06279f51bb50d6a9d49a639a Mon Sep 17 00:00:00 2001 From: rscales Date: Mon, 16 Jun 2025 12:31:16 -0700 Subject: [PATCH 17/74] Implement SQLGetDiag Rec and Field for statement --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 55 +++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 9c734e072a9..21d060ad70f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -234,6 +234,7 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, using ODBC::GetStringAttribute; using ODBC::ODBCConnection; using ODBC::ODBCEnvironment; + using ODBC::ODBCStatement; LOG_DEBUG( "SQLGetDiagFieldW called with handleType: {}, handle: {}, recNumber: {}, " @@ -277,7 +278,9 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, } case SQL_HANDLE_STMT: { - return SQL_ERROR; + ODBCStatement* statement = reinterpret_cast(handle); + diagnostics = &statement->GetDiagnostics(); + break; } default: @@ -308,12 +311,44 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, return SQL_SUCCESS; } - // TODO Implement statement header functions - case SQL_DIAG_CURSOR_ROW_COUNT: + case SQL_DIAG_CURSOR_ROW_COUNT: { + if (handleType == SQL_HANDLE_STMT) { + if (diagInfoPtr) { + // Will always be 0 if only select supported + *static_cast(diagInfoPtr) = 0; + } + + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLLEN); + } + + return SQL_SUCCESS; + } + + return SQL_ERROR; + } + + // Not supported case SQL_DIAG_DYNAMIC_FUNCTION: - case SQL_DIAG_DYNAMIC_FUNCTION_CODE: + case SQL_DIAG_DYNAMIC_FUNCTION_CODE: { + if (handleType == SQL_HANDLE_STMT) { + return SQL_SUCCESS; + } + + return SQL_ERROR; + } + case SQL_DIAG_ROW_COUNT: { if (handleType == SQL_HANDLE_STMT) { + if (diagInfoPtr) { + // Will always be 0 if only select supported + *static_cast(diagInfoPtr) = 0; + } + + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLLEN); + } + return SQL_SUCCESS; } @@ -372,8 +407,11 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, } case SQL_HANDLE_STMT: { - // TODO Implement for case of statement - return SQL_ERROR; + ODBCStatement* statement = reinterpret_cast(handle); + ODBCConnection* connection = &statement->GetConnection(); + std::string dsn = connection->GetDSN(); + return GetStringAttribute(isUnicode, dsn, true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); } default: @@ -445,10 +483,10 @@ SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT r SQLWCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLengthPtr) { using driver::odbcabstraction::Diagnostics; - using ODBC::ConvertToSqlWChar; using ODBC::GetStringAttribute; using ODBC::ODBCConnection; using ODBC::ODBCEnvironment; + using ODBC::ODBCStatement; LOG_DEBUG( "SQLGetDiagRecW called with handleType: {}, handle: {}, recNumber: {}, " @@ -488,7 +526,8 @@ SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT r } case SQL_HANDLE_STMT: { - return SQL_ERROR; + auto* statement = ODBCStatement::of(handle); + diagnostics = &statement->GetDiagnostics(); } default: From e232a05beccdf6b38ac726247f129fd340bb57c4 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:18:45 -0700 Subject: [PATCH 18/74] Unicode support for DSN ODBC APIs * Let compiler append `W` to ODBC APIs where applicable. --- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 2 + cpp/src/arrow/flight/sql/odbc/entry_points.cc | 155 ++++++++--------- .../odbc/flight_sql/config/configuration.cc | 48 +++-- .../odbc/flight_sql/flight_sql_connection.cc | 2 +- .../include/flight_sql/config/configuration.h | 1 + .../flight_sql/ui/add_property_window.h | 6 +- .../include/flight_sql/ui/custom_window.h | 2 +- .../flight_sql/include/flight_sql/ui/window.h | 32 ++-- .../flight/sql/odbc/flight_sql/system_dsn.cc | 25 +-- .../flight/sql/odbc/flight_sql/system_dsn.h | 4 +- .../sql/odbc/flight_sql/system_trust_store.cc | 15 +- .../sql/odbc/flight_sql/system_trust_store.h | 4 +- .../odbc/flight_sql/ui/add_property_window.cc | 18 +- .../sql/odbc/flight_sql/ui/custom_window.cc | 2 +- .../flight_sql/ui/dsn_configuration_window.cc | 164 ++++++++++-------- .../flight/sql/odbc/flight_sql/ui/window.cc | 58 +++---- .../sql/odbc/flight_sql/win_system_dsn.cc | 35 ++-- cpp/src/arrow/flight/sql/odbc/odbc.def | 2 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 43 +++-- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 39 ++--- .../odbc_impl/odbc_connection.cc | 35 ++-- .../flight/sql/odbc/tests/CMakeLists.txt | 2 - .../flight/sql/odbc/tests/connection_test.cc | 14 +- .../flight/sql/odbc/tests/odbc_test_suite.cc | 4 +- 24 files changed, 383 insertions(+), 329 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 449db0fedf4..2017fa512bf 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -32,6 +32,8 @@ else() set(ODBCINST odbcinst) endif() +add_definitions(-DUNICODE=1) + add_subdirectory(flight_sql) add_subdirectory(odbcabstraction) add_subdirectory(tests) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index ef6aa2e5fd0..b397631992f 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -61,20 +61,20 @@ SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT stmt, SQLUSMALLINT option) { return arrow::SQLFreeStmt(stmt, option); } -SQLRETURN SQL_API SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, - SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, - SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr) { - return arrow::SQLGetDiagFieldW(handleType, handle, recNumber, diagIdentifier, - diagInfoPtr, bufferLength, stringLengthPtr); +SQLRETURN SQL_API SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + return arrow::SQLGetDiagField(handleType, handle, recNumber, diagIdentifier, + diagInfoPtr, bufferLength, stringLengthPtr); } -SQLRETURN SQL_API SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, - SQLSMALLINT recNumber, SQLWCHAR* sqlState, - SQLINTEGER* nativeErrorPtr, SQLWCHAR* messageText, - SQLSMALLINT bufferLength, SQLSMALLINT* textLengthPtr) { - return arrow::SQLGetDiagRecW(handleType, handle, recNumber, sqlState, nativeErrorPtr, - messageText, bufferLength, textLengthPtr); +SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, + SQLSMALLINT recNumber, SQLWCHAR* sqlState, + SQLINTEGER* nativeErrorPtr, SQLWCHAR* messageText, + SQLSMALLINT bufferLength, SQLSMALLINT* textLengthPtr) { + return arrow::SQLGetDiagRec(handleType, handle, recNumber, sqlState, nativeErrorPtr, + messageText, bufferLength, textLengthPtr); } SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, @@ -87,36 +87,35 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePt return arrow::SQLSetEnvAttr(env, attr, valuePtr, strLen); } -SQLRETURN SQL_API SQLSetConnectAttrW(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, - SQLINTEGER valueLen) { +SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, + SQLINTEGER valueLen) { LOG_DEBUG("SQLSetConnectAttrW called with conn: {}, attr: {}, value: {}, valueLen: {}", conn, attr, value, valueLen); return SQL_ERROR; } -SQLRETURN SQL_API SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, - SQLPOINTER infoValuePtr, SQLSMALLINT bufLen, - SQLSMALLINT* length) { - return arrow::SQLGetInfoW(conn, infoType, infoValuePtr, bufLen, length); +SQLRETURN SQL_API SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, + SQLSMALLINT bufLen, SQLSMALLINT* length) { + return arrow::SQLGetInfo(conn, infoType, infoValuePtr, bufLen, length); } -SQLRETURN SQL_API SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, - SQLWCHAR* inConnectionString, - SQLSMALLINT inConnectionStringLen, - SQLWCHAR* outConnectionString, - SQLSMALLINT outConnectionStringBufferLen, - SQLSMALLINT* outConnectionStringLen, - SQLUSMALLINT driverCompletion) { - return arrow::SQLDriverConnectW( +SQLRETURN SQL_API SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion) { + return arrow::SQLDriverConnect( conn, windowHandle, inConnectionString, inConnectionStringLen, outConnectionString, outConnectionStringBufferLen, outConnectionStringLen, driverCompletion); } -SQLRETURN SQL_API SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, - SQLWCHAR* userName, SQLSMALLINT userNameLen, - SQLWCHAR* password, SQLSMALLINT passwordLen) { - return arrow::SQLConnectW(conn, dsnName, dsnNameLen, userName, userNameLen, password, - passwordLen); +SQLRETURN SQL_API SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, + SQLWCHAR* password, SQLSMALLINT passwordLen) { + return arrow::SQLConnect(conn, dsnName, dsnNameLen, userName, userNameLen, password, + passwordLen); } SQLRETURN SQL_API SQLDisconnect(SQLHDBC conn) { return arrow::SQLDisconnect(conn); } @@ -142,11 +141,11 @@ SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { return SQL_ERROR; } -SQLRETURN SQL_API SQLColAttributeW(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, - SQLUSMALLINT fieldIdentifier, - SQLPOINTER characterAttributePtr, - SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, - SQLLEN* numericAttributePtr) { +SQLRETURN SQL_API SQLColAttribute(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, + SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, + SQLLEN* numericAttributePtr) { LOG_DEBUG( "SQLColAttributeW called with statementHandle: {}, columnNumber: {}, " "fieldIdentifier: {}, characterAttributePtr: {}, bufferLength: {}, " @@ -156,11 +155,11 @@ SQLRETURN SQL_API SQLColAttributeW(SQLHSTMT statementHandle, SQLUSMALLINT column return SQL_ERROR; } -SQLRETURN SQL_API SQLColumnsW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, - SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, - SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, - SQLSMALLINT tableNameLength, SQLWCHAR* columnName, - SQLSMALLINT columnNameLength) { +SQLRETURN SQL_API SQLColumns(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength, SQLWCHAR* columnName, + SQLSMALLINT columnNameLength) { LOG_DEBUG( "SQLColumnsW called with statementHandle: {}, catalogName: {}, catalogNameLength: " "{}, " @@ -173,10 +172,10 @@ SQLRETURN SQL_API SQLColumnsW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, return SQL_ERROR; } -SQLRETURN SQL_API SQLErrorW(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, - SQLWCHAR FAR* szSqlState, SQLINTEGER FAR* pfNativeError, - SQLWCHAR FAR* szErrorMsg, SQLSMALLINT cbErrorMsgMax, - SQLSMALLINT FAR* pcbErrorMsg) { +SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, + SQLWCHAR FAR* szSqlState, SQLINTEGER FAR* pfNativeError, + SQLWCHAR FAR* szErrorMsg, SQLSMALLINT cbErrorMsgMax, + SQLSMALLINT FAR* pcbErrorMsg) { LOG_DEBUG( "SQLErrorW called with handleType: {}, handle: {}, hstmt: {}, szSqlState: {}, " "pfNativeError: {}, szErrorMsg: {}, cbErrorMsgMax: {}, pcbErrorMsg: {}", @@ -185,8 +184,8 @@ SQLRETURN SQL_API SQLErrorW(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, return SQL_ERROR; } -SQLRETURN SQL_API SQLExecDirectW(SQLHSTMT statementHandle, SQLWCHAR* statementText, - SQLINTEGER textLength) { +SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, SQLWCHAR* statementText, + SQLINTEGER textLength) { LOG_DEBUG( "SQLExecDirectW called with statementHandle: {}, statementText: {}, textLength: {}", statementHandle, fmt::ptr(statementText), textLength); @@ -203,13 +202,13 @@ SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { return SQL_ERROR; } -SQLRETURN SQL_API SQLForeignKeysW(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, - SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, - SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, - SQLSMALLINT pKTableNameLength, SQLWCHAR* fKCatalogName, - SQLSMALLINT fKCatalogNameLength, SQLWCHAR* fKSchemaName, - SQLSMALLINT fKSchemaNameLength, SQLWCHAR* fKTableName, - SQLSMALLINT fKTableNameLength) { +SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, + SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, + SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, + SQLSMALLINT pKTableNameLength, SQLWCHAR* fKCatalogName, + SQLSMALLINT fKCatalogNameLength, SQLWCHAR* fKSchemaName, + SQLSMALLINT fKSchemaNameLength, SQLWCHAR* fKTableName, + SQLSMALLINT fKTableNameLength) { LOG_DEBUG( "SQLForeignKeysW called with statementHandle: {}, pKCatalogName: {}, " "pKCatalogNameLength: " @@ -226,9 +225,9 @@ SQLRETURN SQL_API SQLForeignKeysW(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogN return SQL_ERROR; } -SQLRETURN SQL_API SQLGetConnectAttrW(SQLHDBC connectionHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER bufferLength, - SQLINTEGER* stringLengthPtr) { +SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { LOG_DEBUG( "SQLGetConnectAttrW called with connectionHandle: {}, attribute: {}, valuePtr: {}, " "bufferLength: {}, stringLengthPtr: {}", @@ -247,9 +246,9 @@ SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLUSMALLINT col_or_Param return SQL_ERROR; } -SQLRETURN SQL_API SQLGetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER bufferLength, - SQLINTEGER* stringLengthPtr) { +SQLRETURN SQL_API SQLGetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { LOG_DEBUG( "SQLGetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " "bufferLength: {}, stringLengthPtr: {}", @@ -257,7 +256,7 @@ SQLRETURN SQL_API SQLGetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute return SQL_ERROR; } -SQLRETURN SQL_API SQLGetTypeInfoW(SQLHSTMT statementHandle, SQLSMALLINT dataType) { +SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { LOG_DEBUG("SQLGetTypeInfoW called with statementHandle: {} dataType: {}", statementHandle, dataType); return SQL_ERROR; @@ -268,10 +267,10 @@ SQLRETURN SQL_API SQLMoreResults(SQLHSTMT statementHandle) { return SQL_ERROR; } -SQLRETURN SQL_API SQLNativeSqlW(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, - SQLINTEGER inStatementTextLength, - SQLWCHAR* outStatementText, SQLINTEGER bufferLength, - SQLINTEGER* outStatementTextLength) { +SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, + SQLINTEGER inStatementTextLength, + SQLWCHAR* outStatementText, SQLINTEGER bufferLength, + SQLINTEGER* outStatementTextLength) { LOG_DEBUG( "SQLNativeSqlW called with connectionHandle: {}, inStatementText: {}, " "inStatementTextLength: " @@ -288,18 +287,18 @@ SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, return SQL_ERROR; } -SQLRETURN SQL_API SQLPrepareW(SQLHSTMT statementHandle, SQLWCHAR* statementText, - SQLINTEGER textLength) { +SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, SQLWCHAR* statementText, + SQLINTEGER textLength) { LOG_DEBUG( "SQLPrepareW called with statementHandle: {}, statementText: {}, textLength: {}", statementHandle, fmt::ptr(statementText), textLength); return SQL_ERROR; } -SQLRETURN SQL_API SQLPrimaryKeysW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, - SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, - SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, - SQLSMALLINT tableNameLength) { +SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength) { LOG_DEBUG( "SQLPrimaryKeysW called with statementHandle: {}, catalogName: {}, " "catalogNameLength: " @@ -309,8 +308,8 @@ SQLRETURN SQL_API SQLPrimaryKeysW(SQLHSTMT statementHandle, SQLWCHAR* catalogNam return SQL_ERROR; } -SQLRETURN SQL_API SQLSetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER stringLength) { +SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER stringLength) { LOG_DEBUG( "SQLSetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " "stringLength: {}", @@ -318,11 +317,11 @@ SQLRETURN SQL_API SQLSetStmtAttrW(SQLHSTMT statementHandle, SQLINTEGER attribute return SQL_ERROR; } -SQLRETURN SQL_API SQLTablesW(SQLHSTMT statementHandle, SQLWCHAR* catalogName, - SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, - SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, - SQLSMALLINT tableNameLength, SQLWCHAR* tableType, - SQLSMALLINT tableTypeLength) { +SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength, SQLWCHAR* tableType, + SQLSMALLINT tableTypeLength) { LOG_DEBUG( "SQLTablesW called with statementHandle: {}, catalogName: {}, catalogNameLength: " "{}, " diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc index 7cebebe56eb..db18239c47a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/config/configuration.cc @@ -17,6 +17,8 @@ #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" +#include "arrow/result.h" +#include "arrow/util/utf8.h" #include #include @@ -27,7 +29,6 @@ namespace driver { namespace flight_sql { namespace config { - static const char DEFAULT_DSN[] = "Apache Arrow Flight SQL"; static const char DEFAULT_ENABLE_ENCRYPTION[] = TRUE_STR; static const char DEFAULT_USE_CERT_STORE[] = TRUE_STR; @@ -36,19 +37,27 @@ static const char DEFAULT_DISABLE_CERT_VERIFICATION[] = FALSE_STR; namespace { std::string ReadDsnString(const std::string& dsn, const std::string_view& key, const std::string& dflt = "") { + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); + std::wstring wKey = arrow::util::UTF8ToWideString(key).ValueOr(L""); + std::wstring wDflt = arrow::util::UTF8ToWideString(dflt).ValueOr(L""); + #define BUFFER_SIZE (1024) - std::vector buf(BUFFER_SIZE); - int ret = SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), - static_cast(buf.size()), "ODBC.INI"); + std::vector buf(BUFFER_SIZE); + int ret = + SQLGetPrivateProfileString(wDsn.c_str(), wKey.c_str(), wDflt.c_str(), buf.data(), + static_cast(buf.size()), L"ODBC.INI"); if (ret > BUFFER_SIZE) { // If there wasn't enough space, try again with the right size buffer. buf.resize(ret + 1); - ret = SQLGetPrivateProfileString(dsn.c_str(), key.data(), dflt.c_str(), buf.data(), - static_cast(buf.size()), "ODBC.INI"); + ret = + SQLGetPrivateProfileString(wDsn.c_str(), wKey.c_str(), wDflt.c_str(), buf.data(), + static_cast(buf.size()), L"ODBC.INI"); } - return std::string(buf.data(), ret); + std::wstring wResult = std::wstring(buf.data(), ret); + std::string result = arrow::util::WideStringToUTF8(wResult).ValueOr(""); + return result; } void RemoveAllKnownKeys(std::vector& keys) { @@ -65,28 +74,32 @@ void RemoveAllKnownKeys(std::vector& keys) { } std::vector ReadAllKeys(const std::string& dsn) { - std::vector buf(BUFFER_SIZE); + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); + + std::vector buf(BUFFER_SIZE); - int ret = SQLGetPrivateProfileString(dsn.c_str(), NULL, "", buf.data(), - static_cast(buf.size()), "ODBC.INI"); + int ret = SQLGetPrivateProfileString(wDsn.c_str(), NULL, L"", buf.data(), + static_cast(buf.size()), L"ODBC.INI"); if (ret > BUFFER_SIZE) { // If there wasn't enough space, try again with the right size buffer. buf.resize(ret + 1); - ret = SQLGetPrivateProfileString(dsn.c_str(), NULL, "", buf.data(), - static_cast(buf.size()), "ODBC.INI"); + ret = SQLGetPrivateProfileString(wDsn.c_str(), NULL, L"", buf.data(), + static_cast(buf.size()), L"ODBC.INI"); } // When you pass NULL to SQLGetPrivateProfileString it gives back a \0 delimited list of // all the keys. The below loop simply tokenizes all the keys and places them into a // vector. std::vector keys; - char* begin = buf.data(); + wchar_t* begin = buf.data(); while (begin && *begin != '\0') { - char* cur; + wchar_t* cur; for (cur = begin; *cur != '\0'; ++cur) { } - keys.emplace_back(begin, cur); + + std::string key = arrow::util::WideStringToUTF8(std::wstring(begin, cur)).ValueOr(""); + keys.emplace_back(key); begin = ++cur; } return keys; @@ -150,6 +163,11 @@ const std::string& Configuration::Get(const std::string_view& key) const { return itr->second; } +void Configuration::Set(const std::string_view& key, const std::wstring& wValue) { + std::string value = arrow::util::WideStringToUTF8(wValue).ValueOr(""); + Set(key, value); +} + void Configuration::Set(const std::string_view& key, const std::string& value) { const std::string copy = boost::trim_copy(value); if (!copy.empty()) { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc index 422316a9f8b..6d5d95865ba 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc @@ -83,7 +83,7 @@ namespace { #if _WIN32 || _WIN64 constexpr auto SYSTEM_TRUST_STORE_DEFAULT = true; -constexpr auto STORES = {"CA", "MY", "ROOT", "SPC"}; +constexpr auto STORES = {L"CA", L"MY", L"ROOT", L"SPC"}; inline std::string GetCerts() { std::string certs; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h index 8c4d6865505..c94cc5b7832 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h @@ -52,6 +52,7 @@ class Configuration { void Clear(); bool IsSet(const std::string_view& key) const; const std::string& Get(const std::string_view& key) const; + void Set(const std::string_view& key, const std::wstring& wValue); void Set(const std::string_view& key, const std::string& value); void Emplace(const std::string_view& key, std::string&& value); /** diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/add_property_window.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/add_property_window.h index 01d93829a46..b7a8016447c 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/add_property_window.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/add_property_window.h @@ -70,7 +70,7 @@ class AddPropertyWindow : public CustomWindow { * * @return true if the dialog was OK'd, false otherwise. */ - bool GetProperty(std::string& key, std::string& value); + bool GetProperty(std::wstring& key, std::wstring& value); private: /** @@ -97,9 +97,9 @@ class AddPropertyWindow : public CustomWindow { std::unique_ptr valueEdit; - std::string key; + std::wstring key; - std::string value; + std::wstring value; /** Window width. */ int width; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/custom_window.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/custom_window.h index 0fc3737ed8b..649f0ef6547 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/custom_window.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/custom_window.h @@ -65,7 +65,7 @@ class CustomWindow : public Window { * @param className Window class name. * @param title Window title. */ - CustomWindow(Window* parent, const char* className, const char* title); + CustomWindow(Window* parent, const wchar_t* className, const wchar_t* title); /** * Destructor. diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h index e56ad88dec6..596ff47c577 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/window.h @@ -44,7 +44,7 @@ class Window { * @param className Window class name. * @param title Window title. */ - Window(Window* parent, const char* className, const char* title); + Window(Window* parent, const wchar_t* className, const wchar_t* title); /** * Constructor for the existing window. @@ -102,7 +102,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateGroupBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id); + const wchar_t* title, int id); /** * Create child label window. @@ -116,7 +116,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateLabel(int posX, int posY, int sizeX, int sizeY, - const char* title, int id); + const wchar_t* title, int id); /** * Create child Edit window. @@ -131,7 +131,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateEdit(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, int style = 0); + const wchar_t* title, int id, int style = 0); /** * Create child button window. @@ -146,7 +146,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateButton(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, int style = 0); + const wchar_t* title, int id, int style = 0); /** * Create child CheckBox window. @@ -161,7 +161,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateCheckBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, bool state); + const wchar_t* title, int id, bool state); /** * Create child ComboBox window. @@ -175,7 +175,7 @@ class Window { * @return Auto pointer containing new window. */ std::unique_ptr CreateComboBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id); + const wchar_t* title, int id); /** * Show window. @@ -201,15 +201,15 @@ class Window { void SetVisible(bool isVisible); - void ListAddColumn(const std::string& name, int index, int width); + void ListAddColumn(const std::wstring& name, int index, int width); - void ListAddItem(const std::vector& items); + void ListAddItem(const std::vector& items); void ListDeleteSelectedItem(); - std::vector > ListGetAll(); + std::vector > ListGetAll(); - void AddTab(const std::string& name, int index); + void AddTab(const std::wstring& name, int index); bool IsTextEmpty() const; @@ -218,14 +218,14 @@ class Window { * * @param text Text. */ - void GetText(std::string& text) const; + void GetText(std::wstring& text) const; /** * Set window text. * * @param text Text. */ - void SetText(const std::string& text) const; + void SetText(const std::wstring& text) const; /** * Get CheckBox state. @@ -246,7 +246,7 @@ class Window { * * @param str String. */ - void AddString(const std::string& str); + void AddString(const std::wstring& str); /** * Set current ComboBox selection. @@ -285,10 +285,10 @@ class Window { void SetHandle(HWND value) { handle = value; } /** Window class name. */ - std::string className; + std::wstring className; /** Window title. */ - std::string title; + std::wstring title; /** Window handle. */ HWND handle; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc index 95b47bdb1e2..f0006b36c9a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.cc @@ -19,6 +19,8 @@ #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" +#include "arrow/result.h" +#include "arrow/util/utf8.h" #include #include @@ -29,14 +31,14 @@ using driver::flight_sql::config::Configuration; void PostLastInstallerError() { #define BUFFER_SIZE (1024) DWORD code; - char msg[BUFFER_SIZE]; + wchar_t msg[BUFFER_SIZE]; SQLInstallerError(1, &code, msg, BUFFER_SIZE, NULL); - std::stringstream buf; - buf << "Message: \"" << msg << "\", Code: " << code; - std::string errorMsg = buf.str(); + std::wstringstream buf; + buf << L"Message: \"" << msg << L"\", Code: " << code; + std::wstring errorMsg = buf.str(); - MessageBox(NULL, errorMsg.c_str(), "Error!", MB_ICONEXCLAMATION | MB_OK); + MessageBox(NULL, errorMsg.c_str(), L"Error!", MB_ICONEXCLAMATION | MB_OK); SQLPostInstallerError(code, errorMsg.c_str()); } @@ -46,7 +48,7 @@ void PostLastInstallerError() { * @param dsn DSN name. * @return True on success and false on fail. */ -bool UnregisterDsn(const std::string& dsn) { +bool UnregisterDsn(const std::wstring& dsn) { if (SQLRemoveDSNFromIni(dsn.c_str())) { return true; } @@ -62,10 +64,11 @@ bool UnregisterDsn(const std::string& dsn) { * @param driver Driver. * @return True on success and false on fail. */ -bool RegisterDsn(const Configuration& config, LPCSTR driver) { +bool RegisterDsn(const Configuration& config, LPCWSTR driver) { const std::string& dsn = config.Get(FlightSqlConnection::DSN); + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); - if (!SQLWriteDSNToIni(dsn.c_str(), driver)) { + if (!SQLWriteDSNToIni(wDsn.c_str(), driver)) { PostLastInstallerError(); return false; } @@ -78,8 +81,10 @@ bool RegisterDsn(const Configuration& config, LPCSTR driver) { continue; } - if (!SQLWritePrivateProfileString(dsn.c_str(), key.data(), it->second.c_str(), - "ODBC.INI")) { + std::wstring wKey = arrow::util::UTF8ToWideString(key).ValueOr(L""); + std::wstring wValue = arrow::util::UTF8ToWideString(it->second).ValueOr(L""); + if (!SQLWritePrivateProfileString(wDsn.c_str(), wKey.c_str(), wValue.c_str(), + L"ODBC.INI")) { PostLastInstallerError(); return false; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h index f3744d3428a..1ac9b4d9b80 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_dsn.h @@ -54,7 +54,7 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config, * @param driver Driver. * @return True on success and false on fail. */ -bool RegisterDsn(const Configuration& config, LPCSTR driver); +bool RegisterDsn(const Configuration& config, LPCWSTR driver); /** * Unregister specified DSN. @@ -62,4 +62,4 @@ bool RegisterDsn(const Configuration& config, LPCSTR driver); * @param dsn DSN name. * @return True on success and false on fail. */ -bool UnregisterDsn(const std::string& dsn); +bool UnregisterDsn(const std::wstring& dsn); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.cc index 67db1fc35be..ebc8fd90adf 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.cc @@ -15,6 +15,9 @@ // specific language governing permissions and limitations // under the License. +#include "arrow/result.h" +#include "arrow/util/utf8.h" + #include "arrow/flight/sql/odbc/flight_sql/system_trust_store.h" #if defined _WIN32 || defined _WIN64 @@ -32,18 +35,20 @@ std::string SystemTrustStore::GetNext() const { CryptBinaryToString(p_context_->pbCertEncoded, p_context_->cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &size); - std::string cert; - cert.resize(size); + std::wstring wCert; + wCert.resize(size); CryptBinaryToString(p_context_->pbCertEncoded, p_context_->cbCertEncoded, - CRYPT_STRING_BASE64HEADER, &cert[0], &size); - cert.resize(size); + CRYPT_STRING_BASE64HEADER, &wCert[0], &size); + wCert.resize(size); + + std::string cert = arrow::util::WideStringToUTF8(wCert).ValueOr(""); return cert; } bool SystemTrustStore::SystemHasStore() { return h_store_ != nullptr; } -SystemTrustStore::SystemTrustStore(const char* store) +SystemTrustStore::SystemTrustStore(const wchar_t* store) : stores_(store), h_store_(CertOpenSystemStore(NULL, store)), p_context_(nullptr) {} SystemTrustStore::~SystemTrustStore() { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h index f8e02fea526..0ff3adc2f48 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/system_trust_store.h @@ -41,12 +41,12 @@ namespace flight_sql { /// https://github.com/apache/drill/blob/master/contrib/native/client/src/clientlib/wincert.ipp. class SystemTrustStore { private: - const char* stores_; + const wchar_t* stores_; HCERTSTORE h_store_; PCCERT_CONTEXT p_context_; public: - explicit SystemTrustStore(const char* store); + explicit SystemTrustStore(const wchar_t* store); ~SystemTrustStore(); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc index 75aa491f781..15799c1f9a2 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/add_property_window.cc @@ -33,7 +33,7 @@ namespace flight_sql { namespace config { AddPropertyWindow::AddPropertyWindow(Window* parent) - : CustomWindow(parent, "AddProperty", "Add Property"), + : CustomWindow(parent, L"AddProperty", L"Add Property"), width(300), height(120), accepted(false), @@ -69,7 +69,7 @@ void AddPropertyWindow::Create() { } } -bool AddPropertyWindow::GetProperty(std::string& key, std::string& value) { +bool AddPropertyWindow::GetProperty(std::wstring& key, std::wstring& value) { if (accepted) { key = this->key; value = this->value; @@ -87,10 +87,10 @@ void AddPropertyWindow::OnCreate() { int cancelPosX = width - MARGIN - BUTTON_WIDTH; int okPosX = cancelPosX - INTERVAL - BUTTON_WIDTH; - okButton = CreateButton(okPosX, groupPosY, BUTTON_WIDTH, BUTTON_HEIGHT, "Ok", + okButton = CreateButton(okPosX, groupPosY, BUTTON_WIDTH, BUTTON_HEIGHT, L"Ok", ChildId::OK_BUTTON, BS_DEFPUSHBUTTON); cancelButton = CreateButton(cancelPosX, groupPosY, BUTTON_WIDTH, BUTTON_HEIGHT, - "Cancel", ChildId::CANCEL_BUTTON); + L"Cancel", ChildId::CANCEL_BUTTON); isInitialized = true; CheckEnableOk(); } @@ -104,15 +104,15 @@ int AddPropertyWindow::CreateEdits(int posX, int posY, int sizeX) { int rowPos = posY; labels.push_back( - CreateLabel(posX, rowPos, LABEL_WIDTH, ROW_HEIGHT, "Key:", ChildId::KEY_LABEL)); - keyEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, "", ChildId::KEY_EDIT); + CreateLabel(posX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Key:", ChildId::KEY_LABEL)); + keyEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, L"", ChildId::KEY_EDIT); rowPos += INTERVAL + ROW_HEIGHT; - labels.push_back( - CreateLabel(posX, rowPos, LABEL_WIDTH, ROW_HEIGHT, "Value:", ChildId::VALUE_LABEL)); + labels.push_back(CreateLabel(posX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Value:", + ChildId::VALUE_LABEL)); valueEdit = - CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, "", ChildId::VALUE_EDIT); + CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, L"", ChildId::VALUE_EDIT); rowPos += INTERVAL + ROW_HEIGHT; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc index 8f660a21329..bde7967c7e9 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/custom_window.cc @@ -84,7 +84,7 @@ LRESULT CALLBACK CustomWindow::WndProc(HWND hwnd, UINT msg, WPARAM wParam, return DefWindowProc(hwnd, msg, wParam, lParam); } -CustomWindow::CustomWindow(Window* parent, const char* className, const char* title) +CustomWindow::CustomWindow(Window* parent, const wchar_t* className, const wchar_t* title) : Window(parent, className, title) { WNDCLASS wcx; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index e469b6b067f..58bf9d37b48 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -15,8 +15,11 @@ // specific language governing permissions and limitations // under the License. -#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/dsn_configuration_window.h" +#include "arrow/result.h" +#include "arrow/util/utf8.h" + #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/ui/dsn_configuration_window.h" #include #include @@ -55,7 +58,7 @@ namespace config { DsnConfigurationWindow::DsnConfigurationWindow(Window* parent, config::Configuration& config) - : CustomWindow(parent, "FlightConfigureDSN", "Configure Apache Arrow Flight SQL"), + : CustomWindow(parent, L"FlightConfigureDSN", L"Configure Apache Arrow Flight SQL"), width(480), height(375), config(config), @@ -91,11 +94,11 @@ void DsnConfigurationWindow::Create() { throw odbcabstraction::DriverException(buf.str()); } } - +//-AL- fix this file next void DsnConfigurationWindow::OnCreate() { tabControl = CreateTabControl(ChildId::TAB_CONTROL); - tabControl->AddTab("Common", COMMON_TAB); - tabControl->AddTab("Advanced", ADVANCED_TAB); + tabControl->AddTab(L"Common", COMMON_TAB); + tabControl->AddTab(L"Advanced", ADVANCED_TAB); int groupPosY = 3 * MARGIN; int groupSizeY = width - 2 * MARGIN; @@ -118,11 +121,11 @@ void DsnConfigurationWindow::OnCreate() { int buttonPosY = std::max(commonGroupPosY, advancedGroupPosY); testButton = CreateButton(testPosX, buttonPosY, BUTTON_WIDTH + 20, BUTTON_HEIGHT, - "Test Connection", ChildId::TEST_CONNECTION_BUTTON); - okButton = CreateButton(okPosX, buttonPosY, BUTTON_WIDTH, BUTTON_HEIGHT, "Ok", + L"Test Connection", ChildId::TEST_CONNECTION_BUTTON); + okButton = CreateButton(okPosX, buttonPosY, BUTTON_WIDTH, BUTTON_HEIGHT, L"Ok", ChildId::OK_BUTTON); cancelButton = CreateButton(cancelPosX, buttonPosY, BUTTON_WIDTH, BUTTON_HEIGHT, - "Cancel", ChildId::CANCEL_BUTTON); + L"Cancel", ChildId::CANCEL_BUTTON); isInitialized = true; CheckEnableOk(); SelectTab(COMMON_TAB); @@ -138,31 +141,35 @@ int DsnConfigurationWindow::CreateConnectionSettingsGroup(int posX, int posY, in int rowPos = posY + 2 * INTERVAL; - const char* val = config.Get(FlightSqlConnection::DSN).c_str(); + std::string val = config.Get(FlightSqlConnection::DSN); + std::wstring wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Data Source Name:", ChildId::NAME_LABEL)); - nameEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, ChildId::NAME_EDIT); + L"Data Source Name:", ChildId::NAME_LABEL)); + nameEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), + ChildId::NAME_EDIT); rowPos += INTERVAL + ROW_HEIGHT; - val = config.Get(FlightSqlConnection::HOST).c_str(); - labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Host Name:", ChildId::SERVER_LABEL)); - serverEdit = - CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, ChildId::SERVER_EDIT); + val = config.Get(FlightSqlConnection::HOST); + wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); + labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Host Name:", + ChildId::SERVER_LABEL)); + serverEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), + ChildId::SERVER_EDIT); rowPos += INTERVAL + ROW_HEIGHT; - val = config.Get(FlightSqlConnection::PORT).c_str(); - labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Port:", ChildId::PORT_LABEL)); - portEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, ChildId::PORT_EDIT, - ES_NUMBER); + val = config.Get(FlightSqlConnection::PORT); + wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); + labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Port:", + ChildId::PORT_LABEL)); + portEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), + ChildId::PORT_EDIT, ES_NUMBER); rowPos += INTERVAL + ROW_HEIGHT; connectionSettingsGroupBox = - CreateGroupBox(posX, posY, sizeX, rowPos - posY, "Connection settings", + CreateGroupBox(posX, posY, sizeX, rowPos - posY, L"Connection settings", ChildId::CONNECTION_SETTINGS_GROUP_BOX); return rowPos - posY; @@ -179,36 +186,39 @@ int DsnConfigurationWindow::CreateAuthSettingsGroup(int posX, int posY, int size int rowPos = posY + 2 * INTERVAL; labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Authentication Type:", ChildId::AUTH_TYPE_LABEL)); + L"Authentication Type:", ChildId::AUTH_TYPE_LABEL)); authTypeComboBox = CreateComboBox(editPosX, rowPos, editSizeX, ROW_HEIGHT, - "Authentication Type:", ChildId::AUTH_TYPE_COMBOBOX); - authTypeComboBox->AddString("Basic Authentication"); - authTypeComboBox->AddString("Token Authentication"); + L"Authentication Type:", ChildId::AUTH_TYPE_COMBOBOX); + authTypeComboBox->AddString(L"Basic Authentication"); + authTypeComboBox->AddString(L"Token Authentication"); rowPos += INTERVAL + ROW_HEIGHT; - const char* val = config.Get(FlightSqlConnection::UID).c_str(); + std::string val = config.Get(FlightSqlConnection::UID); + std::wstring wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); - labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "User:", ChildId::USER_LABEL)); - userEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, ChildId::USER_EDIT); + labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"User:", + ChildId::USER_LABEL)); + userEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), + ChildId::USER_EDIT); rowPos += INTERVAL + ROW_HEIGHT; - val = config.Get(FlightSqlConnection::PWD).c_str(); - labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Password:", ChildId::PASSWORD_LABEL)); - passwordEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, + val = config.Get(FlightSqlConnection::PWD); + wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); + labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Password:", + ChildId::PASSWORD_LABEL)); + passwordEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), ChildId::USER_EDIT, ES_PASSWORD); rowPos += INTERVAL + ROW_HEIGHT; const auto& token = config.Get(FlightSqlConnection::TOKEN); - val = token.c_str(); + wVal = arrow::util::UTF8ToWideString(token).ValueOr(L""); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Authentication Token:", ChildId::AUTH_TOKEN_LABEL)); - authTokenEdit = - CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, val, ChildId::AUTH_TOKEN_EDIT); + L"Authentication Token:", ChildId::AUTH_TOKEN_LABEL)); + authTokenEdit = CreateEdit(editPosX, rowPos, editSizeX, ROW_HEIGHT, wVal.c_str(), + ChildId::AUTH_TOKEN_EDIT); authTokenEdit->SetEnabled(false); // Ensure the right elements are selected. @@ -218,7 +228,7 @@ int DsnConfigurationWindow::CreateAuthSettingsGroup(int posX, int posY, int size rowPos += INTERVAL + ROW_HEIGHT; authSettingsGroupBox = - CreateGroupBox(posX, posY, sizeX, rowPos - posY, "Authentication settings", + CreateGroupBox(posX, posY, sizeX, rowPos - posY, L"Authentication settings", ChildId::AUTH_SETTINGS_GROUP_BOX); return rowPos - posY; @@ -234,37 +244,38 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in int rowPos = posY + 2 * INTERVAL; - const char* val = config.Get(FlightSqlConnection::USE_ENCRYPTION).c_str(); + std::string val = config.Get(FlightSqlConnection::USE_ENCRYPTION); const bool enableEncryption = driver::odbcabstraction::AsBool(val).value_or(true); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Use Encryption:", ChildId::ENABLE_ENCRYPTION_LABEL)); + L"Use Encryption:", ChildId::ENABLE_ENCRYPTION_LABEL)); enableEncryptionCheckBox = - CreateCheckBox(editPosX, rowPos - 2, editSizeX, ROW_HEIGHT, "", + CreateCheckBox(editPosX, rowPos - 2, editSizeX, ROW_HEIGHT, L"", ChildId::ENABLE_ENCRYPTION_CHECKBOX, enableEncryption); rowPos += INTERVAL + ROW_HEIGHT; - val = config.Get(FlightSqlConnection::TRUSTED_CERTS).c_str(); + val = config.Get(FlightSqlConnection::TRUSTED_CERTS); + std::wstring wVal = arrow::util::UTF8ToWideString(val).ValueOr(L""); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, - "Certificate:", ChildId::CERTIFICATE_LABEL)); + L"Certificate:", ChildId::CERTIFICATE_LABEL)); certificateEdit = CreateEdit(editPosX, rowPos, editSizeX - MARGIN - BUTTON_WIDTH, - ROW_HEIGHT, val, ChildId::CERTIFICATE_EDIT); + ROW_HEIGHT, wVal.c_str(), ChildId::CERTIFICATE_EDIT); certificateBrowseButton = CreateButton(editPosX + editSizeX - BUTTON_WIDTH, rowPos - 2, BUTTON_WIDTH, - BUTTON_HEIGHT, "Browse", ChildId::CERTIFICATE_BROWSE_BUTTON); + BUTTON_HEIGHT, L"Browse", ChildId::CERTIFICATE_BROWSE_BUTTON); rowPos += INTERVAL + ROW_HEIGHT; val = config.Get(FlightSqlConnection::USE_SYSTEM_TRUST_STORE).c_str(); const bool useSystemCertStore = driver::odbcabstraction::AsBool(val).value_or(true); - labels.push_back( - CreateLabel(labelPosX, rowPos, LABEL_WIDTH, 2 * ROW_HEIGHT, - "Use System Certificate Store:", ChildId::USE_SYSTEM_CERT_STORE_LABEL)); + labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, 2 * ROW_HEIGHT, + L"Use System Certificate Store:", + ChildId::USE_SYSTEM_CERT_STORE_LABEL)); useSystemCertStoreCheckBox = - CreateCheckBox(editPosX, rowPos - 2, 20, 2 * ROW_HEIGHT, "", + CreateCheckBox(editPosX, rowPos - 2, 20, 2 * ROW_HEIGHT, L"", ChildId::USE_SYSTEM_CERT_STORE_CHECKBOX, useSystemCertStore); val = config.Get(FlightSqlConnection::DISABLE_CERTIFICATE_VERIFICATION).c_str(); @@ -273,17 +284,17 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in const int rightCheckPosX = rightPosX + (editPosX - labelPosX); const bool disableCertVerification = driver::odbcabstraction::AsBool(val).value_or(false); - labels.push_back(CreateLabel( - rightPosX, rowPos, LABEL_WIDTH, 2 * ROW_HEIGHT, - "Disable Certificate Verification:", ChildId::DISABLE_CERT_VERIFICATION_LABEL)); + labels.push_back(CreateLabel(rightPosX, rowPos, LABEL_WIDTH, 2 * ROW_HEIGHT, + L"Disable Certificate Verification:", + ChildId::DISABLE_CERT_VERIFICATION_LABEL)); disableCertVerificationCheckBox = CreateCheckBox( - rightCheckPosX, rowPos - 2, 20, 2 * ROW_HEIGHT, "", + rightCheckPosX, rowPos - 2, 20, 2 * ROW_HEIGHT, L"", ChildId::DISABLE_CERT_VERIFICATION_CHECKBOX, disableCertVerification); rowPos += INTERVAL + static_cast(1.5 * ROW_HEIGHT); encryptionSettingsGroupBox = - CreateGroupBox(posX, posY, sizeX, rowPos - posY, "Encryption settings", + CreateGroupBox(posX, posY, sizeX, rowPos - posY, L"Encryption settings", ChildId::AUTH_SETTINGS_GROUP_BOX); return rowPos - posY; @@ -301,12 +312,15 @@ int DsnConfigurationWindow::CreatePropertiesGroup(int posX, int posY, int sizeX) propertyList = CreateList(labelPosX, rowPos, listSize, listHeight, ChildId::PROPERTY_LIST); - propertyList->ListAddColumn("Key", 0, columnSize); - propertyList->ListAddColumn("Value", 1, columnSize); + propertyList->ListAddColumn(L"Key", 0, columnSize); + propertyList->ListAddColumn(L"Value", 1, columnSize); const auto keys = config.GetCustomKeys(); for (const auto& key : keys) { - propertyList->ListAddItem({key, config.Get(key)}); + std::wstring wKey = arrow::util::UTF8ToWideString(key).ValueOr(L""); + std::wstring wVal = arrow::util::UTF8ToWideString(config.Get(key)).ValueOr(L""); + + propertyList->ListAddItem({wKey, wVal}); } SendMessage(propertyList->GetHandle(), LVM_SETEXTENDEDLISTVIEWSTYLE, @@ -316,15 +330,15 @@ int DsnConfigurationWindow::CreatePropertiesGroup(int posX, int posY, int sizeX) int deletePosX = width - INTERVAL - MARGIN - BUTTON_WIDTH; int addPosX = deletePosX - INTERVAL - BUTTON_WIDTH; - addButton = CreateButton(addPosX, rowPos, BUTTON_WIDTH, BUTTON_HEIGHT, "Add", + addButton = CreateButton(addPosX, rowPos, BUTTON_WIDTH, BUTTON_HEIGHT, L"Add", ChildId::ADD_BUTTON); - deleteButton = CreateButton(deletePosX, rowPos, BUTTON_WIDTH, BUTTON_HEIGHT, "Delete", + deleteButton = CreateButton(deletePosX, rowPos, BUTTON_WIDTH, BUTTON_HEIGHT, L"Delete", ChildId::DELETE_BUTTON); rowPos += INTERVAL + BUTTON_HEIGHT; propertyGroupBox = CreateGroupBox(posX, posY, sizeX, rowPos - posY, - "Advanced properties", ChildId::PROPERTY_GROUP_BOX); + L"Advanced properties", ChildId::PROPERTY_GROUP_BOX); return rowPos - posY; } @@ -384,7 +398,7 @@ void DsnConfigurationWindow::CheckEnableOk() { void DsnConfigurationWindow::SaveParameters(Configuration& targetConfig) { targetConfig.Clear(); - std::string text; + std::wstring text; nameEdit->GetText(text); targetConfig.Set(FlightSqlConnection::DSN, text); serverEdit->GetText(text); @@ -427,7 +441,9 @@ void DsnConfigurationWindow::SaveParameters(Configuration& targetConfig) { // Get all the list properties. const auto properties = propertyList->ListGetAll(); for (const auto& property : properties) { - targetConfig.Set(property[0], property[1]); + std::string propertyKey = arrow::util::WideStringToUTF8(property[0]).ValueOr(""); + std::string propertyValue = arrow::util::WideStringToUTF8(property[1]).ValueOr(""); + targetConfig.Set(propertyKey, propertyValue); } } @@ -463,10 +479,13 @@ bool DsnConfigurationWindow::OnMessage(UINT msg, WPARAM wParam, LPARAM lParam) { SaveParameters(testConfig); std::string testMessage = TestConnection(testConfig); - MessageBox(NULL, testMessage.c_str(), "Test Connection Success", MB_OK); + std::wstring wTestMessage = + arrow::util::UTF8ToWideString(testMessage).ValueOr(L""); + MessageBox(NULL, wTestMessage.c_str(), L"Test Connection Success", MB_OK); } catch (odbcabstraction::DriverException& err) { - MessageBox(NULL, err.GetMessageText().c_str(), "Error!", - MB_ICONEXCLAMATION | MB_OK); + std::wstring wMessageText = + arrow::util::UTF8ToWideString(err.GetMessageText()).ValueOr(L""); + MessageBox(NULL, wMessageText.c_str(), L"Error!", MB_ICONEXCLAMATION | MB_OK); } break; @@ -477,8 +496,9 @@ bool DsnConfigurationWindow::OnMessage(UINT msg, WPARAM wParam, LPARAM lParam) { accepted = true; PostMessage(GetHandle(), WM_CLOSE, 0, 0); } catch (odbcabstraction::DriverException& err) { - MessageBox(NULL, err.GetMessageText().c_str(), "Error!", - MB_ICONEXCLAMATION | MB_OK); + std::wstring wMessageText = + arrow::util::UTF8ToWideString(err.GetMessageText()).ValueOr(L""); + MessageBox(NULL, wMessageText.c_str(), L"Error!", MB_ICONEXCLAMATION | MB_OK); } break; @@ -520,7 +540,7 @@ bool DsnConfigurationWindow::OnMessage(UINT msg, WPARAM wParam, LPARAM lParam) { case ChildId::CERTIFICATE_BROWSE_BUTTON: { OPENFILENAME openFileName; - char fileName[FILENAME_MAX]; + wchar_t fileName[FILENAME_MAX]; ZeroMemory(&openFileName, sizeof(openFileName)); openFileName.lStructSize = sizeof(openFileName); @@ -529,7 +549,7 @@ bool DsnConfigurationWindow::OnMessage(UINT msg, WPARAM wParam, LPARAM lParam) { openFileName.lpstrFile[0] = '\0'; openFileName.nMaxFile = FILENAME_MAX; // TODO: What type should this be? - openFileName.lpstrFilter = "All\0*.*"; + openFileName.lpstrFilter = L"All\0*.*"; openFileName.nFilterIndex = 1; openFileName.lpstrFileTitle = NULL; openFileName.nMaxFileTitle = 0; @@ -566,8 +586,8 @@ bool DsnConfigurationWindow::OnMessage(UINT msg, WPARAM wParam, LPARAM lParam) { addWindow.Update(); if (ProcessMessages(addWindow) == Result::OK) { - std::string key; - std::string value; + std::wstring key; + std::wstring value; addWindow.GetProperty(key, value); propertyList->ListAddItem({key, value}); } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/window.cc index f88cd8a3f88..2940c95578a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/window.cc @@ -49,7 +49,7 @@ HINSTANCE GetHInstance() { return hInstance; } -Window::Window(Window* parent, const char* className, const char* title) +Window::Window(Window* parent, const wchar_t* className, const wchar_t* title) : className(className), title(title), handle(NULL), parent(parent), created(false) { // No-op. } @@ -88,7 +88,7 @@ void Window::Create(DWORD style, int posX, int posY, int width, int height, int } std::unique_ptr Window::CreateTabControl(int id) { - std::unique_ptr child(new Window(this, WC_TABCONTROL, "")); + std::unique_ptr child(new Window(this, WC_TABCONTROL, L"")); // Get the dimensions of the parent window's client area, and // create a tab control child window of that size. @@ -103,7 +103,7 @@ std::unique_ptr Window::CreateTabControl(int id) { std::unique_ptr Window::CreateList(int posX, int posY, int sizeX, int sizeY, int id) { - std::unique_ptr child(new Window(this, WC_LISTVIEW, "")); + std::unique_ptr child(new Window(this, WC_LISTVIEW, L"")); child->Create( WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT | LVS_EDITLABELS | WS_TABSTOP, posX, @@ -113,8 +113,8 @@ std::unique_ptr Window::CreateList(int posX, int posY, int sizeX, int si } std::unique_ptr Window::CreateGroupBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id) { - std::unique_ptr child(new Window(this, "Button", title)); + const wchar_t* title, int id) { + std::unique_ptr child(new Window(this, L"Button", title)); child->Create(WS_CHILD | WS_VISIBLE | BS_GROUPBOX, posX, posY, sizeX, sizeY, id); @@ -122,8 +122,8 @@ std::unique_ptr Window::CreateGroupBox(int posX, int posY, int sizeX, in } std::unique_ptr Window::CreateLabel(int posX, int posY, int sizeX, int sizeY, - const char* title, int id) { - std::unique_ptr child(new Window(this, "Static", title)); + const wchar_t* title, int id) { + std::unique_ptr child(new Window(this, L"Static", title)); child->Create(WS_CHILD | WS_VISIBLE, posX, posY, sizeX, sizeY, id); @@ -131,8 +131,8 @@ std::unique_ptr Window::CreateLabel(int posX, int posY, int sizeX, int s } std::unique_ptr Window::CreateEdit(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, int style) { - std::unique_ptr child(new Window(this, "Edit", title)); + const wchar_t* title, int id, int style) { + std::unique_ptr child(new Window(this, L"Edit", title)); child->Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL | WS_TABSTOP | style, posX, posY, sizeX, sizeY, id); @@ -141,8 +141,8 @@ std::unique_ptr Window::CreateEdit(int posX, int posY, int sizeX, int si } std::unique_ptr Window::CreateButton(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, int style) { - std::unique_ptr child(new Window(this, "Button", title)); + const wchar_t* title, int id, int style) { + std::unique_ptr child(new Window(this, L"Button", title)); child->Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | style, posX, posY, sizeX, sizeY, id); @@ -150,8 +150,8 @@ std::unique_ptr Window::CreateButton(int posX, int posY, int sizeX, int } std::unique_ptr Window::CreateCheckBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id, bool state) { - std::unique_ptr child(new Window(this, "Button", title)); + const wchar_t* title, int id, bool state) { + std::unique_ptr child(new Window(this, L"Button", title)); child->Create(WS_CHILD | WS_VISIBLE | BS_CHECKBOX | WS_TABSTOP, posX, posY, sizeX, sizeY, id); @@ -162,8 +162,8 @@ std::unique_ptr Window::CreateCheckBox(int posX, int posY, int sizeX, in } std::unique_ptr Window::CreateComboBox(int posX, int posY, int sizeX, int sizeY, - const char* title, int id) { - std::unique_ptr child(new Window(this, "Combobox", title)); + const wchar_t* title, int id) { + std::unique_ptr child(new Window(this, L"Combobox", title)); child->Create(WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP, posX, posY, sizeX, sizeY, id); @@ -194,12 +194,12 @@ bool Window::IsTextEmpty() const { return (len <= 0); } -void Window::ListAddColumn(const std::string& name, int index, int width) { +void Window::ListAddColumn(const std::wstring& name, int index, int width) { LVCOLUMN lvc; lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; lvc.fmt = LVCFMT_LEFT; lvc.cx = width; - lvc.pszText = const_cast(name.c_str()); + lvc.pszText = const_cast(name.c_str()); lvc.iSubItem = index; if (ListView_InsertColumn(handle, index, &lvc) == -1) { @@ -209,10 +209,10 @@ void Window::ListAddColumn(const std::string& name, int index, int width) { } } -void Window::ListAddItem(const std::vector& items) { +void Window::ListAddItem(const std::vector& items) { LVITEM lvi = {0}; lvi.mask = LVIF_TEXT; - lvi.pszText = const_cast(items[0].c_str()); + lvi.pszText = const_cast(items[0].c_str()); int ret = ListView_InsertItem(handle, &lvi); if (ret < 0) { @@ -223,7 +223,7 @@ void Window::ListAddItem(const std::vector& items) { for (size_t i = 1; i < items.size(); ++i) { ListView_SetItemText(handle, ret, static_cast(i), - const_cast(items[i].c_str())); + const_cast(items[i].c_str())); } } @@ -238,15 +238,15 @@ void Window::ListDeleteSelectedItem() { } } -std::vector > Window::ListGetAll() { +std::vector > Window::ListGetAll() { #define BUF_LEN 1024 - char buf[BUF_LEN]; + wchar_t buf[BUF_LEN]; - std::vector > values; + std::vector > values; const int numColumns = Header_GetItemCount(ListView_GetHeader(handle)); const int numItems = ListView_GetItemCount(handle); for (int i = 0; i < numItems; ++i) { - std::vector row; + std::vector row; for (int j = 0; j < numColumns; ++j) { ListView_GetItemText(handle, i, j, buf, BUF_LEN); row.emplace_back(buf); @@ -257,11 +257,11 @@ std::vector > Window::ListGetAll() { return values; } -void Window::AddTab(const std::string& name, int index) { +void Window::AddTab(const std::wstring& name, int index) { TCITEM tabControlItem; tabControlItem.mask = TCIF_TEXT | TCIF_IMAGE; tabControlItem.iImage = -1; - tabControlItem.pszText = const_cast(name.c_str()); + tabControlItem.pszText = const_cast(name.c_str()); if (TabCtrl_InsertItem(handle, index, &tabControlItem) == -1) { std::stringstream buf; buf << "Can not add tab, error code: " << GetLastError(); @@ -269,7 +269,7 @@ void Window::AddTab(const std::string& name, int index) { } } -void Window::GetText(std::string& text) const { +void Window::GetText(std::wstring& text) const { if (!IsEnabled()) { text.clear(); @@ -292,7 +292,7 @@ void Window::GetText(std::string& text) const { boost::algorithm::trim(text); } -void Window::SetText(const std::string& text) const { +void Window::SetText(const std::wstring& text) const { SNDMSG(handle, WM_SETTEXT, 0, reinterpret_cast(text.c_str())); } @@ -304,7 +304,7 @@ void Window::SetChecked(bool state) { Button_SetCheck(handle, state ? BST_CHECKED : BST_UNCHECKED); } -void Window::AddString(const std::string& str) { +void Window::AddString(const std::wstring& str) { SNDMSG(handle, CB_ADDSTRING, 0, reinterpret_cast(str.c_str())); } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc index 18b5c399c2c..2017936dd90 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/win_system_dsn.cc @@ -22,6 +22,9 @@ #include #include +#include "arrow/result.h" +#include "arrow/util/utf8.h" + #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/connection_string_parser.h" @@ -44,12 +47,6 @@ using driver::flight_sql::config::Result; using driver::flight_sql::config::Window; using driver::odbcabstraction::DriverException; -BOOL CALLBACK ConfigDriver(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, - LPCSTR lpszArgs, LPSTR lpszMsg, WORD cbMsgMax, - WORD* pcbMsgOut) { - return false; -} - bool DisplayConnectionWindow(void* windowParent, Configuration& config) { HWND hwndParent = (HWND)windowParent; @@ -69,10 +66,12 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config) { std::stringstream buf; buf << "SQL State: " << err.GetSqlState() << ", Message: " << err.GetMessageText() << ", Code: " << err.GetNativeError(); - std::string message = buf.str(); - MessageBox(NULL, message.c_str(), "Error!", MB_ICONEXCLAMATION | MB_OK); + std::wstring wMessage = arrow::util::UTF8ToWideString(buf.str()).ValueOr(L""); + MessageBox(NULL, wMessage.c_str(), L"Error!", MB_ICONEXCLAMATION | MB_OK); - SQLPostInstallerError(err.GetNativeError(), err.GetMessageText().c_str()); + std::wstring wMessageText = + arrow::util::UTF8ToWideString(err.GetMessageText()).ValueOr(L""); + SQLPostInstallerError(err.GetNativeError(), wMessageText.c_str()); } return false; @@ -93,15 +92,17 @@ bool DisplayConnectionWindow(void* windowParent, Configuration& config, } } -BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attributes) { +BOOL INSTAPI ConfigDSNW(HWND hwndParent, WORD req, LPCWSTR wDriver, LPCWSTR wAttributes) { Configuration config; ConnectionStringParser parser(config); - parser.ParseConfigAttributes(attributes); + std::string attributes = + arrow::util::WideStringToUTF8(std::wstring(wAttributes)).ValueOr(""); + parser.ParseConfigAttributes(attributes.c_str()); switch (req) { case ODBC_ADD_DSN: { config.LoadDefaults(); - if (!DisplayConnectionWindow(hwndParent, config) || !RegisterDsn(config, driver)) + if (!DisplayConnectionWindow(hwndParent, config) || !RegisterDsn(config, wDriver)) return FALSE; break; @@ -109,13 +110,14 @@ BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attribut case ODBC_CONFIG_DSN: { const std::string& dsn = config.Get(FlightSqlConnection::DSN); - if (!SQLValidDSN(dsn.c_str())) return FALSE; + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); + if (!SQLValidDSN(wDsn.c_str())) return FALSE; Configuration loaded(config); loaded.LoadDsn(dsn); - if (!DisplayConnectionWindow(hwndParent, loaded) || !UnregisterDsn(dsn.c_str()) || - !RegisterDsn(loaded, driver)) + if (!DisplayConnectionWindow(hwndParent, loaded) || !UnregisterDsn(wDsn.c_str()) || + !RegisterDsn(loaded, wDriver)) return FALSE; break; @@ -123,7 +125,8 @@ BOOL INSTAPI ConfigDSN(HWND hwndParent, WORD req, LPCSTR driver, LPCSTR attribut case ODBC_REMOVE_DSN: { const std::string& dsn = config.Get(FlightSqlConnection::DSN); - if (!SQLValidDSN(dsn.c_str()) || !UnregisterDsn(dsn)) return FALSE; + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); + if (!SQLValidDSN(wDsn.c_str()) || !UnregisterDsn(wDsn)) return FALSE; break; } diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index c90c181d7c1..ed3203afbb4 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -17,7 +17,7 @@ LIBRARY arrow_flight_sql_odbc EXPORTS - ConfigDSN + ConfigDSNW SQLAllocConnect SQLAllocEnv SQLAllocHandle diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 21d060ad70f..4e732f16f56 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -224,10 +224,9 @@ inline bool IsValidStringFieldArgs(SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLen return hasValidBuffer || stringLengthPtr; } -SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, - SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, - SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr) { +SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { // TODO: Implement additional fields types // https://github.com/apache/arrow/issues/46573 using driver::odbcabstraction::Diagnostics; @@ -478,10 +477,10 @@ SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, return SQL_ERROR; } -SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, - SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, - SQLWCHAR* messageText, SQLSMALLINT bufferLength, - SQLSMALLINT* textLengthPtr) { +SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, + SQLWCHAR* messageText, SQLSMALLINT bufferLength, + SQLSMALLINT* textLengthPtr) { using driver::odbcabstraction::Diagnostics; using ODBC::GetStringAttribute; using ODBC::ODBCConnection; @@ -677,17 +676,17 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, }); } -SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, - SQLWCHAR* inConnectionString, - SQLSMALLINT inConnectionStringLen, - SQLWCHAR* outConnectionString, - SQLSMALLINT outConnectionStringBufferLen, - SQLSMALLINT* outConnectionStringLen, - SQLUSMALLINT driverCompletion) { +SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion) { // TODO: Implement FILEDSN and SAVEFILE keywords according to the spec // https://github.com/apache/arrow/issues/46449 - // TODO: Copy connection string properly in SQLDriverConnectW according to the + // TODO: Copy connection string properly in SQLDriverConnect according to the // spec https://github.com/apache/arrow/issues/46560 using driver::odbcabstraction::Connection; @@ -712,7 +711,7 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, std::vector missing_properties; - // TODO: Implement SQL_DRIVER_COMPLETE_REQUIRED in SQLDriverConnectW according to the + // TODO: Implement SQL_DRIVER_COMPLETE_REQUIRED in SQLDriverConnect according to the // spec https://github.com/apache/arrow/issues/46448 #if defined _WIN32 || defined _WIN64 // Load the DSN window according to driverCompletion @@ -757,9 +756,9 @@ SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, }); } -SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, - SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, - SQLSMALLINT passwordLen) { +SQLRETURN SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, + SQLSMALLINT passwordLen) { using driver::flight_sql::FlightSqlConnection; using driver::flight_sql::config::Configuration; using ODBC::ODBCConnection; @@ -811,8 +810,8 @@ SQLRETURN SQLDisconnect(SQLHDBC conn) { }); } -SQLRETURN SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, - SQLSMALLINT bufLen, SQLSMALLINT* length) { +SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, + SQLSMALLINT bufLen, SQLSMALLINT* length) { // TODO: complete implementation of SQLGetInfoW and write tests using ODBC::ODBCConnection; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 9350cead384..eb2f677a385 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -32,29 +32,28 @@ namespace arrow { SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result); SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle); SQLRETURN SQLFreeStmt(SQLHSTMT stmt, SQLUSMALLINT option); -SQLRETURN SQLGetDiagFieldW(SQLSMALLINT handleType, SQLHANDLE handle, - SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, - SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr); -SQLRETURN SQLGetDiagRecW(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, - SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, - SQLWCHAR* messageText, SQLSMALLINT bufferLength, - SQLSMALLINT* textLengthPtr); +SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr); +SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, + SQLWCHAR* messageText, SQLSMALLINT bufferLength, + SQLSMALLINT* textLengthPtr); SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr); SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER strLen); -SQLRETURN SQLDriverConnectW(SQLHDBC conn, SQLHWND windowHandle, - SQLWCHAR* inConnectionString, - SQLSMALLINT inConnectionStringLen, - SQLWCHAR* outConnectionString, - SQLSMALLINT outConnectionStringBufferLen, - SQLSMALLINT* outConnectionStringLen, - SQLUSMALLINT driverCompletion); -SQLRETURN SQLConnectW(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, - SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, - SQLSMALLINT passwordLen); +SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, + SQLWCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLen, + SQLWCHAR* outConnectionString, + SQLSMALLINT outConnectionStringBufferLen, + SQLSMALLINT* outConnectionStringLen, + SQLUSMALLINT driverCompletion); +SQLRETURN SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, + SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, + SQLSMALLINT passwordLen); SQLRETURN SQLDisconnect(SQLHDBC conn); -SQLRETURN SQLGetInfoW(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, - SQLSMALLINT bufLen, SQLSMALLINT* length); +SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, + SQLSMALLINT bufLen, SQLSMALLINT* length); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index f28ee1789b0..3da4a63c5f5 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -17,6 +17,9 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/result.h" +#include "arrow/util/utf8.h" + #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h" @@ -56,40 +59,44 @@ const boost::xpressive::sregex CONNECTION_STR_REGEX( void loadPropertiesFromDSN(const std::string& dsn, Connection::ConnPropertyMap& properties) { const size_t BUFFER_SIZE = 1024 * 10; - std::vector outputBuffer; + std::vector outputBuffer; outputBuffer.resize(BUFFER_SIZE, '\0'); SQLSetConfigMode(ODBC_BOTH_DSN); - SQLGetPrivateProfileString(dsn.c_str(), NULL, "", &outputBuffer[0], BUFFER_SIZE, - "odbc.ini"); + std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); + + SQLGetPrivateProfileString(wDsn.c_str(), NULL, L"", &outputBuffer[0], BUFFER_SIZE, + L"odbc.ini"); // The output buffer holds the list of keys in a series of NUL-terminated strings. // The series is terminated with an empty string (eg a NUL-terminator terminating the // last key followed by a NUL terminator after). - std::vector keys; + std::vector keys; size_t pos = 0; while (pos < BUFFER_SIZE) { - std::string key(&outputBuffer[pos]); - if (key.empty()) { + std::wstring wKey(&outputBuffer[pos]); + if (wKey.empty()) { break; } - size_t len = key.size(); + size_t len = wKey.size(); // Skip over Driver or DSN keys. - if (!boost::iequals(key, "DSN") && !boost::iequals(key, "Driver")) { - keys.emplace_back(std::move(key)); + if (!boost::iequals(wKey, L"DSN") && !boost::iequals(wKey, L"Driver")) { + keys.emplace_back(std::move(wKey)); } pos += len + 1; } - for (auto& key : keys) { + for (auto& wKey : keys) { outputBuffer.clear(); outputBuffer.resize(BUFFER_SIZE, '\0'); - SQLGetPrivateProfileString(dsn.c_str(), key.data(), "", &outputBuffer[0], BUFFER_SIZE, - "odbc.ini"); + SQLGetPrivateProfileString(wDsn.c_str(), wKey.data(), L"", &outputBuffer[0], + BUFFER_SIZE, L"odbc.ini"); - std::string value = std::string(&outputBuffer[0]); - auto propIter = properties.find(std::string(key)); + std::wstring wValue = std::wstring(&outputBuffer[0]); + std::string value = arrow::util::WideStringToUTF8(wValue).ValueOr(""); + std::string key = arrow::util::WideStringToUTF8(std::wstring(wKey)).ValueOr(""); + auto propIter = properties.find(key); if (propIter == properties.end()) { properties.emplace(std::make_pair(std::move(key), std::move(value))); } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 41e51182275..5f055682dfb 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -19,8 +19,6 @@ add_custom_target(tests) include_directories(${ODBC_INCLUDE_DIRS}) -add_definitions(-DUNICODE=1) - find_package(SQLite3Alt REQUIRED) set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 176129ba627..99fce0684c7 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -400,10 +400,8 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); - // TODO: Check that outstr remains empty after SqlWcharToString - // is fixed to handle empty `outstr` - // std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); - // EXPECT_TRUE(out_connection_string.empty()); + std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); + EXPECT_TRUE(out_connection_string.empty()); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); @@ -463,7 +461,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { EXPECT_TRUE(ret == SQL_SUCCESS); // Remove DSN - EXPECT_TRUE(UnregisterDsn(dsn)); + EXPECT_TRUE(UnregisterDsn(wdsn)); // Disconnect from ODBC ret = SQLDisconnect(conn); @@ -541,7 +539,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { EXPECT_TRUE(ret == SQL_SUCCESS); // Remove DSN - EXPECT_TRUE(UnregisterDsn(dsn)); + EXPECT_TRUE(UnregisterDsn(wdsn)); // Disconnect from ODBC ret = SQLDisconnect(conn); @@ -618,7 +616,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); // Remove DSN - EXPECT_TRUE(UnregisterDsn(dsn)); + EXPECT_TRUE(UnregisterDsn(wdsn)); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); @@ -681,7 +679,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { EXPECT_TRUE(ret == SQL_SUCCESS); // Remove DSN - EXPECT_TRUE(UnregisterDsn(dsn)); + EXPECT_TRUE(UnregisterDsn(wdsn)); // Disconnect from ODBC ret = SQLDisconnect(conn); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index c079c3c175c..2d2a8f5d305 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -262,8 +262,8 @@ bool writeDSN(Connection::ConnPropertyMap properties) { } std::string driver = config.Get(FlightSqlConnection::DRIVER); - - return RegisterDsn(config, driver.c_str()); + std::wstring wDriver = arrow::util::UTF8ToWideString(driver).ValueOr(L""); + return RegisterDsn(config, wDriver.c_str()); } } // namespace integration_tests } // namespace odbc From 3bbbaa986e329b3f4a91dde9b87e7eab6bb65fe2 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 16 Jun 2025 17:38:43 -0700 Subject: [PATCH 19/74] Initialize Kernel functions As per changes from #46261, we need to initialize Kernel library explicitly to get the functions registered --- .../flight/sql/odbc/flight_sql/CMakeLists.txt | 2 +- .../sql/odbc/flight_sql/flight_sql_driver.cc | 4 ++++ .../flight/sql/odbc/flight_sql/utils_test.cc | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt index e9f282e91f9..895eff8ed9a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt @@ -102,7 +102,7 @@ if(WIN32) endif() target_link_libraries(arrow_odbc_spi_impl PUBLIC odbcabstraction arrow_flight_sql_shared - Boost::locale) + arrow_compute_shared Boost::locale) # Link libraries on MINGW64 only if(MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc index cb0e5c5ae5c..0736dac8486 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_driver.cc @@ -16,7 +16,9 @@ // under the License. #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/flight_sql_driver.h" +#include "arrow/compute/api.h" #include "arrow/flight/sql/odbc/flight_sql/flight_sql_connection.h" +#include "arrow/flight/sql/odbc/flight_sql/utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spd_logger.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/utils.h" @@ -56,6 +58,8 @@ LogLevel ToLogLevel(int64_t level) { FlightSqlDriver::FlightSqlDriver() : diagnostics_("Apache Arrow", "Flight SQL", OdbcVersion::V_3), version_("0.9.0.0") { RegisterLog(); + // Register Kernel functions to library + ThrowIfNotOK(arrow::compute::Initialize()); } std::shared_ptr FlightSqlDriver::CreateConnection(OdbcVersion odbc_version) { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils_test.cc index 1575bf09fab..f5d61da50bf 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils_test.cc @@ -19,6 +19,7 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/calendar_utils.h" +#include "arrow/compute/initialize.h" #include "arrow/testing/builder.h" #include "arrow/testing/gtest_util.h" #include "arrow/testing/util.h" @@ -27,6 +28,13 @@ namespace driver { namespace flight_sql { +class UtilTestsWithCompute : public ::testing::Test { + public: + // This must be done before using the compute kernels in order to + // register them to the FunctionRegistry. + void SetUp() override { ASSERT_OK(arrow::compute::Initialize()); } +}; + void AssertConvertedArray(const std::shared_ptr& expected_array, const std::shared_ptr& converted_array, uint64_t size, arrow::Type::type arrow_type) { @@ -80,7 +88,7 @@ void TestTime64ArrayConversion(const std::vector& input, AssertConvertedArray(expected_array, converted_array, input.size(), arrow_type); } -TEST(Utils, Time32ToTimeStampArray) { +TEST_F(UtilTestsWithCompute, Time32ToTimeStampArray) { std::vector input_data = {14896, 17820}; const auto seconds_from_epoch = odbcabstraction::GetTodayTimeFromEpoch(); @@ -100,7 +108,7 @@ TEST(Utils, Time32ToTimeStampArray) { arrow::Type::TIMESTAMP); } -TEST(Utils, Time64ToTimeStampArray) { +TEST_F(UtilTestsWithCompute, Time64ToTimeStampArray) { std::vector input_data = {1579489200000, 1646881200000}; const auto seconds_from_epoch = odbcabstraction::GetTodayTimeFromEpoch(); @@ -120,7 +128,7 @@ TEST(Utils, Time64ToTimeStampArray) { arrow::Type::TIMESTAMP); } -TEST(Utils, StringToDateArray) { +TEST_F(UtilTestsWithCompute, StringToDateArray) { std::shared_ptr expected; arrow::ArrayFromVector({1579489200000, 1646881200000}, &expected); @@ -129,7 +137,7 @@ TEST(Utils, StringToDateArray) { odbcabstraction::CDataType_DATE, arrow::Type::DATE64); } -TEST(Utils, StringToTimeArray) { +TEST_F(UtilTestsWithCompute, StringToTimeArray) { std::shared_ptr expected; arrow::ArrayFromVector( time64(arrow::TimeUnit::MICRO), {36000000000, 43200000000}, &expected); From c13d7bb3c800f0713899f92cca94063bd725ab67 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:47:10 -0700 Subject: [PATCH 20/74] Implement SQLSetConnectAttr and SQLGetConnectAttr --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 21 +- cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 35 ++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 4 + .../odbc_impl/odbc_connection.h | 5 +- .../odbc_impl/odbc_connection.cc | 42 +- .../odbc_impl/odbc_statement.cc | 1 + .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../sql/odbc/tests/connection_attr_test.cc | 567 ++++++++++++++++++ .../flight/sql/odbc/tests/connection_test.cc | 160 ++--- .../flight/sql/odbc/tests/odbc_test_suite.cc | 51 +- .../flight/sql/odbc/tests/odbc_test_suite.h | 16 +- 12 files changed, 764 insertions(+), 140 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index b397631992f..82153008345 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -87,11 +87,16 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePt return arrow::SQLSetEnvAttr(env, attr, valuePtr, strLen); } +SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC conn, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + return arrow::SQLGetConnectAttr(conn, attribute, valuePtr, bufferLength, + stringLengthPtr); +} + SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, SQLINTEGER valueLen) { - LOG_DEBUG("SQLSetConnectAttrW called with conn: {}, attr: {}, value: {}, valueLen: {}", - conn, attr, value, valueLen); - return SQL_ERROR; + return arrow::SQLSetConnectAttr(conn, attr, value, valueLen); } SQLRETURN SQL_API SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, @@ -225,16 +230,6 @@ SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogNa return SQL_ERROR; } -SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER bufferLength, - SQLINTEGER* stringLengthPtr) { - LOG_DEBUG( - "SQLGetConnectAttrW called with connectionHandle: {}, attribute: {}, valuePtr: {}, " - "bufferLength: {}, stringLengthPtr: {}", - connectionHandle, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLUSMALLINT col_or_Param_Num, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index ed3203afbb4..925109a5a9f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -36,6 +36,7 @@ EXPORTS SQLFetch SQLForeignKeysW SQLFreeEnv + SQLFreeConnect SQLFreeHandle SQLFreeStmt SQLGetConnectAttrW diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 4e732f16f56..1e59791f2e2 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -676,6 +676,41 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, }); } +SQLRETURN SQLGetConnectAttr(SQLHDBC conn, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + + LOG_DEBUG( + "SQLGetConnectAttrW called with conn: {}, attribute: {}, valuePtr: {}, " + "bufferLength: {}, stringLengthPtr: {}", + conn, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + const bool isUnicode = true; + ODBCConnection* connection = reinterpret_cast(conn); + return connection->GetConnectAttr(attribute, valuePtr, bufferLength, stringLengthPtr, + isUnicode); + }); +} + +SQLRETURN SQLSetConnectAttr(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER valuePtr, + SQLINTEGER valueLen) { + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + + LOG_DEBUG( + "SQLSetConnectAttrW called with conn: {}, attr: {}, valuePtr: {}, valueLen: {}", + conn, attr, valuePtr, valueLen); + + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { + const bool isUnicode = true; + ODBCConnection* connection = reinterpret_cast(conn); + connection->SetConnectAttr(attr, valuePtr, valueLen, isUnicode); + return SQL_SUCCESS; + }); +} + SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, SQLWCHAR* inConnectionString, SQLSMALLINT inConnectionStringLen, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index eb2f677a385..0118ba77434 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -43,6 +43,10 @@ SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLen, SQLINTEGER* strLenPtr); SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER strLen); +SQLRETURN SQLGetConnectAttr(SQLHDBC conn, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); +SQLRETURN SQLSetConnectAttr(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER value, + SQLINTEGER valueLen); SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, SQLWCHAR* inConnectionString, SQLSMALLINT inConnectionStringLen, diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h index 966110502f8..92946e5a0a5 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h @@ -64,8 +64,9 @@ class ODBCConnection : public ODBCHandle { SQLSMALLINT* outputLength, bool isUnicode); void SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength, bool isUnicode); - void GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER bufferLength, - SQLINTEGER* outputLength, bool isUnicode); + SQLRETURN GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* outputLength, + bool isUnicode); ~ODBCConnection() = default; diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 3da4a63c5f5..c9867409407 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -468,7 +468,7 @@ void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, bool successfully_written = false; switch (attribute) { // Internal connection attributes -#ifdef SQL_ATR_ASYNC_DBC_EVENT +#ifdef SQL_ATTR_ASYNC_DBC_EVENT case SQL_ATTR_ASYNC_DBC_EVENT: throw DriverException("Optional feature not supported.", "HYC00"); #endif @@ -476,7 +476,7 @@ void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, case SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE: throw DriverException("Optional feature not supported.", "HYC00"); #endif -#ifdef SQL_ATTR_ASYNC_PCALLBACK +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK case SQL_ATTR_ASYNC_DBC_PCALLBACK: throw DriverException("Optional feature not supported.", "HYC00"); #endif @@ -504,7 +504,7 @@ void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, throw DriverException("Cannot set read-only attribute", "HY092"); case SQL_ATTR_TRACE: // DM-only throw DriverException("Cannot set read-only attribute", "HY092"); - case SQL_ATTR_TRACEFILE: + case SQL_ATTR_TRACEFILE: // DM-only throw DriverException("Optional feature not supported.", "HYC00"); case SQL_ATTR_TRANSLATE_LIB: throw DriverException("Optional feature not supported.", "HYC00"); @@ -578,59 +578,59 @@ void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, } } -void ODBCConnection::GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, - SQLINTEGER bufferLength, SQLINTEGER* outputLength, - bool isUnicode) { +SQLRETURN ODBCConnection::GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* outputLength, bool isUnicode) { using driver::odbcabstraction::Connection; boost::optional spiAttribute; switch (attribute) { // Internal connection attributes -#ifdef SQL_ATR_ASYNC_DBC_EVENT +#ifdef SQL_ATTR_ASYNC_DBC_EVENT case SQL_ATTR_ASYNC_DBC_EVENT: GetAttribute(static_cast(NULL), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; #endif #ifdef SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE case SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE: GetAttribute(static_cast(SQL_ASYNC_DBC_ENABLE_OFF), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; #endif -#ifdef SQL_ATTR_ASYNC_PCALLBACK +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK case SQL_ATTR_ASYNC_DBC_PCALLBACK: GetAttribute(static_cast(NULL), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; #endif #ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT case SQL_ATTR_ASYNC_DBC_PCONTEXT: GetAttribute(static_cast(NULL), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; #endif case SQL_ATTR_ASYNC_ENABLE: GetAttribute(static_cast(SQL_ASYNC_ENABLE_OFF), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; case SQL_ATTR_AUTO_IPD: GetAttribute(static_cast(SQL_FALSE), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; case SQL_ATTR_AUTOCOMMIT: GetAttribute(static_cast(SQL_AUTOCOMMIT_ON), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; #ifdef SQL_ATTR_DBC_INFO_TOKEN case SQL_ATTR_DBC_INFO_TOKEN: throw DriverException("Cannot read set-only attribute", "HY092"); #endif case SQL_ATTR_ENLIST_IN_DTC: GetAttribute(static_cast(NULL), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; case SQL_ATTR_ODBC_CURSORS: // DM-only. throw DriverException("Invalid attribute", "HY092"); case SQL_ATTR_QUIET_MODE: GetAttribute(static_cast(NULL), value, bufferLength, outputLength); - return; + return SQL_SUCCESS; case SQL_ATTR_TRACE: // DM-only throw DriverException("Invalid attribute", "HY092"); case SQL_ATTR_TRACEFILE: @@ -640,7 +640,7 @@ void ODBCConnection::GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, case SQL_ATTR_TRANSLATE_OPTION: throw DriverException("Optional feature not supported.", "HYC00"); case SQL_ATTR_TXN_ISOLATION: - throw DriverException("Optional feature not supported.", "HCY00"); + throw DriverException("Optional feature not supported.", "HYC00"); // ODBCAbstraction-level connection attributes. case SQL_ATTR_CURRENT_CATALOG: { @@ -649,9 +649,8 @@ void ODBCConnection::GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, throw DriverException("Optional feature not supported.", "HYC00"); } const std::string& infoValue = boost::get(*catalog); - GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, - GetDiagnostics()); - return; + return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, + outputLength, GetDiagnostics()); } // These all are uint32_t attributes. @@ -680,6 +679,7 @@ void ODBCConnection::GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, GetAttribute(static_cast(boost::get(*spiAttribute)), value, bufferLength, outputLength); + return SQL_SUCCESS; } void ODBCConnection::disconnect() { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index f6c06060d67..e5ac4f41408 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -581,6 +581,7 @@ void ODBCStatement::SetStmtAttr(SQLINTEGER statementAttribute, SQLPOINTER value, return; case SQL_ATTR_ASYNC_ENABLE: + throw DriverException("Unsupported attribute", "HYC00"); #ifdef SQL_ATTR_ASYNC_STMT_EVENT case SQL_ATTR_ASYNC_STMT_EVENT: throw DriverException("Unsupported attribute", "HYC00"); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 5f055682dfb..9f30502ac36 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -32,6 +32,7 @@ set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS add_arrow_test(connection_test SOURCES connection_test.cc + connection_attr_test.cc odbc_test_suite.cc odbc_test_suite.h ${ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS} diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc new file mode 100644 index 00000000000..d9d39319b27 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc @@ -0,0 +1,567 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow { +namespace flight { +namespace odbc { +namespace integration_tests { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAsyncDbcEventUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_EVENT + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_EVENT, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + // Driver Manager on Windows returns error code HY118 + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY118); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncEnableUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_ENABLE + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_ENABLE, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncDbcPcCallbackUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCALLBACK, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncDbcPcContextUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCONTEXT, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAutoIpdReadOnly) { + this->connect(); + + // Verify read-only attribute cannot be set + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_AUTO_IPD, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY092); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrConnectionDeadReadOnly) { + this->connect(); + + // Verify read-only attribute cannot be set + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_CONNECTION_DEAD, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY092); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrDbcInfoTokenUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_DBC_INFO_TOKEN + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_DBC_INFO_TOKEN, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrEnlistInDtcUnsupported) { + this->connect(); + + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ENLIST_IN_DTC, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrOdbcCursorsDMOnly) { + this->allocEnvConnHandles(); + + // Verify DM-only attribute is settable via Driver Manager + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ODBC_CURSORS, + reinterpret_cast(SQL_CUR_USE_DRIVER), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + std::string connect_str = this->getConnectionString(); + this->connectWithString(connect_str); + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrQuietModeReadOnly) { + this->connect(); + + // Verify read-only attribute cannot be set + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_QUIET_MODE, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY092); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTraceDMOnly) { + this->connect(); + + // Verify DM-only attribute is settable via Driver Manager + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_TRACE, + reinterpret_cast(SQL_OPT_TRACE_OFF), 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTracefileDMOnly) { + this->connect(); + + // Verify DM-only attribute is handled by Driver Manager + + // Use placeholder value as we want the call to fail, or else + // the driver manager will produce a trace file. + std::wstring trace_file = L"invalid/file/path"; + std::vector trace_file0(trace_file.begin(), trace_file.end()); + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_TRACEFILE, &trace_file0[0], + static_cast(trace_file0.size())); + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY000); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTranslateLabDMOnly) { + this->connect(); + + // Verify DM-only attribute is handled by Driver Manager + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_TRANSLATE_LIB, 0, 0); + EXPECT_EQ(ret, SQL_ERROR); + // Checks for invalid argument return error + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY024); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTranslateOptionUnsupported) { + this->connect(); + + SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_TRANSLATE_OPTION, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTxnIsolationUnsupported) { + this->connect(); + + SQLRETURN ret = + SQLSetConnectAttr(this->conn, SQL_ATTR_TXN_ISOLATION, + reinterpret_cast(SQL_TXN_READ_UNCOMMITTED), 0); + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrDbcInfoTokenSetOnly) { + this->connect(); + +#ifdef SQL_ATTR_DBC_INFO_TOKEN + // Verify that set-only attribute cannot be read + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_DBC_INFO_TOKEN, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY092); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrOdbcCursorsDMOnly) { + this->connect(); + + // Verify that DM-only attribute is handled by driver manager + SQLULEN cursor_attr; + SQLRETURN ret = + SQLGetConnectAttr(this->conn, SQL_ATTR_ODBC_CURSORS, &cursor_attr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(cursor_attr, SQL_CUR_USE_DRIVER); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTraceDMOnly) { + this->connect(); + + // Verify that DM-only attribute is handled by driver manager + SQLUINTEGER trace; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TRACE, &trace, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(trace, SQL_OPT_TRACE_OFF); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTraceFileDMOnly) { + this->connect(); + + // Verify that DM-only attribute is handled by driver manager + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLINTEGER outstrlen; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TRACEFILE, outstr, + ODBC_BUFFER_SIZE, &outstrlen); + + EXPECT_EQ(ret, SQL_SUCCESS); + std::string out_connection_string = + ODBC::SqlWcharToString(outstr, static_cast(outstrlen)); + EXPECT_TRUE(!out_connection_string.empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTranslateLibUnsupported) { + this->connect(); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLINTEGER outstrlen; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TRANSLATE_LIB, outstr, + ODBC_BUFFER_SIZE, &outstrlen); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTranslateOptionUnsupported) { + this->connect(); + + SQLINTEGER option; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TRANSLATE_OPTION, &option, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTxnIsolationUnsupported) { + this->connect(); + + SQLINTEGER isolation; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TXN_ISOLATION, &isolation, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, + TestSQLGetConnectAttrAsyncDbcFunctionsEnableUnsupported) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE + // Verifies that the Windows driver manager returns HY114 for unsupported functionality + SQLUINTEGER enable; + SQLRETURN ret = + SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE, &enable, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY114); +#endif + + this->disconnect(); +} + +// Tests for supported attributes + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcEventDefault) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_EVENT + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_EVENT, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ptr, reinterpret_cast(NULL)); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcPcallbackDefault) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCALLBACK, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ptr, reinterpret_cast(NULL)); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcPcontextDefault) { + this->connect(); + +#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCONTEXT, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ptr, reinterpret_cast(NULL)); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncEnableDefault) { + this->connect(); + + SQLULEN enable; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_ENABLE, &enable, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(enable, SQL_ASYNC_ENABLE_OFF); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAutoIpdDefault) { + this->connect(); + + SQLUINTEGER ipd; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_AUTO_IPD, &ipd, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ipd, static_cast(SQL_FALSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAutocommitDefault) { + this->connect(); + + SQLUINTEGER auto_commit; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_AUTOCOMMIT, &auto_commit, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(auto_commit, SQL_AUTOCOMMIT_ON); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrEnlistInDtcDefault) { + this->connect(); + + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ENLIST_IN_DTC, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ptr, reinterpret_cast(NULL)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrQuietModeDefault) { + this->connect(); + + SQLPOINTER ptr = NULL; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_QUIET_MODE, ptr, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ptr, reinterpret_cast(NULL)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAccessModeValid) { + this->connect(); + + // The driver always returns SQL_MODE_READ_WRITE + + // Check default value first + SQLUINTEGER mode = -1; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ACCESS_MODE, &mode, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(mode, SQL_MODE_READ_WRITE); + + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ACCESS_MODE, + reinterpret_cast(SQL_MODE_READ_WRITE), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + mode = -1; + + ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ACCESS_MODE, &mode, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(mode, SQL_MODE_READ_WRITE); + + // Attempt to set to SQL_MODE_READ_ONLY, driver should return warning and not error + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ACCESS_MODE, + reinterpret_cast(SQL_MODE_READ_ONLY), 0); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + + // Verify warning status + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_01S02); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrConnectionTimeoutValid) { + this->connect(); + + // Check default value first + SQLUINTEGER timeout = -1; + SQLRETURN ret = + SQLGetConnectAttr(this->conn, SQL_ATTR_CONNECTION_TIMEOUT, &timeout, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(timeout, 0); + + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_CONNECTION_TIMEOUT, + reinterpret_cast(42), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + timeout = -1; + + ret = SQLGetConnectAttr(this->conn, SQL_ATTR_CONNECTION_TIMEOUT, &timeout, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(timeout, 42); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrLoginTimeoutValid) { + this->connect(); + + // Check default value first + SQLUINTEGER timeout = -1; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_LOGIN_TIMEOUT, &timeout, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(timeout, 0); + + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_LOGIN_TIMEOUT, + reinterpret_cast(42), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + timeout = -1; + + ret = SQLGetConnectAttr(this->conn, SQL_ATTR_LOGIN_TIMEOUT, &timeout, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(timeout, 42); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrPacketSizeValid) { + this->connect(); + + // The driver always returns 0. PACKET_SIZE value is unused by the driver. + + // Check default value first + SQLUINTEGER size = -1; + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_PACKET_SIZE, &size, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(size, 0); + + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_PACKET_SIZE, + reinterpret_cast(0), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + size = -1; + + ret = SQLGetConnectAttr(this->conn, SQL_ATTR_PACKET_SIZE, &size, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(size, 0); + + // Attempt to set to non-zero value, driver should return warning and not error + ret = SQLSetConnectAttr(this->conn, SQL_ATTR_PACKET_SIZE, + reinterpret_cast(2), 0); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + + // Verify warning status + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_01S02); + + this->disconnect(); +} + +} // namespace integration_tests +} // namespace odbc +} // namespace flight +} // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 99fce0684c7..8f49fa0d265 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -302,16 +302,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnect) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = this->getConnectionString(); @@ -331,7 +331,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnect) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Check that outstr has same content as connect_str std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); @@ -349,17 +349,17 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnect) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { @@ -370,16 +370,16 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Invalid connect string std::string connect_str = getInvalidConnectionString(); @@ -398,7 +398,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { EXPECT_TRUE(ret == SQL_ERROR); - VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, error_state_28000); std::string out_connection_string = ODBC::SqlWcharToString(outstr, outstrlen); EXPECT_TRUE(out_connection_string.empty()); @@ -406,12 +406,12 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { @@ -422,16 +422,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = this->getConnectionString(); @@ -458,7 +458,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Remove DSN EXPECT_TRUE(UnregisterDsn(wdsn)); @@ -470,17 +470,17 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLConnect) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { @@ -491,16 +491,16 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = getConnectionString(); @@ -536,7 +536,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Remove DSN EXPECT_TRUE(UnregisterDsn(wdsn)); @@ -548,17 +548,17 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInputUidPwd) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { @@ -569,16 +569,16 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = getConnectionString(); @@ -613,7 +613,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { // so connection still fails despite passing valid uid in SQLConnect call EXPECT_TRUE(ret == SQL_ERROR); - VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("28000")); + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, error_state_28000); // Remove DSN EXPECT_TRUE(UnregisterDsn(wdsn)); @@ -621,12 +621,12 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectInvalidUid) { // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { @@ -637,16 +637,16 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = getConnectionString(); @@ -676,7 +676,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Remove DSN EXPECT_TRUE(UnregisterDsn(wdsn)); @@ -688,17 +688,17 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLConnectDSNPrecedence) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { @@ -709,16 +709,16 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Attempt to disconnect without a connection, expect to fail ret = SQLDisconnect(conn); @@ -726,17 +726,17 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { EXPECT_TRUE(ret == SQL_ERROR); // Expect ODBC driver manager to return error state - VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, std::string("08003")); + VerifyOdbcErrorState(SQL_HANDLE_DBC, conn, error_state_08003); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { @@ -747,16 +747,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Invalid connect string std::string connect_str = this->getInvalidConnectionString(); @@ -786,7 +786,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, HEADER_LEVEL, SQL_DIAG_NUMBER, &diag_number, sizeof(SQLINTEGER), &diag_number_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_EQ(diag_number, 1); @@ -797,7 +797,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SERVER_NAME, server_name, ODBC_BUFFER_SIZE, &server_name_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // SQL_DIAG_MESSAGE_TEXT SQLWCHAR message_text[ODBC_BUFFER_SIZE]; @@ -806,7 +806,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, message_text, ODBC_BUFFER_SIZE, &message_text_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_GT(message_text_length, 100); @@ -817,7 +817,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_NATIVE, &diag_native, sizeof(diag_native), &diag_native_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_EQ(diag_native, 200); @@ -829,7 +829,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { sql_state_size * driver::odbcabstraction::GetSqlWCharSize(), &sql_state_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // 28000 EXPECT_EQ(sql_state[0], '2'); @@ -841,12 +841,12 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { @@ -860,16 +860,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Invalid connect string std::string connect_str = this->getInvalidConnectionString(); @@ -900,19 +900,19 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, message_text, SQL_NTS, &message_text_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_GT(message_text_length, 100); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { @@ -923,16 +923,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Invalid connect string std::string connect_str = this->getInvalidConnectionString(); @@ -959,7 +959,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { ret = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, sql_state, &native_error, message, ODBC_BUFFER_SIZE, &message_length); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_GT(message_length, 120); @@ -975,12 +975,12 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } TYPED_TEST(FlightSQLODBCTestBase, TestConnect) { @@ -996,22 +996,22 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLAllocFreeStmt) { // Allocate a statement using alloc statement SQLRETURN ret = SQLAllocStmt(this->conn, &statement); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // TODO Uncomment once SQLExecDirect is implemented // SQLWCHAR sql_buffer[ODBC_BUFFER_SIZE] = L"SELECT 1"; // ret = SQLExecDirect(statement, sql_buffer, SQL_NTS); - // EXPECT_TRUE(ret == SQL_SUCCESS); + // EXPECT_EQ(ret, SQL_SUCCESS); // ret = SQLFreeStmt(statement, SQL_CLOSE); - // EXPECT_TRUE(ret == SQL_SUCCESS); + // EXPECT_EQ(ret, SQL_SUCCESS); // Free statement handle ret = SQLFreeStmt(statement, SQL_DROP); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); this->disconnect(); } @@ -1028,16 +1028,16 @@ TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Connect string std::string connect_str = this->getConnectionString(); @@ -1053,27 +1053,27 @@ TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { static_cast(connect_str0.size()), outstr, ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a statement using alloc statement ret = SQLAllocStmt(conn, &statement); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Disconnect from ODBC without closing the statement first ret = SQLDisconnect(conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } } // namespace integration_tests diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index 2d2a8f5d305..ac6c83e3cd1 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -29,25 +29,30 @@ namespace arrow { namespace flight { namespace odbc { namespace integration_tests { -void FlightSQLODBCRemoteTestBase::connect() { - std::string connect_str = getConnectionString(); - connectWithString(connect_str); -} -void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { + +void FlightSQLODBCRemoteTestBase::allocEnvConnHandles() { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Allocate a connection using alloc handle ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); +} + +void FlightSQLODBCRemoteTestBase::connect() { + allocEnvConnHandles(); + std::string connect_str = getConnectionString(); + connectWithString(connect_str); +} +void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { // Connect string std::vector connect_str0(connect_str.begin(), connect_str.end()); @@ -55,9 +60,9 @@ void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { SQLSMALLINT outstrlen; // Connecting to ODBC server. - ret = SQLDriverConnect(conn, NULL, &connect_str0[0], - static_cast(connect_str0.size()), outstr, - ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + SQLRETURN ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); if (ret != SQL_SUCCESS) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; @@ -66,36 +71,38 @@ void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { // Assert connection is successful before we continue ASSERT_TRUE(ret == SQL_SUCCESS); - // Allocate a statement using alloc handle - ret = SQLAllocHandle(SQL_HANDLE_STMT, conn, &stmt); + // TODO: enable after SQLGetStmtAttr is supported. + //// Allocate a statement using alloc handle + // ret = SQLAllocHandle(SQL_HANDLE_STMT, conn, &stmt); - ASSERT_TRUE(ret == SQL_SUCCESS); + // ASSERT_TRUE(ret == SQL_SUCCESS); } void FlightSQLODBCRemoteTestBase::disconnect() { - // Close statement - SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); + // TODO: enable after SQLGetStmtAttr is supported. + //// Close statement + // SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); - EXPECT_TRUE(ret == SQL_SUCCESS); + // EXPECT_EQ(ret, SQL_SUCCESS); // Disconnect from ODBC - ret = SQLDisconnect(conn); + SQLRETURN ret = SQLDisconnect(conn); if (ret != SQL_SUCCESS) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; } - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free connection handle ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free environment handle ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - EXPECT_TRUE(ret == SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); } std::string FlightSQLODBCRemoteTestBase::getConnectionString() { @@ -197,7 +204,7 @@ bool compareConnPropertyMap(Connection::ConnPropertyMap map1, } void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, - std::string expected_state) { + std::string_view expected_state) { using ODBC::SqlWcharToString; SQLWCHAR sql_state[7] = {}; 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 168204e8d0b..970d397806e 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 @@ -50,8 +50,10 @@ using driver::odbcabstraction::Connection; class FlightSQLODBCRemoteTestBase : public ::testing::Test { public: + /// \brief Allocate environment and connection handles + void allocEnvConnHandles(); /// \brief Connect to Arrow Flight SQL server using connection string defined in - /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" + /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN", allocate statement handle void connect(); /// \brief Connect to Arrow Flight SQL server using connection string void connectWithString(std::string connection_str); @@ -152,9 +154,19 @@ bool compareConnPropertyMap(Connection::ConnPropertyMap map1, /// Get error message from ODBC driver using SQLGetDiagRec std::string GetOdbcErrorMessage(SQLSMALLINT handle_type, SQLHANDLE handle); +static constexpr std::string_view error_state_01S02 = "01S02"; +static constexpr std::string_view error_state_08003 = "08003"; +static constexpr std::string_view error_state_28000 = "28000"; +static constexpr std::string_view error_state_HY000 = "HY000"; +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_HY114 = "HY114"; +static constexpr std::string_view error_state_HY118 = "HY118"; + /// Verify ODBC Error State void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, - std::string expected_state); + std::string_view expected_state); /// \brief Write connection string into DSN /// \param[in] connection_str the connection string. From 53f28cc0b4111dbe85b15a2da7fbc85b90bbb709 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:08:43 -0700 Subject: [PATCH 21/74] ODBC Test Segfault Fix * Move `connection_test.cc` to last in `CMakeLists.txt`, this resolves the seg fault. The seg fault was not reproducible on my local environment, even when I use the msys2 shell. * Rename test executable to flight_sql_odbc_test --- .gitignore | 1 + cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 12 +++++++++++- .../flight_sql/ui/dsn_configuration_window.cc | 2 +- .../sql/odbc/odbcabstraction/CMakeLists.txt | 17 +---------------- .../odbcabstraction/odbc_impl/encoding_utils.h | 2 +- .../arrow/flight/sql/odbc/tests/CMakeLists.txt | 5 +++-- .../sql/odbc/tests/connection_attr_test.cc | 16 +++++++--------- .../flight/sql/odbc/tests/connection_test.cc | 11 +++-------- .../flight/sql/odbc/tests/odbc_test_suite.cc | 11 +++-------- .../flight/sql/odbc/tests/odbc_test_suite.h | 10 ++-------- 10 files changed, 33 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 8354aa8f816..d9fda0a1641 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ dependency-reduced-pom.xml MANIFEST compile_commands.json build.ninja +build*/ # Generated Visual Studio files *.vcxproj diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 2017fa512bf..5cc4a8f3abe 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -34,6 +34,15 @@ endif() add_definitions(-DUNICODE=1) +include(FetchContent) +fetchcontent_declare(spdlog + URL https://github.com/gabime/spdlog/archive/76fb40d95455f249bd70824ecfcae7a8f0930fa3.zip + CONFIGURE_COMMAND + "" + BUILD_COMMAND + "") +fetchcontent_makeavailable(spdlog) + add_subdirectory(flight_sql) add_subdirectory(odbcabstraction) add_subdirectory(tests) @@ -71,7 +80,8 @@ add_arrow_lib(arrow_flight_sql_odbc ${ODBC_LIBRARIES} ${ODBCINST} odbcabstraction - arrow_odbc_spi_impl) + arrow_odbc_spi_impl + spdlog::spdlog) foreach(LIB_TARGET ${ARROW_FLIGHT_SQL_ODBC_LIBRARIES}) target_compile_definitions(${LIB_TARGET} PRIVATE ARROW_FLIGHT_SQL_ODBC_EXPORTING) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index 58bf9d37b48..78d56fb1f07 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -94,7 +94,7 @@ void DsnConfigurationWindow::Create() { throw odbcabstraction::DriverException(buf.str()); } } -//-AL- fix this file next + void DsnConfigurationWindow::OnCreate() { tabControl = CreateTabControl(ChildId::TAB_CONTROL); tabControl->AddTab(L"Common", COMMON_TAB); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt index e1e52492648..dd8b6dd2f1e 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/CMakeLists.txt @@ -63,19 +63,4 @@ set_target_properties(odbcabstraction RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/$/lib) -include(FetchContent) -fetchcontent_declare(spdlog - URL https://github.com/gabime/spdlog/archive/76fb40d95455f249bd70824ecfcae7a8f0930fa3.zip - CONFIGURE_COMMAND - "" - BUILD_COMMAND - "") -fetchcontent_getproperties(spdlog) -if(NOT spdlog_POPULATED) - fetchcontent_populate(spdlog) -endif() - -add_library(spdlog INTERFACE) -target_include_directories(spdlog INTERFACE ${spdlog_SOURCE_DIR}/include) - -target_link_libraries(odbcabstraction PUBLIC spdlog) +target_link_libraries(odbcabstraction PUBLIC spdlog::spdlog) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 3d6a80f835d..4c802fdfc31 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -83,7 +83,7 @@ inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, /// \param[in] msg_len Number of characters in wchar_msg /// \return wchar_msg in std::string format inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLSMALLINT msg_len = SQL_NTS) { - if (!wchar_msg || wchar_msg[0] == 0) { + if (!wchar_msg || wchar_msg[0] == 0 || msg_len == 0) { return std::string(); } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 9f30502ac36..0eea2fa0c90 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -29,10 +29,11 @@ set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS ../../example/sqlite_server.cc ../../example/sqlite_tables_schema_batch_reader.cc) -add_arrow_test(connection_test +add_arrow_test(flight_sql_odbc_test SOURCES - connection_test.cc connection_attr_test.cc + # Connection test needs to be put last to resolve segfault issue + connection_test.cc odbc_test_suite.cc odbc_test_suite.h ${ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS} diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc index d9d39319b27..3310709e901 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc @@ -26,10 +26,8 @@ #include "gtest/gtest.h" -namespace arrow { -namespace flight { -namespace odbc { -namespace integration_tests { +namespace arrow::flight::sql::odbc { + TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAsyncDbcEventUnsupported) { this->connect(); @@ -272,6 +270,9 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTraceFileDMOnly) { ODBC_BUFFER_SIZE, &outstrlen); EXPECT_EQ(ret, SQL_SUCCESS); + // Length is returned in bytes for SQLGetConnectAttr, + // we want the number of characters + outstrlen /= driver::odbcabstraction::GetSqlWCharSize(); std::string out_connection_string = ODBC::SqlWcharToString(outstr, static_cast(outstrlen)); EXPECT_TRUE(!out_connection_string.empty()); @@ -429,7 +430,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrEnlistInDtcDefault) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrQuietModeDefault) { this->connect(); - SQLPOINTER ptr = NULL; + HWND ptr = NULL; SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_QUIET_MODE, ptr, 0, 0); EXPECT_EQ(ret, SQL_SUCCESS); @@ -561,7 +562,4 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrPacketSizeValid) { this->disconnect(); } -} // namespace integration_tests -} // namespace odbc -} // namespace flight -} // namespace arrow +} // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 8f49fa0d265..10c4252338f 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -26,10 +26,8 @@ #include "gtest/gtest.h" -namespace arrow { -namespace flight { -namespace odbc { -namespace integration_tests { +namespace arrow::flight::sql::odbc { + TEST(SQLAllocHandle, TestSQLAllocHandleEnv) { // ODBC Environment SQLHENV env; @@ -1076,10 +1074,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { EXPECT_EQ(ret, SQL_SUCCESS); } -} // namespace integration_tests -} // namespace odbc -} // namespace flight -} // namespace arrow +} // namespace arrow::flight::sql::odbc int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index ac6c83e3cd1..11888e21058 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -25,10 +25,7 @@ #include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" -namespace arrow { -namespace flight { -namespace odbc { -namespace integration_tests { +namespace arrow::flight::sql::odbc { void FlightSQLODBCRemoteTestBase::allocEnvConnHandles() { // Allocate an environment handle @@ -272,7 +269,5 @@ bool writeDSN(Connection::ConnPropertyMap properties) { std::wstring wDriver = arrow::util::UTF8ToWideString(driver).ValueOr(L""); return RegisterDsn(config, wDriver.c_str()); } -} // namespace integration_tests -} // namespace odbc -} // namespace flight -} // namespace arrow + +} // namespace arrow::flight::sql::odbc 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 970d397806e..5f8b3b9c757 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 @@ -42,10 +42,7 @@ #define TEST_CONNECT_STR "ARROW_FLIGHT_SQL_ODBC_CONN" #define TEST_DSN "Apache Arrow Flight SQL Test DSN" -namespace arrow { -namespace flight { -namespace odbc { -namespace integration_tests { +namespace arrow::flight::sql::odbc { using driver::odbcabstraction::Connection; class FlightSQLODBCRemoteTestBase : public ::testing::Test { @@ -177,7 +174,4 @@ bool writeDSN(std::string connection_str); /// \param[in] properties map. /// \return true on success bool writeDSN(Connection::ConnPropertyMap properties); -} // namespace integration_tests -} // namespace odbc -} // namespace flight -} // namespace arrow +} // namespace arrow::flight::sql::odbc From 100328d7e2bf65844463992c051bbd420906baac Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:34:00 -0700 Subject: [PATCH 22/74] SQLExecDirect implementation * SQLGetStmtAttr stub implementation * Fix missing break statement in SQLGetDiagRec * Run ShutdownProtobufLibrary as part of test environment * Add comment for GH-46889 * Pass call options to executed prepared statement `call_options_` contains the authentication token which is needed --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 28 +++------ .../odbc/flight_sql/flight_sql_statement.cc | 27 +++++--- .../odbc/flight_sql/flight_sql_statement.h | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 35 +++++++++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 3 + .../odbc_impl/encoding_utils.h | 2 +- .../flight/sql/odbc/tests/CMakeLists.txt | 4 ++ .../flight/sql/odbc/tests/connection_test.cc | 13 ++-- .../flight/sql/odbc/tests/odbc_test_suite.cc | 29 ++++----- .../flight/sql/odbc/tests/statement_test.cc | 61 +++++++++++++++++++ 10 files changed, 154 insertions(+), 49 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 82153008345..722a3975186 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -125,6 +125,16 @@ SQLRETURN SQL_API SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNam SQLRETURN SQL_API SQLDisconnect(SQLHDBC conn) { return arrow::SQLDisconnect(conn); } +SQLRETURN SQL_API SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + return arrow::SQLGetStmtAttr(stmt, attribute, valuePtr, bufferLength, stringLengthPtr); +} + +SQLRETURN SQL_API SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, + SQLINTEGER textLength) { + return arrow::SQLExecDirect(stmt, queryText, textLength); +} + SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { @@ -189,14 +199,6 @@ SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, return SQL_ERROR; } -SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, SQLWCHAR* statementText, - SQLINTEGER textLength) { - LOG_DEBUG( - "SQLExecDirectW called with statementHandle: {}, statementText: {}, textLength: {}", - statementHandle, fmt::ptr(statementText), textLength); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { LOG_DEBUG("SQLExecute called with statementHandle: {}", statementHandle); return SQL_ERROR; @@ -241,16 +243,6 @@ SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLUSMALLINT col_or_Param return SQL_ERROR; } -SQLRETURN SQL_API SQLGetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER bufferLength, - SQLINTEGER* stringLengthPtr) { - LOG_DEBUG( - "SQLGetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " - "bufferLength: {}, stringLengthPtr: {}", - statementHandle, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { LOG_DEBUG("SQLGetTypeInfoW called with statementHandle: {} dataType: {}", statementHandle, dataType); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc index 1e8498ad7e3..4f94ef70168 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc @@ -52,9 +52,10 @@ using driver::odbcabstraction::Statement; namespace { void ClosePreparedStatementIfAny( - std::shared_ptr& prepared_statement) { + std::shared_ptr& prepared_statement, + const FlightCallOptions& options) { if (prepared_statement != nullptr) { - ThrowIfNotOK(prepared_statement->Close()); + ThrowIfNotOK(prepared_statement->Close(options)); prepared_statement.reset(); } } @@ -77,6 +78,10 @@ FlightSqlStatement::FlightSqlStatement( call_options_.timeout = TimeoutDuration{-1}; } +FlightSqlStatement::~FlightSqlStatement() { + ClosePreparedStatementIfAny(prepared_statement_, call_options_); +} + bool FlightSqlStatement::SetAttribute(StatementAttributeId attribute, const Attribute& value) { switch (attribute) { @@ -108,7 +113,7 @@ boost::optional FlightSqlStatement::GetAttribute( boost::optional> FlightSqlStatement::Prepare( const std::string& query) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.Prepare(call_options_, query); @@ -124,7 +129,9 @@ boost::optional> FlightSqlStatement::Prepare( bool FlightSqlStatement::ExecutePrepared() { assert(prepared_statement_.get() != nullptr); - Result> result = prepared_statement_->Execute(); + Result> result = + prepared_statement_->Execute(call_options_); + ThrowIfNotOK(result.status()); current_result_set_ = std::make_shared( @@ -135,7 +142,7 @@ bool FlightSqlStatement::ExecutePrepared() { } bool FlightSqlStatement::Execute(const std::string& query) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.Execute(call_options_, query); ThrowIfNotOK(result.status()); @@ -157,7 +164,7 @@ std::shared_ptr FlightSqlStatement::GetTables( const std::string* catalog_name, const std::string* schema_name, const std::string* table_name, const std::string* table_type, const ColumnNames& column_names) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); std::vector table_types; @@ -210,7 +217,7 @@ std::shared_ptr FlightSqlStatement::GetTables_V3( std::shared_ptr FlightSqlStatement::GetColumns_V2( const std::string* catalog_name, const std::string* schema_name, const std::string* table_name, const std::string* column_name) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.GetTables( call_options_, catalog_name, schema_name, table_name, true, nullptr); @@ -231,7 +238,7 @@ std::shared_ptr FlightSqlStatement::GetColumns_V2( std::shared_ptr FlightSqlStatement::GetColumns_V3( const std::string* catalog_name, const std::string* schema_name, const std::string* table_name, const std::string* column_name) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.GetTables( call_options_, catalog_name, schema_name, table_name, true, nullptr); @@ -250,7 +257,7 @@ std::shared_ptr FlightSqlStatement::GetColumns_V3( } std::shared_ptr FlightSqlStatement::GetTypeInfo_V2(int16_t data_type) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.GetXdbcTypeInfo(call_options_); ThrowIfNotOK(result.status()); @@ -268,7 +275,7 @@ std::shared_ptr FlightSqlStatement::GetTypeInfo_V2(int16_t data_type) } std::shared_ptr FlightSqlStatement::GetTypeInfo_V3(int16_t data_type) { - ClosePreparedStatementIfAny(prepared_statement_); + ClosePreparedStatementIfAny(prepared_statement_, call_options_); Result> result = sql_client_.GetXdbcTypeInfo(call_options_); ThrowIfNotOK(result.status()); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h index 7ffb02ba40b..56341223259 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h @@ -50,6 +50,7 @@ class FlightSqlStatement : public odbcabstraction::Statement { arrow::flight::sql::FlightSqlClient& sql_client, arrow::flight::FlightCallOptions call_options, const odbcabstraction::MetadataSettings& metadata_settings); + ~FlightSqlStatement(); bool SetAttribute(StatementAttributeId attribute, const Attribute& value) override; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 1e59791f2e2..4053375b430 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -527,6 +527,7 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re case SQL_HANDLE_STMT: { auto* statement = ODBCStatement::of(handle); diagnostics = &statement->GetDiagnostics(); + break; } default: @@ -870,4 +871,38 @@ SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePt }); } +SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + using ODBC::ODBCStatement; + // TODO: complete implementation of SQLGetStmtAttrW and write tests + LOG_DEBUG( + "SQLGetStmtAttrW called with stmt: {}, attribute: {}, valuePtr: {}, " + "bufferLength: {}, stringLengthPtr: {}", + stmt, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + bool isUnicode = true; + statement->GetStmtAttr(attribute, valuePtr, bufferLength, stringLengthPtr, isUnicode); + // TODO: change GetStmtAttr to return SQLRETURN instead of void, + // and return value from GetStmtAttr instead + return SQL_SUCCESS; + }); +} + +SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength) { + LOG_DEBUG("SQLExecDirectW called with stmt: {}, queryText: {}, textLength: {}", stmt, + fmt::ptr(queryText), textLength); + using ODBC::ODBCStatement; + // The driver is built to handle select statements only. + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + std::string query = ODBC::SqlWcharToString(queryText, textLength); + + statement->Prepare(query); + statement->ExecutePrepared(); + + return SQL_SUCCESS; + }); +} } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 0118ba77434..af8fc4056ff 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -60,4 +60,7 @@ SQLRETURN SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, SQLRETURN SQLDisconnect(SQLHDBC conn); SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, SQLSMALLINT bufLen, SQLSMALLINT* length); +SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); +SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h index 4c802fdfc31..94f4569ba89 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h @@ -82,7 +82,7 @@ inline size_t ConvertToSqlWChar(const std::string_view& str, SQLWCHAR* buffer, /// \param[in] wchar_msg SqlWchar to convert /// \param[in] msg_len Number of characters in wchar_msg /// \return wchar_msg in std::string format -inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLSMALLINT msg_len = SQL_NTS) { +inline std::string SqlWcharToString(SQLWCHAR* wchar_msg, SQLINTEGER msg_len = SQL_NTS) { if (!wchar_msg || wchar_msg[0] == 0 || msg_len == 0) { return std::string(); } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 0eea2fa0c90..7dcd11935cc 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -32,10 +32,14 @@ set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS add_arrow_test(flight_sql_odbc_test SOURCES connection_attr_test.cc + statement_test.cc # Connection test needs to be put last to resolve segfault issue connection_test.cc odbc_test_suite.cc odbc_test_suite.h + # Enable Protobuf cleanup after test execution + # GH-46889: move protobuf_test_util to a more common location + ../../../../engine/substrait/protobuf_test_util.cc ${ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS} EXTRA_LINK_LIBS ${ODBC_LIBRARIES} diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 10c4252338f..22fd4a5ea12 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -24,6 +24,7 @@ #include #include +#include "google/protobuf/message_lite.h" #include "gtest/gtest.h" namespace arrow::flight::sql::odbc { @@ -996,15 +997,15 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLAllocFreeStmt) { EXPECT_EQ(ret, SQL_SUCCESS); - // TODO Uncomment once SQLExecDirect is implemented - // SQLWCHAR sql_buffer[ODBC_BUFFER_SIZE] = L"SELECT 1"; - // ret = SQLExecDirect(statement, sql_buffer, SQL_NTS); + SQLWCHAR sql_buffer[ODBC_BUFFER_SIZE] = L"SELECT 1"; + ret = SQLExecDirect(statement, sql_buffer, SQL_NTS); - // EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); - // ret = SQLFreeStmt(statement, SQL_CLOSE); + // Close statement handle + ret = SQLFreeStmt(statement, SQL_CLOSE); - // EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Free statement handle ret = SQLFreeStmt(statement, SQL_DROP); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index 11888e21058..f20cde44136 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -68,22 +68,20 @@ void FlightSQLODBCRemoteTestBase::connectWithString(std::string connect_str) { // Assert connection is successful before we continue ASSERT_TRUE(ret == SQL_SUCCESS); - // TODO: enable after SQLGetStmtAttr is supported. - //// Allocate a statement using alloc handle - // ret = SQLAllocHandle(SQL_HANDLE_STMT, conn, &stmt); + // Allocate a statement using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_STMT, conn, &stmt); - // ASSERT_TRUE(ret == SQL_SUCCESS); + ASSERT_TRUE(ret == SQL_SUCCESS); } void FlightSQLODBCRemoteTestBase::disconnect() { - // TODO: enable after SQLGetStmtAttr is supported. - //// Close statement - // SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); + // Close statement + SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); - // EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ret, SQL_SUCCESS); // Disconnect from ODBC - SQLRETURN ret = SQLDisconnect(conn); + ret = SQLDisconnect(conn); if (ret != SQL_SUCCESS) { std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; @@ -126,12 +124,15 @@ std::string FindTokenInCallHeaders(const CallHeaders& incoming_headers) { return (::toupper(char1) == ::toupper(char2)); }; - const std::string auth_val(incoming_headers.find(kAuthHeader)->second); std::string bearer_token(""); - if (auth_val.size() > kBearerPrefix.length()) { - if (std::equal(auth_val.begin(), auth_val.begin() + kBearerPrefix.length(), - kBearerPrefix.begin(), char_compare)) { - bearer_token = auth_val.substr(kBearerPrefix.length()); + auto authHeader = incoming_headers.find(kAuthHeader); + if (authHeader != incoming_headers.end()) { + const std::string auth_val(authHeader->second); + if (auth_val.size() > kBearerPrefix.length()) { + if (std::equal(auth_val.begin(), auth_val.begin() + kBearerPrefix.length(), + kBearerPrefix.begin(), char_compare)) { + bearer_token = auth_val.substr(kBearerPrefix.length()); + } } } return bearer_token; 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..5f9ee060760 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -0,0 +1,61 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectSimpleQuery) { + 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); + + // TODO: after SQLFetch and SQLGetData are implemented, fetch data to verify + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectInvalidQuery) { + this->connect(); + + std::wstring wsql = L"SELECT;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_ERROR); + // ODBC provides generic error code HY000 to all statement errors + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY000); + + this->disconnect(); +} +} // namespace arrow::flight::sql::odbc From 0a86a39621ae0cf013443fe5f9b1200209245970 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 3 Jul 2025 10:17:51 -0700 Subject: [PATCH 23/74] Implement SQLGetInfo --- .../sql/odbc/flight_sql/get_info_cache.cc | 14 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 21 +- .../odbc_impl/odbc_connection.h | 4 +- .../odbc_impl/odbc_connection.cc | 124 +- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../sql/odbc/tests/connection_info_test.cc | 1481 +++++++++++++++++ 6 files changed, 1566 insertions(+), 79 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc index d18322badbe..cf10c658dbf 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc @@ -1173,6 +1173,7 @@ void GetInfoCache::LoadDefaultsForMissingEntries() { SetDefaultIfMissing(info_, SQL_CONVERT_DECIMAL, static_cast(0)); SetDefaultIfMissing(info_, SQL_CONVERT_DOUBLE, static_cast(0)); SetDefaultIfMissing(info_, SQL_CONVERT_FLOAT, static_cast(0)); + SetDefaultIfMissing(info_, SQL_CONVERT_FUNCTIONS, static_cast(0)); SetDefaultIfMissing(info_, SQL_CONVERT_GUID, static_cast(0)); SetDefaultIfMissing(info_, SQL_CONVERT_INTEGER, static_cast(0)); SetDefaultIfMissing(info_, SQL_CONVERT_INTERVAL_YEAR_MONTH, static_cast(0)); @@ -1251,6 +1252,7 @@ void GetInfoCache::LoadDefaultsForMissingEntries() { SetDefaultIfMissing(info_, SQL_MAX_COLUMNS_IN_ORDER_BY, static_cast(0)); SetDefaultIfMissing(info_, SQL_MAX_COLUMNS_IN_SELECT, static_cast(0)); SetDefaultIfMissing(info_, SQL_MAX_COLUMNS_IN_TABLE, static_cast(0)); + SetDefaultIfMissing(info_, SQL_MAX_CONCURRENT_ACTIVITIES, static_cast(0)); SetDefaultIfMissing(info_, SQL_MAX_CURSOR_NAME_LEN, static_cast(0)); SetDefaultIfMissing(info_, SQL_MAX_DRIVER_CONNECTIONS, static_cast(0)); SetDefaultIfMissing(info_, SQL_MAX_IDENTIFIER_LEN, static_cast(65535)); @@ -1270,6 +1272,7 @@ void GetInfoCache::LoadDefaultsForMissingEntries() { SetDefaultIfMissing(info_, SQL_OJ_CAPABILITIES, static_cast(SQL_OJ_LEFT | SQL_OJ_RIGHT | SQL_OJ_FULL)); SetDefaultIfMissing(info_, SQL_ORDER_BY_COLUMNS_IN_SELECT, "Y"); + SetDefaultIfMissing(info_, SQL_OUTER_JOINS, "N"); SetDefaultIfMissing(info_, SQL_PROCEDURE_TERM, ""); SetDefaultIfMissing(info_, SQL_PROCEDURES, "N"); SetDefaultIfMissing(info_, SQL_QUOTED_IDENTIFIER_CASE, @@ -1278,6 +1281,7 @@ void GetInfoCache::LoadDefaultsForMissingEntries() { SetDefaultIfMissing(info_, SQL_SCHEMA_USAGE, static_cast(SQL_SU_DML_STATEMENTS)); SetDefaultIfMissing(info_, SQL_SEARCH_PATTERN_ESCAPE, "\\"); + SetDefaultIfMissing(info_, SQL_SPECIAL_CHARACTERS, ""); SetDefaultIfMissing( info_, SQL_SERVER_NAME, "Arrow Flight SQL Server"); // This might actually need to be the hostname. @@ -1332,6 +1336,16 @@ void GetInfoCache::LoadDefaultsForMissingEntries() { SQL_FN_TSI_FRAC_SECOND | SQL_FN_TSI_SECOND | SQL_FN_TSI_MINUTE | SQL_FN_TSI_HOUR | SQL_FN_TSI_DAY | SQL_FN_TSI_WEEK | SQL_FN_TSI_MONTH | SQL_FN_TSI_QUARTER | SQL_FN_TSI_YEAR)); + SetDefaultIfMissing( + info_, SQL_TIMEDATE_FUNCTIONS, + static_cast( + SQL_FN_TD_CURRENT_DATE | SQL_FN_TD_CURRENT_TIME | SQL_FN_TD_CURRENT_TIMESTAMP | + SQL_FN_TD_CURDATE | SQL_FN_TD_CURTIME | SQL_FN_TD_DAYNAME | + SQL_FN_TD_DAYOFMONTH | SQL_FN_TD_DAYOFWEEK | SQL_FN_TD_DAYOFYEAR | + SQL_FN_TD_EXTRACT | SQL_FN_TD_HOUR | SQL_FN_TD_MINUTE | SQL_FN_TD_MONTH | + SQL_FN_TD_MONTHNAME | SQL_FN_TD_NOW | SQL_FN_TD_QUARTER | SQL_FN_TD_SECOND | + SQL_FN_TD_TIMESTAMPADD | SQL_FN_TD_TIMESTAMPDIFF | SQL_FN_TD_WEEK | + SQL_FN_TD_YEAR)); SetDefaultIfMissing(info_, SQL_UNION, static_cast(SQL_U_UNION | SQL_U_UNION_ALL)); SetDefaultIfMissing(info_, SQL_XOPEN_CLI_YEAR, "1995"); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 4053375b430..3faf56bf290 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -847,27 +847,26 @@ SQLRETURN SQLDisconnect(SQLHDBC conn) { } SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, - SQLSMALLINT bufLen, SQLSMALLINT* length) { - // TODO: complete implementation of SQLGetInfoW and write tests + SQLSMALLINT bufLen, SQLSMALLINT* stringLengthPtr) { using ODBC::ODBCConnection; LOG_DEBUG( - "SQLGetInfoW called with conn: {}, infoType: {}, infoValuePtr: {}, bufLen: {}, " - "length: {}", - conn, infoType, infoValuePtr, bufLen, fmt::ptr(length)); + "SQLGetInfo called with conn: {}, infoType: {}, infoValuePtr: {}, bufLen: {}, " + "stringLengthPtr: {}", + conn, infoType, infoValuePtr, bufLen, fmt::ptr(stringLengthPtr)); return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); - // Partially stubbed implementation of SQLGetInfoW - if (infoType == SQL_DRIVER_ODBC_VER) { - std::string_view ver("03.80"); + // Set character type to be Unicode by default + const bool isUnicode = true; - return ODBC::GetStringAttribute(true, ver, true, infoValuePtr, bufLen, length, - connection->GetDiagnostics()); + if (!infoValuePtr && !stringLengthPtr) { + return static_cast SQL_ERROR; } - return static_cast(SQL_ERROR); + return connection->GetInfo(infoType, infoValuePtr, bufLen, stringLengthPtr, + isUnicode); }); } diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h index 92946e5a0a5..0e9498bcb8a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h @@ -60,8 +60,8 @@ class ODBCConnection : public ODBCHandle { const driver::odbcabstraction::Connection::ConnPropertyMap& properties, std::vector& missing_properties); - void GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, SQLSMALLINT bufferLength, - SQLSMALLINT* outputLength, bool isUnicode); + SQLRETURN GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, SQLSMALLINT bufferLength, + SQLSMALLINT* outputLength, bool isUnicode); void SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength, bool isUnicode); SQLRETURN GetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index c9867409407..1f3d365397a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -136,152 +136,144 @@ void ODBCConnection::connect(std::string dsn, m_attributeTrackingStatement = std::make_shared(*this, spiStatement); } -void ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, - SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, - bool isUnicode) { +SQLRETURN ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, + SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, + bool isUnicode) { switch (infoType) { case SQL_ACTIVE_ENVIRONMENTS: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; #ifdef SQL_ASYNC_DBC_FUNCTIONS case SQL_ASYNC_DBC_FUNCTIONS: GetAttribute(static_cast(SQL_ASYNC_DBC_NOT_CAPABLE), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; #endif case SQL_ASYNC_MODE: GetAttribute(static_cast(SQL_AM_NONE), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; #ifdef SQL_ASYNC_NOTIFICATION case SQL_ASYNC_NOTIFICATION: GetAttribute(static_cast(SQL_ASYNC_NOTIFICATION_NOT_CAPABLE), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; #endif case SQL_BATCH_ROW_COUNT: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_BATCH_SUPPORT: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_DATA_SOURCE_NAME: - GetStringAttribute(isUnicode, m_dsn, true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, m_dsn, true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_DRIVER_ODBC_VER: - GetStringAttribute(isUnicode, "03.80", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "03.80", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_DYNAMIC_CURSOR_ATTRIBUTES1: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_DYNAMIC_CURSOR_ATTRIBUTES2: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1: GetAttribute(static_cast(SQL_CA1_NEXT), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2: GetAttribute(static_cast(SQL_CA2_READ_ONLY_CONCURRENCY), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_FILE_USAGE: GetAttribute(static_cast(SQL_FILE_NOT_SUPPORTED), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_KEYSET_CURSOR_ATTRIBUTES1: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_KEYSET_CURSOR_ATTRIBUTES2: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_MAX_ASYNC_CONCURRENT_STATEMENTS: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_ODBC_INTERFACE_CONFORMANCE: GetAttribute(static_cast(SQL_OIC_CORE), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; // case SQL_ODBC_STANDARD_CLI_CONFORMANCE: - mentioned in SQLGetInfo spec with no // description and there is no constant for this. case SQL_PARAM_ARRAY_ROW_COUNTS: GetAttribute(static_cast(SQL_PARC_NO_BATCH), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_PARAM_ARRAY_SELECTS: GetAttribute(static_cast(SQL_PAS_NO_SELECT), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_ROW_UPDATES: - GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_SCROLL_OPTIONS: GetAttribute(static_cast(SQL_SO_FORWARD_ONLY), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_STATIC_CURSOR_ATTRIBUTES1: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_STATIC_CURSOR_ATTRIBUTES2: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_BOOKMARK_PERSISTENCE: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_DESCRIBE_PARAMETER: - GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_MULT_RESULT_SETS: - GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_MULTIPLE_ACTIVE_TXN: - GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_NEED_LONG_DATA_LEN: - GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "N", true, value, bufferLength, outputLength, + GetDiagnostics()); case SQL_TXN_CAPABLE: GetAttribute(static_cast(SQL_TC_NONE), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_TXN_ISOLATION_OPTION: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_TABLE_TERM: - GetStringAttribute(isUnicode, "table", true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, "table", true, value, bufferLength, outputLength, + GetDiagnostics()); // Deprecated ODBC 2.x fields required for backwards compatibility. case SQL_ODBC_API_CONFORMANCE: GetAttribute(static_cast(SQL_OAC_LEVEL1), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_FETCH_DIRECTION: GetAttribute(static_cast(SQL_FETCH_NEXT), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_LOCK_TYPES: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_POS_OPERATIONS: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_POSITIONED_STATEMENTS: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_SCROLL_CONCURRENCY: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; case SQL_STATIC_SENSITIVITY: GetAttribute(static_cast(0), value, bufferLength, outputLength); - break; + return SQL_SUCCESS; // Driver-level string properties. case SQL_USER_NAME: @@ -316,9 +308,8 @@ void ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, case SQL_XOPEN_CLI_YEAR: { const auto& info = m_spiConnection->GetInfo(infoType); const std::string& infoValue = boost::get(info); - GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, + GetDiagnostics()); } // Driver-level 32-bit integer properties. @@ -408,7 +399,7 @@ void ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, const auto& info = m_spiConnection->GetInfo(infoType); uint32_t infoValue = boost::get(info); GetAttribute(infoValue, value, bufferLength, outputLength); - break; + return SQL_SUCCESS; } // Driver-level 16-bit integer properties. @@ -443,7 +434,7 @@ void ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, const auto& info = m_spiConnection->GetInfo(infoType); uint16_t infoValue = boost::get(info); GetAttribute(infoValue, value, bufferLength, outputLength); - break; + return SQL_SUCCESS; } // Special case - SQL_DATABASE_NAME is an alias for SQL_ATTR_CURRENT_CATALOG. @@ -453,13 +444,14 @@ void ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, throw DriverException("Optional feature not supported.", "HYC00"); } const std::string& infoValue = boost::get(*attr); - GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, - GetDiagnostics()); - break; + return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, + GetDiagnostics()); } default: - throw DriverException("Unknown SQLGetInfo type: " + std::to_string(infoType)); + throw DriverException("Unknown SQLGetInfo type: " + std::to_string(infoType), "HY096"); } + + return SQL_ERROR; } void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, SQLPOINTER value, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 7dcd11935cc..1a3da7fad31 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -32,6 +32,7 @@ set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS add_arrow_test(flight_sql_odbc_test SOURCES connection_attr_test.cc + connection_info_test.cc statement_test.cc # Connection test needs to be put last to resolve segfault issue connection_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc new file mode 100644 index 00000000000..b8734a8f8b2 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc @@ -0,0 +1,1481 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +// Helper Functions + +// Validate unsigned short SQLUSMALLINT return value +void validate(SQLHDBC connection, SQLUSMALLINT infoType, SQLUSMALLINT expected_value) { + SQLUSMALLINT info_value; + SQLSMALLINT message_length; + + SQLRETURN ret = SQLGetInfo(connection, infoType, &info_value, 0, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(info_value, expected_value); +} + +// Validate unsigned long SQLUINTEGER return value +void validate(SQLHDBC connection, SQLUSMALLINT infoType, SQLUINTEGER expected_value) { + SQLUINTEGER info_value; + SQLSMALLINT message_length; + + SQLRETURN ret = SQLGetInfo(connection, infoType, &info_value, 0, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(info_value, expected_value); +} + +// Validate unsigned length SQLULEN return value +void validate(SQLHDBC connection, SQLUSMALLINT infoType, SQLULEN expected_value) { + SQLULEN info_value; + SQLSMALLINT message_length; + + SQLRETURN ret = SQLGetInfo(connection, infoType, &info_value, 0, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(info_value, expected_value); +} + +// Validate wchar string SQLWCHAR return value +void validate(SQLHDBC connection, SQLUSMALLINT infoType, SQLWCHAR* expected_value) { + SQLWCHAR info_value[ODBC_BUFFER_SIZE] = L""; + SQLSMALLINT message_length; + + SQLRETURN ret = + SQLGetInfo(connection, infoType, info_value, ODBC_BUFFER_SIZE, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(*info_value, *expected_value); +} + +// Validate unsigned long SQLUINTEGER return value is greater than +void validateGreaterThan(SQLHDBC connection, SQLUSMALLINT infoType, + SQLUINTEGER compared_value) { + SQLUINTEGER info_value; + SQLSMALLINT message_length; + + SQLRETURN ret = SQLGetInfo(connection, infoType, &info_value, 0, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(info_value, compared_value); +} + +// Validate unsigned length SQLULEN return value is greater than +void validateGreaterThan(SQLHDBC connection, SQLUSMALLINT infoType, + SQLULEN compared_value) { + SQLULEN info_value; + SQLSMALLINT message_length; + + SQLRETURN ret = SQLGetInfo(connection, infoType, &info_value, 0, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(info_value, compared_value); +} + +// Validate wchar string SQLWCHAR return value is not empty +void validateNotEmptySQLWCHAR(SQLHDBC connection, SQLUSMALLINT infoType, + bool allowTruncation) { + SQLWCHAR info_value[ODBC_BUFFER_SIZE] = L""; + SQLSMALLINT message_length; + + SQLRETURN ret = + SQLGetInfo(connection, infoType, info_value, ODBC_BUFFER_SIZE, &message_length); + + if (allowTruncation && ret == SQL_SUCCESS_WITH_INFO) { + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + } else { + EXPECT_EQ(ret, SQL_SUCCESS); + } + + EXPECT_GT(wcslen(info_value), 0); +} + +// Driver Information + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACTIVE_ENVIRONMENTS) { + this->connect(); + + validate(this->conn, SQL_ACTIVE_ENVIRONMENTS, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_DBC_FUNCTIONS) { + this->connect(); + +#ifdef SQL_ASYNC_DBC_FUNCTIONS + validate(this->conn, SQL_ASYNC_DBC_FUNCTIONS, + static_cast(SQL_ASYNC_DBC_NOT_CAPABLE)); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_MODE) { + this->connect(); + + validate(this->conn, SQL_ASYNC_MODE, static_cast(SQL_AM_NONE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_NOTIFICATION) { + this->connect(); + +#ifdef SQL_ASYNC_NOTIFICATION + validate(this->conn, SQL_ASYNC_NOTIFICATION, + static_cast(SQL_ASYNC_NOTIFICATION_NOT_CAPABLE)); +#endif + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_ROW_COUNT) { + this->connect(); + + validate(this->conn, SQL_BATCH_ROW_COUNT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_SUPPORT) { + this->connect(); + + validate(this->conn, SQL_BATCH_SUPPORT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_NAME) { + this->connect(); + + validate(this->conn, SQL_DATA_SOURCE_NAME, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_AWARE_POOLING_SUPPORTED) { + // A driver does not need to implement SQL_DRIVER_AWARE_POOLING_SUPPORTED and the + // Driver Manager will not honor to the driver's return value. + this->connect(); + + validate(this->conn, SQL_DRIVER_AWARE_POOLING_SUPPORTED, + static_cast(SQL_DRIVER_AWARE_POOLING_NOT_CAPABLE)); + + this->disconnect(); +} + +// These information types are implemented by the Driver Manager alone. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDBC) { + this->connect(); + + // Value returned from driver manager is the connection address + validateGreaterThan(this->conn, SQL_DRIVER_HDBC, static_cast(0)); + + this->disconnect(); +} + +// These information types are implemented by the Driver Manager alone. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDESC) { + // TODO This is failing due to no descriptor being created + GTEST_SKIP(); + this->connect(); + + validate(this->conn, SQL_DRIVER_HDESC, static_cast(0)); + + this->disconnect(); +} + +// These information types are implemented by the Driver Manager alone. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HENV) { + this->connect(); + + // Value returned from driver manager is the env address + validateGreaterThan(this->conn, SQL_DRIVER_HENV, static_cast(0)); + + this->disconnect(); +} + +// These information types are implemented by the Driver Manager alone. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HLIB) { + this->connect(); + + validateGreaterThan(this->conn, SQL_DRIVER_HLIB, static_cast(0)); + + this->disconnect(); +} + +// These information types are implemented by the Driver Manager alone. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HSTMT) { + // TODO This is failing due to no statement being created + // This should run after SQLGetStmtAttr is implemented + GTEST_SKIP(); + this->connect(); + + validate(this->conn, SQL_DRIVER_HSTMT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_NAME) { + this->connect(); + + validate(this->conn, SQL_DRIVER_NAME, L"Arrow Flight ODBC Driver"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_ODBC_VER) { + this->connect(); + + validate(this->conn, SQL_DRIVER_ODBC_VER, L"03.80"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_VER) { + this->connect(); + + validate(this->conn, SQL_DRIVER_VER, L"00.09.0000.0"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES1) { + this->connect(); + + validate(this->conn, SQL_DYNAMIC_CURSOR_ATTRIBUTES1, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES2) { + this->connect(); + + validate(this->conn, SQL_DYNAMIC_CURSOR_ATTRIBUTES2, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1) { + this->connect(); + + validate(this->conn, SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1, + static_cast(SQL_CA1_NEXT)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2) { + this->connect(); + + validate(this->conn, SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2, + static_cast(SQL_CA2_READ_ONLY_CONCURRENCY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FILE_USAGE) { + this->connect(); + + validate(this->conn, SQL_FILE_USAGE, static_cast(SQL_FILE_NOT_SUPPORTED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_GETDATA_EXTENSIONS) { + this->connect(); + + validate(this->conn, SQL_GETDATA_EXTENSIONS, + static_cast(SQL_GD_ANY_COLUMN | SQL_GD_ANY_ORDER)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INFO_SCHEMA_VIEWS) { + this->connect(); + + validate(this->conn, SQL_INFO_SCHEMA_VIEWS, + static_cast(SQL_ISV_TABLES | SQL_ISV_COLUMNS | SQL_ISV_VIEWS)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES1) { + this->connect(); + + validate(this->conn, SQL_KEYSET_CURSOR_ATTRIBUTES1, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES2) { + this->connect(); + + validate(this->conn, SQL_KEYSET_CURSOR_ATTRIBUTES2, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ASYNC_CONCURRENT_STATEMENTS) { + this->connect(); + + validate(this->conn, SQL_MAX_ASYNC_CONCURRENT_STATEMENTS, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CONCURRENT_ACTIVITIES) { + this->connect(); + + validate(this->conn, SQL_MAX_CONCURRENT_ACTIVITIES, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_DRIVER_CONNECTIONS) { + this->connect(); + + validate(this->conn, SQL_MAX_DRIVER_CONNECTIONS, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_INTERFACE_CONFORMANCE) { + this->connect(); + + validate(this->conn, SQL_ODBC_INTERFACE_CONFORMANCE, + static_cast(SQL_OIC_CORE)); + + this->disconnect(); +} + +// case SQL_ODBC_STANDARD_CLI_CONFORMANCE: - mentioned in SQLGetInfo spec with no +// description and there is no constant for this. +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_STANDARD_CLI_CONFORMANCE) { + // Type commented out in odbc_connection.cc + GTEST_SKIP(); + this->connect(); + + // Type does not exist in sql.h + // validate(this->conn, SQL_ODBC_STANDARD_CLI_CONFORMANCE, + // static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_VER) { + // This is implemented only in the Driver Manager. + this->connect(); + + validate(this->conn, SQL_ODBC_VER, L"03.80.0000"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_ROW_COUNTS) { + this->connect(); + + validate(this->conn, SQL_PARAM_ARRAY_ROW_COUNTS, + static_cast(SQL_PARC_NO_BATCH)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_SELECTS) { + this->connect(); + + validate(this->conn, SQL_PARAM_ARRAY_SELECTS, + static_cast(SQL_PAS_NO_SELECT)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ROW_UPDATES) { + this->connect(); + + validate(this->conn, SQL_ROW_UPDATES, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SEARCH_PATTERN_ESCAPE) { + this->connect(); + + validate(this->conn, SQL_SEARCH_PATTERN_ESCAPE, L"\\"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SERVER_NAME) { + this->connect(); + + validateNotEmptySQLWCHAR(this->conn, SQL_SERVER_NAME, false); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES1) { + this->connect(); + + validate(this->conn, SQL_STATIC_CURSOR_ATTRIBUTES1, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES2) { + this->connect(); + + validate(this->conn, SQL_STATIC_CURSOR_ATTRIBUTES2, static_cast(0)); + + this->disconnect(); +} + +// DBMS Product Information + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATABASE_NAME) { + this->connect(); + + validate(this->conn, SQL_DATABASE_NAME, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_NAME) { + this->connect(); + + validateNotEmptySQLWCHAR(this->conn, SQL_DBMS_NAME, false); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_VER) { + this->connect(); + + validateNotEmptySQLWCHAR(this->conn, SQL_DBMS_VER, false); + + this->disconnect(); +} + +// Data Source Information + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_PROCEDURES) { + this->connect(); + + validate(conn, SQL_ACCESSIBLE_PROCEDURES, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_TABLES) { + this->connect(); + + validate(conn, SQL_ACCESSIBLE_TABLES, L"Y"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BOOKMARK_PERSISTENCE) { + this->connect(); + + validate(conn, SQL_BOOKMARK_PERSISTENCE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_TERM) { + this->connect(); + + validate(conn, SQL_CATALOG_TERM, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLLATION_SEQ) { + this->connect(); + + validate(conn, SQL_COLLATION_SEQ, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONCAT_NULL_BEHAVIOR) { + this->connect(); + + validate(conn, SQL_CONCAT_NULL_BEHAVIOR, static_cast(SQL_CB_NULL)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_COMMIT_BEHAVIOR) { + this->connect(); + + validate(conn, SQL_CURSOR_COMMIT_BEHAVIOR, static_cast(SQL_CB_CLOSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_ROLLBACK_BEHAVIOR) { + this->connect(); + + validate(conn, SQL_CURSOR_ROLLBACK_BEHAVIOR, static_cast(SQL_CB_CLOSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_SENSITIVITY) { + this->connect(); + + validate(conn, SQL_CURSOR_SENSITIVITY, static_cast(SQL_UNSPECIFIED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_READ_ONLY) { + this->connect(); + + validate(conn, SQL_DATA_SOURCE_READ_ONLY, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DEFAULT_TXN_ISOLATION) { + this->connect(); + + validate(conn, SQL_DEFAULT_TXN_ISOLATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DESCRIBE_PARAMETER) { + this->connect(); + + validate(conn, SQL_DESCRIBE_PARAMETER, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULT_RESULT_SETS) { + this->connect(); + + validate(conn, SQL_MULT_RESULT_SETS, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULTIPLE_ACTIVE_TXN) { + this->connect(); + + validate(conn, SQL_MULTIPLE_ACTIVE_TXN, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_NEED_LONG_DATA_LEN) { + this->connect(); + + validate(conn, SQL_NEED_LONG_DATA_LEN, L"N"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NULL_COLLATION) { + this->connect(); + + validate(conn, SQL_NULL_COLLATION, static_cast(SQL_NC_START)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_PROCEDURE_TERM) { + this->connect(); + + validate(conn, SQL_PROCEDURE_TERM, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCHEMA_TERM) { + this->connect(); + + validate(conn, SQL_SCHEMA_TERM, L"schema"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCROLL_OPTIONS) { + this->connect(); + + validate(conn, SQL_SCROLL_OPTIONS, static_cast(SQL_SO_FORWARD_ONLY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TABLE_TERM) { + this->connect(); + + validate(conn, SQL_TABLE_TERM, L"table"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_CAPABLE) { + this->connect(); + + validate(conn, SQL_TXN_CAPABLE, static_cast(SQL_TC_NONE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_ISOLATION_OPTION) { + this->connect(); + + validate(conn, SQL_TXN_ISOLATION_OPTION, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_USER_NAME) { + this->connect(); + + validate(conn, SQL_USER_NAME, L""); + + this->disconnect(); +} + +// Supported SQL + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_AGGREGATE_FUNCTIONS) { + this->connect(); + + validate( + conn, SQL_AGGREGATE_FUNCTIONS, + static_cast(SQL_AF_ALL | SQL_AF_AVG | SQL_AF_COUNT | SQL_AF_DISTINCT | + SQL_AF_MAX | SQL_AF_MIN | SQL_AF_SUM)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_DOMAIN) { + this->connect(); + + validate(conn, SQL_ALTER_DOMAIN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_SCHEMA) { + // Type commented out in odbc_connection.cc + GTEST_SKIP(); + this->connect(); + + // Type does not exist in sql.h + // validate(conn, SQL_ALTER_SCHEMA, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_TABLE) { + this->connect(); + + validate(conn, SQL_ALTER_TABLE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ANSI_SQL_DATETIME_LITERALS) { + // Type commented out in odbc_connection.cc + GTEST_SKIP(); + this->connect(); + + // Type does not exist in sql.h + // validate(conn, SQL_ANSI_SQL_DATETIME_LITERALS, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_LOCATION) { + this->connect(); + + validate(conn, SQL_CATALOG_LOCATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME) { + this->connect(); + + validate(conn, SQL_CATALOG_NAME, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME_SEPARATOR) { + this->connect(); + + validate(conn, SQL_CATALOG_NAME_SEPARATOR, L""); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CATALOG_USAGE) { + this->connect(); + + validate(conn, SQL_CATALOG_USAGE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLUMN_ALIAS) { + this->connect(); + + validate(conn, SQL_COLUMN_ALIAS, L"Y"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CORRELATION_NAME) { + this->connect(); + + validate(conn, SQL_CORRELATION_NAME, static_cast(SQL_CN_NONE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_ASSERTION) { + this->connect(); + + validate(conn, SQL_CREATE_ASSERTION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_CHARACTER_SET) { + this->connect(); + + validate(conn, SQL_CREATE_CHARACTER_SET, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_COLLATION) { + this->connect(); + + validate(conn, SQL_CREATE_COLLATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_DOMAIN) { + this->connect(); + + validate(conn, SQL_CREATE_DOMAIN, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_SCHEMA) { + this->connect(); + + validate(conn, SQL_CREATE_SCHEMA, static_cast(1)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_TABLE) { + this->connect(); + + validate(conn, SQL_CREATE_TABLE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_TRANSLATION) { + this->connect(); + + validate(conn, SQL_CREATE_TRANSLATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DDL_INDEX) { + this->connect(); + + validate(conn, SQL_DDL_INDEX, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_ASSERTION) { + this->connect(); + + validate(conn, SQL_DROP_ASSERTION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_CHARACTER_SET) { + this->connect(); + + validate(conn, SQL_DROP_CHARACTER_SET, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_COLLATION) { + this->connect(); + + validate(conn, SQL_DROP_COLLATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_DOMAIN) { + this->connect(); + + validate(conn, SQL_DROP_DOMAIN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_SCHEMA) { + this->connect(); + + validate(conn, SQL_DROP_SCHEMA, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TABLE) { + this->connect(); + + validate(conn, SQL_DROP_TABLE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TRANSLATION) { + this->connect(); + + validate(conn, SQL_DROP_TRANSLATION, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_VIEW) { + this->connect(); + + validate(conn, SQL_DROP_VIEW, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_EXPRESSIONS_IN_ORDERBY) { + this->connect(); + + validate(conn, SQL_EXPRESSIONS_IN_ORDERBY, L"N"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_GROUP_BY) { + this->connect(); + + validate(conn, SQL_GROUP_BY, + static_cast(SQL_GB_GROUP_BY_CONTAINS_SELECT)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_CASE) { + this->connect(); + + validate(conn, SQL_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_QUOTE_CHAR) { + this->connect(); + + validate(conn, SQL_IDENTIFIER_QUOTE_CHAR, L"\""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INDEX_KEYWORDS) { + this->connect(); + + validate(conn, SQL_INDEX_KEYWORDS, static_cast(SQL_IK_NONE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INSERT_STATEMENT) { + this->connect(); + + validate(conn, SQL_INSERT_STATEMENT, + static_cast(SQL_IS_INSERT_LITERALS | SQL_IS_INSERT_SEARCHED | + SQL_IS_SELECT_INTO)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INTEGRITY) { + this->connect(); + + validate(conn, SQL_INTEGRITY, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYWORDS) { + this->connect(); + + validateNotEmptySQLWCHAR(conn, SQL_KEYWORDS, true); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_LIKE_ESCAPE_CLAUSE) { + this->connect(); + + validate(conn, SQL_LIKE_ESCAPE_CLAUSE, L"Y"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NON_NULLABLE_COLUMNS) { + this->connect(); + + validate(conn, SQL_NON_NULLABLE_COLUMNS, static_cast(SQL_NNC_NULL)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OJ_CAPABILITIES) { + this->connect(); + + validate(this->conn, SQL_OJ_CAPABILITIES, + static_cast(SQL_OJ_LEFT | SQL_OJ_RIGHT | SQL_OJ_FULL)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_ORDER_BY_COLUMNS_IN_SELECT) { + this->connect(); + + validate(conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, L"Y"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OUTER_JOINS) { + this->connect(); + + validate(conn, SQL_OUTER_JOINS, L"N"); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PROCEDURES) { + this->connect(); + + validate(conn, SQL_PROCEDURES, L"N"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_QUOTED_IDENTIFIER_CASE) { + this->connect(); + + validate(conn, SQL_QUOTED_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SCHEMA_USAGE) { + this->connect(); + + validate(conn, SQL_SCHEMA_USAGE, static_cast(SQL_SU_DML_STATEMENTS)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SPECIAL_CHARACTERS) { + this->connect(); + + validate(conn, SQL_SPECIAL_CHARACTERS, L""); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SQL_CONFORMANCE) { + this->connect(); + + validate(conn, SQL_SQL_CONFORMANCE, static_cast(SQL_SC_SQL92_ENTRY)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SUBQUERIES) { + this->connect(); + + validate(conn, SQL_SUBQUERIES, + static_cast(SQL_SQ_CORRELATED_SUBQUERIES | SQL_SQ_COMPARISON | + SQL_SQ_EXISTS | SQL_SQ_IN | SQL_SQ_QUANTIFIED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_UNION) { + this->connect(); + + validate(conn, SQL_UNION, static_cast(SQL_U_UNION | SQL_U_UNION_ALL)); + + this->disconnect(); +} + +// SQL Limits + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_BINARY_LITERAL_LEN) { + this->connect(); + + validate(conn, SQL_MAX_BINARY_LITERAL_LEN, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CATALOG_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_CATALOG_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CHAR_LITERAL_LEN) { + this->connect(); + + validate(conn, SQL_MAX_CHAR_LITERAL_LEN, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_COLUMN_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_COLUMN_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_GROUP_BY) { + this->connect(); + + validate(conn, SQL_MAX_COLUMNS_IN_GROUP_BY, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_INDEX) { + this->connect(); + + validate(conn, SQL_MAX_COLUMNS_IN_INDEX, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_ORDER_BY) { + this->connect(); + + validate(conn, SQL_MAX_COLUMNS_IN_ORDER_BY, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_SELECT) { + this->connect(); + + validate(conn, SQL_MAX_COLUMNS_IN_SELECT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_TABLE) { + this->connect(); + + validate(conn, SQL_MAX_COLUMNS_IN_TABLE, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CURSOR_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_CURSOR_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_IDENTIFIER_LEN) { + this->connect(); + + validate(conn, SQL_MAX_IDENTIFIER_LEN, static_cast(65535)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_INDEX_SIZE) { + this->connect(); + + validate(conn, SQL_MAX_INDEX_SIZE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_PROCEDURE_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_PROCEDURE_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ROW_SIZE) { + this->connect(); + + validate(conn, SQL_MAX_ROW_SIZE, L""); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_ROW_SIZE_INCLUDES_LONG) { + this->connect(); + + validate(conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, L"N"); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_SCHEMA_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_SCHEMA_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_STATEMENT_LEN) { + this->connect(); + + validate(conn, SQL_MAX_STATEMENT_LEN, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_TABLE_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_TABLE_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_TABLES_IN_SELECT) { + this->connect(); + + validate(conn, SQL_MAX_TABLES_IN_SELECT, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_USER_NAME_LEN) { + this->connect(); + + validate(conn, SQL_MAX_USER_NAME_LEN, static_cast(0)); + + this->disconnect(); +} + +// Scalar Function Information + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FUNCTIONS) { + this->connect(); + + validate(conn, SQL_CONVERT_FUNCTIONS, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NUMERIC_FUNCTIONS) { + this->connect(); + + validate(conn, SQL_NUMERIC_FUNCTIONS, static_cast(4058942)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_STRING_FUNCTIONS) { + this->connect(); + + validate(conn, SQL_STRING_FUNCTIONS, + static_cast(SQL_FN_STR_LTRIM | SQL_FN_STR_LENGTH | + SQL_FN_STR_REPLACE | SQL_FN_STR_RTRIM)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SYSTEM_FUNCTIONS) { + this->connect(); + + validate(conn, SQL_SYSTEM_FUNCTIONS, + static_cast(SQL_FN_SYS_IFNULL | SQL_FN_SYS_USERNAME)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_ADD_INTERVALS) { + this->connect(); + + validate(conn, SQL_TIMEDATE_ADD_INTERVALS, + static_cast(SQL_FN_TSI_FRAC_SECOND | SQL_FN_TSI_SECOND | + SQL_FN_TSI_MINUTE | SQL_FN_TSI_HOUR | SQL_FN_TSI_DAY | + SQL_FN_TSI_WEEK | SQL_FN_TSI_MONTH | + SQL_FN_TSI_QUARTER | SQL_FN_TSI_YEAR)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_DIFF_INTERVALS) { + this->connect(); + + validate(conn, SQL_TIMEDATE_DIFF_INTERVALS, + static_cast(SQL_FN_TSI_FRAC_SECOND | SQL_FN_TSI_SECOND | + SQL_FN_TSI_MINUTE | SQL_FN_TSI_HOUR | SQL_FN_TSI_DAY | + SQL_FN_TSI_WEEK | SQL_FN_TSI_MONTH | + SQL_FN_TSI_QUARTER | SQL_FN_TSI_YEAR)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_TIMEDATE_FUNCTIONS) { + this->connect(); + + validate(conn, SQL_TIMEDATE_FUNCTIONS, + static_cast( + SQL_FN_TD_CURRENT_DATE | SQL_FN_TD_CURRENT_TIME | + SQL_FN_TD_CURRENT_TIMESTAMP | SQL_FN_TD_CURDATE | SQL_FN_TD_CURTIME | + SQL_FN_TD_DAYNAME | SQL_FN_TD_DAYOFMONTH | SQL_FN_TD_DAYOFWEEK | + SQL_FN_TD_DAYOFYEAR | SQL_FN_TD_EXTRACT | SQL_FN_TD_HOUR | + SQL_FN_TD_MINUTE | SQL_FN_TD_MONTH | SQL_FN_TD_MONTHNAME | SQL_FN_TD_NOW | + SQL_FN_TD_QUARTER | SQL_FN_TD_SECOND | SQL_FN_TD_TIMESTAMPADD | + SQL_FN_TD_TIMESTAMPDIFF | SQL_FN_TD_WEEK | SQL_FN_TD_YEAR)); + + this->disconnect(); +} + +// Conversion Information + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BIGINT) { + this->connect(); + + validate(conn, SQL_CONVERT_BIGINT, static_cast(8)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BINARY) { + this->connect(); + + validate(conn, SQL_CONVERT_BINARY, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_BIT) { + this->connect(); + + validate(conn, SQL_CONVERT_BIT, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_CHAR) { + this->connect(); + + validate(conn, SQL_CONVERT_CHAR, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DATE) { + this->connect(); + + validate(conn, SQL_CONVERT_DATE, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DECIMAL) { + this->connect(); + + validate(conn, SQL_CONVERT_DECIMAL, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_DOUBLE) { + this->connect(); + + validate(conn, SQL_CONVERT_DOUBLE, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FLOAT) { + this->connect(); + + validate(conn, SQL_CONVERT_FLOAT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTEGER) { + this->connect(); + + validate(conn, SQL_CONVERT_INTEGER, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_INTERVAL_DAY_TIME) { + this->connect(); + + validate(conn, SQL_CONVERT_INTERVAL_DAY_TIME, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTERVAL_YEAR_MONTH) { + this->connect(); + + validate(conn, SQL_CONVERT_INTERVAL_YEAR_MONTH, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARBINARY) { + this->connect(); + + validate(conn, SQL_CONVERT_LONGVARBINARY, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARCHAR) { + this->connect(); + + validate(conn, SQL_CONVERT_LONGVARCHAR, static_cast(0)); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_NUMERIC) { + this->connect(); + + validate(conn, SQL_CONVERT_NUMERIC, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_REAL) { + this->connect(); + + validate(conn, SQL_CONVERT_REAL, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_SMALLINT) { + this->connect(); + + validate(conn, SQL_CONVERT_SMALLINT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIME) { + this->connect(); + + validate(conn, SQL_CONVERT_TIME, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIMESTAMP) { + this->connect(); + + validate(conn, SQL_CONVERT_TIMESTAMP, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TINYINT) { + this->connect(); + + validate(conn, SQL_CONVERT_TINYINT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARBINARY) { + this->connect(); + + validate(conn, SQL_CONVERT_VARBINARY, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARCHAR) { + this->connect(); + + validate(conn, SQL_CONVERT_VARCHAR, static_cast(0)); + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc From 80a4274077959eb92f2ac8731047d69b11bf32e0 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 3 Jul 2025 15:29:33 -0700 Subject: [PATCH 24/74] SQLGetInfo Workflow Fixes --- .../odbc_impl/odbc_connection.cc | 19 +- .../sql/odbc/tests/connection_info_test.cc | 238 +++++++++--------- 2 files changed, 131 insertions(+), 126 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 1f3d365397a..15d60ccf1ac 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -169,8 +169,8 @@ SQLRETURN ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, return GetStringAttribute(isUnicode, m_dsn, true, value, bufferLength, outputLength, GetDiagnostics()); case SQL_DRIVER_ODBC_VER: - return GetStringAttribute(isUnicode, "03.80", true, value, bufferLength, outputLength, - GetDiagnostics()); + return GetStringAttribute(isUnicode, "03.80", true, value, bufferLength, + outputLength, GetDiagnostics()); case SQL_DYNAMIC_CURSOR_ATTRIBUTES1: GetAttribute(static_cast(0), value, bufferLength, outputLength); return SQL_SUCCESS; @@ -248,8 +248,8 @@ SQLRETURN ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, GetAttribute(static_cast(0), value, bufferLength, outputLength); return SQL_SUCCESS; case SQL_TABLE_TERM: - return GetStringAttribute(isUnicode, "table", true, value, bufferLength, outputLength, - GetDiagnostics()); + return GetStringAttribute(isUnicode, "table", true, value, bufferLength, + outputLength, GetDiagnostics()); // Deprecated ODBC 2.x fields required for backwards compatibility. case SQL_ODBC_API_CONFORMANCE: GetAttribute(static_cast(SQL_OAC_LEVEL1), value, bufferLength, @@ -308,8 +308,8 @@ SQLRETURN ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, case SQL_XOPEN_CLI_YEAR: { const auto& info = m_spiConnection->GetInfo(infoType); const std::string& infoValue = boost::get(info); - return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, - GetDiagnostics()); + return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, + outputLength, GetDiagnostics()); } // Driver-level 32-bit integer properties. @@ -444,11 +444,12 @@ SQLRETURN ODBCConnection::GetInfo(SQLUSMALLINT infoType, SQLPOINTER value, throw DriverException("Optional feature not supported.", "HYC00"); } const std::string& infoValue = boost::get(*attr); - return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, outputLength, - GetDiagnostics()); + return GetStringAttribute(isUnicode, infoValue, true, value, bufferLength, + outputLength, GetDiagnostics()); } default: - throw DriverException("Unknown SQLGetInfo type: " + std::to_string(infoType), "HY096"); + throw DriverException("Unknown SQLGetInfo type: " + std::to_string(infoType), + "HY096"); } return SQL_ERROR; diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc index b8734a8f8b2..1585e5ad75e 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc @@ -495,7 +495,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_VER) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_PROCEDURES) { this->connect(); - validate(conn, SQL_ACCESSIBLE_PROCEDURES, L"N"); + validate(this->conn, SQL_ACCESSIBLE_PROCEDURES, L"N"); this->disconnect(); } @@ -503,7 +503,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_PROCEDURES) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_TABLES) { this->connect(); - validate(conn, SQL_ACCESSIBLE_TABLES, L"Y"); + validate(this->conn, SQL_ACCESSIBLE_TABLES, L"Y"); this->disconnect(); } @@ -511,7 +511,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_TABLES) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BOOKMARK_PERSISTENCE) { this->connect(); - validate(conn, SQL_BOOKMARK_PERSISTENCE, static_cast(0)); + validate(this->conn, SQL_BOOKMARK_PERSISTENCE, static_cast(0)); this->disconnect(); } @@ -519,7 +519,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BOOKMARK_PERSISTENCE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_TERM) { this->connect(); - validate(conn, SQL_CATALOG_TERM, L""); + validate(this->conn, SQL_CATALOG_TERM, L""); this->disconnect(); } @@ -527,7 +527,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_TERM) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLLATION_SEQ) { this->connect(); - validate(conn, SQL_COLLATION_SEQ, L""); + validate(this->conn, SQL_COLLATION_SEQ, L""); this->disconnect(); } @@ -535,7 +535,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLLATION_SEQ) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONCAT_NULL_BEHAVIOR) { this->connect(); - validate(conn, SQL_CONCAT_NULL_BEHAVIOR, static_cast(SQL_CB_NULL)); + validate(this->conn, SQL_CONCAT_NULL_BEHAVIOR, static_cast(SQL_CB_NULL)); this->disconnect(); } @@ -543,7 +543,8 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONCAT_NULL_BEHAVIOR) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_COMMIT_BEHAVIOR) { this->connect(); - validate(conn, SQL_CURSOR_COMMIT_BEHAVIOR, static_cast(SQL_CB_CLOSE)); + validate(this->conn, SQL_CURSOR_COMMIT_BEHAVIOR, + static_cast(SQL_CB_CLOSE)); this->disconnect(); } @@ -551,7 +552,8 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_COMMIT_BEHAVIOR) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_ROLLBACK_BEHAVIOR) { this->connect(); - validate(conn, SQL_CURSOR_ROLLBACK_BEHAVIOR, static_cast(SQL_CB_CLOSE)); + validate(this->conn, SQL_CURSOR_ROLLBACK_BEHAVIOR, + static_cast(SQL_CB_CLOSE)); this->disconnect(); } @@ -559,7 +561,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_ROLLBACK_BEHAVIOR) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_SENSITIVITY) { this->connect(); - validate(conn, SQL_CURSOR_SENSITIVITY, static_cast(SQL_UNSPECIFIED)); + validate(this->conn, SQL_CURSOR_SENSITIVITY, static_cast(SQL_UNSPECIFIED)); this->disconnect(); } @@ -567,7 +569,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_SENSITIVITY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_READ_ONLY) { this->connect(); - validate(conn, SQL_DATA_SOURCE_READ_ONLY, L"N"); + validate(this->conn, SQL_DATA_SOURCE_READ_ONLY, L"N"); this->disconnect(); } @@ -575,7 +577,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_READ_ONLY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DEFAULT_TXN_ISOLATION) { this->connect(); - validate(conn, SQL_DEFAULT_TXN_ISOLATION, static_cast(0)); + validate(this->conn, SQL_DEFAULT_TXN_ISOLATION, static_cast(0)); this->disconnect(); } @@ -583,7 +585,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DEFAULT_TXN_ISOLATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DESCRIBE_PARAMETER) { this->connect(); - validate(conn, SQL_DESCRIBE_PARAMETER, L"N"); + validate(this->conn, SQL_DESCRIBE_PARAMETER, L"N"); this->disconnect(); } @@ -591,7 +593,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DESCRIBE_PARAMETER) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULT_RESULT_SETS) { this->connect(); - validate(conn, SQL_MULT_RESULT_SETS, L"N"); + validate(this->conn, SQL_MULT_RESULT_SETS, L"N"); this->disconnect(); } @@ -599,7 +601,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULT_RESULT_SETS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULTIPLE_ACTIVE_TXN) { this->connect(); - validate(conn, SQL_MULTIPLE_ACTIVE_TXN, L"N"); + validate(this->conn, SQL_MULTIPLE_ACTIVE_TXN, L"N"); this->disconnect(); } @@ -607,7 +609,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULTIPLE_ACTIVE_TXN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_NEED_LONG_DATA_LEN) { this->connect(); - validate(conn, SQL_NEED_LONG_DATA_LEN, L"N"); + validate(this->conn, SQL_NEED_LONG_DATA_LEN, L"N"); this->disconnect(); } @@ -615,7 +617,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_NEED_LONG_DATA_LEN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NULL_COLLATION) { this->connect(); - validate(conn, SQL_NULL_COLLATION, static_cast(SQL_NC_START)); + validate(this->conn, SQL_NULL_COLLATION, static_cast(SQL_NC_START)); this->disconnect(); } @@ -623,7 +625,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NULL_COLLATION) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_PROCEDURE_TERM) { this->connect(); - validate(conn, SQL_PROCEDURE_TERM, L""); + validate(this->conn, SQL_PROCEDURE_TERM, L""); this->disconnect(); } @@ -631,7 +633,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_PROCEDURE_TERM) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCHEMA_TERM) { this->connect(); - validate(conn, SQL_SCHEMA_TERM, L"schema"); + validate(this->conn, SQL_SCHEMA_TERM, L"schema"); this->disconnect(); } @@ -639,7 +641,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCHEMA_TERM) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCROLL_OPTIONS) { this->connect(); - validate(conn, SQL_SCROLL_OPTIONS, static_cast(SQL_SO_FORWARD_ONLY)); + validate(this->conn, SQL_SCROLL_OPTIONS, static_cast(SQL_SO_FORWARD_ONLY)); this->disconnect(); } @@ -647,7 +649,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCROLL_OPTIONS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TABLE_TERM) { this->connect(); - validate(conn, SQL_TABLE_TERM, L"table"); + validate(this->conn, SQL_TABLE_TERM, L"table"); this->disconnect(); } @@ -655,7 +657,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TABLE_TERM) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_CAPABLE) { this->connect(); - validate(conn, SQL_TXN_CAPABLE, static_cast(SQL_TC_NONE)); + validate(this->conn, SQL_TXN_CAPABLE, static_cast(SQL_TC_NONE)); this->disconnect(); } @@ -663,7 +665,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_CAPABLE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_ISOLATION_OPTION) { this->connect(); - validate(conn, SQL_TXN_ISOLATION_OPTION, static_cast(0)); + validate(this->conn, SQL_TXN_ISOLATION_OPTION, static_cast(0)); this->disconnect(); } @@ -671,7 +673,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_ISOLATION_OPTION) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_USER_NAME) { this->connect(); - validate(conn, SQL_USER_NAME, L""); + validate(this->conn, SQL_USER_NAME, L""); this->disconnect(); } @@ -682,7 +684,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_AGGREGATE_FUNCTIONS) { this->connect(); validate( - conn, SQL_AGGREGATE_FUNCTIONS, + this->conn, SQL_AGGREGATE_FUNCTIONS, static_cast(SQL_AF_ALL | SQL_AF_AVG | SQL_AF_COUNT | SQL_AF_DISTINCT | SQL_AF_MAX | SQL_AF_MIN | SQL_AF_SUM)); @@ -692,7 +694,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_AGGREGATE_FUNCTIONS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_DOMAIN) { this->connect(); - validate(conn, SQL_ALTER_DOMAIN, static_cast(0)); + validate(this->conn, SQL_ALTER_DOMAIN, static_cast(0)); this->disconnect(); } @@ -703,7 +705,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_SCHEMA) { this->connect(); // Type does not exist in sql.h - // validate(conn, SQL_ALTER_SCHEMA, static_cast(0)); + // validate(this->conn, SQL_ALTER_SCHEMA, static_cast(0)); this->disconnect(); } @@ -711,7 +713,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_SCHEMA) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_TABLE) { this->connect(); - validate(conn, SQL_ALTER_TABLE, static_cast(0)); + validate(this->conn, SQL_ALTER_TABLE, static_cast(0)); this->disconnect(); } @@ -722,7 +724,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ANSI_SQL_DATETIME_LITERALS) { this->connect(); // Type does not exist in sql.h - // validate(conn, SQL_ANSI_SQL_DATETIME_LITERALS, L""); + // validate(this->conn, SQL_ANSI_SQL_DATETIME_LITERALS, L""); this->disconnect(); } @@ -730,7 +732,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ANSI_SQL_DATETIME_LITERALS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_LOCATION) { this->connect(); - validate(conn, SQL_CATALOG_LOCATION, static_cast(0)); + validate(this->conn, SQL_CATALOG_LOCATION, static_cast(0)); this->disconnect(); } @@ -738,7 +740,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_LOCATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME) { this->connect(); - validate(conn, SQL_CATALOG_NAME, L"N"); + validate(this->conn, SQL_CATALOG_NAME, L"N"); this->disconnect(); } @@ -746,7 +748,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME_SEPARATOR) { this->connect(); - validate(conn, SQL_CATALOG_NAME_SEPARATOR, L""); + validate(this->conn, SQL_CATALOG_NAME_SEPARATOR, L""); this->disconnect(); } @@ -754,7 +756,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME_SEPARATOR) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CATALOG_USAGE) { this->connect(); - validate(conn, SQL_CATALOG_USAGE, static_cast(0)); + validate(this->conn, SQL_CATALOG_USAGE, static_cast(0)); this->disconnect(); } @@ -762,7 +764,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CATALOG_USAGE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLUMN_ALIAS) { this->connect(); - validate(conn, SQL_COLUMN_ALIAS, L"Y"); + validate(this->conn, SQL_COLUMN_ALIAS, L"Y"); this->disconnect(); } @@ -770,7 +772,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLUMN_ALIAS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CORRELATION_NAME) { this->connect(); - validate(conn, SQL_CORRELATION_NAME, static_cast(SQL_CN_NONE)); + validate(this->conn, SQL_CORRELATION_NAME, static_cast(SQL_CN_NONE)); this->disconnect(); } @@ -778,7 +780,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CORRELATION_NAME) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_ASSERTION) { this->connect(); - validate(conn, SQL_CREATE_ASSERTION, static_cast(0)); + validate(this->conn, SQL_CREATE_ASSERTION, static_cast(0)); this->disconnect(); } @@ -786,7 +788,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_ASSERTION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_CHARACTER_SET) { this->connect(); - validate(conn, SQL_CREATE_CHARACTER_SET, static_cast(0)); + validate(this->conn, SQL_CREATE_CHARACTER_SET, static_cast(0)); this->disconnect(); } @@ -794,7 +796,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_CHARACTER_SET) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_COLLATION) { this->connect(); - validate(conn, SQL_CREATE_COLLATION, static_cast(0)); + validate(this->conn, SQL_CREATE_COLLATION, static_cast(0)); this->disconnect(); } @@ -802,7 +804,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_COLLATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_DOMAIN) { this->connect(); - validate(conn, SQL_CREATE_DOMAIN, static_cast(0)); + validate(this->conn, SQL_CREATE_DOMAIN, static_cast(0)); this->disconnect(); } @@ -810,7 +812,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_DOMAIN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_SCHEMA) { this->connect(); - validate(conn, SQL_CREATE_SCHEMA, static_cast(1)); + validate(this->conn, SQL_CREATE_SCHEMA, static_cast(1)); this->disconnect(); } @@ -818,7 +820,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_SCHEMA) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_TABLE) { this->connect(); - validate(conn, SQL_CREATE_TABLE, static_cast(1)); + validate(this->conn, SQL_CREATE_TABLE, static_cast(1)); this->disconnect(); } @@ -826,7 +828,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_TABLE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_TRANSLATION) { this->connect(); - validate(conn, SQL_CREATE_TRANSLATION, static_cast(0)); + validate(this->conn, SQL_CREATE_TRANSLATION, static_cast(0)); this->disconnect(); } @@ -834,7 +836,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_TRANSLATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DDL_INDEX) { this->connect(); - validate(conn, SQL_DDL_INDEX, static_cast(0)); + validate(this->conn, SQL_DDL_INDEX, static_cast(0)); this->disconnect(); } @@ -842,7 +844,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DDL_INDEX) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_ASSERTION) { this->connect(); - validate(conn, SQL_DROP_ASSERTION, static_cast(0)); + validate(this->conn, SQL_DROP_ASSERTION, static_cast(0)); this->disconnect(); } @@ -850,7 +852,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_ASSERTION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_CHARACTER_SET) { this->connect(); - validate(conn, SQL_DROP_CHARACTER_SET, static_cast(0)); + validate(this->conn, SQL_DROP_CHARACTER_SET, static_cast(0)); this->disconnect(); } @@ -858,7 +860,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_CHARACTER_SET) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_COLLATION) { this->connect(); - validate(conn, SQL_DROP_COLLATION, static_cast(0)); + validate(this->conn, SQL_DROP_COLLATION, static_cast(0)); this->disconnect(); } @@ -866,7 +868,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_COLLATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_DOMAIN) { this->connect(); - validate(conn, SQL_DROP_DOMAIN, static_cast(0)); + validate(this->conn, SQL_DROP_DOMAIN, static_cast(0)); this->disconnect(); } @@ -874,7 +876,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_DOMAIN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_SCHEMA) { this->connect(); - validate(conn, SQL_DROP_SCHEMA, static_cast(0)); + validate(this->conn, SQL_DROP_SCHEMA, static_cast(0)); this->disconnect(); } @@ -882,7 +884,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_SCHEMA) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TABLE) { this->connect(); - validate(conn, SQL_DROP_TABLE, static_cast(0)); + validate(this->conn, SQL_DROP_TABLE, static_cast(0)); this->disconnect(); } @@ -890,7 +892,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TABLE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TRANSLATION) { this->connect(); - validate(conn, SQL_DROP_TRANSLATION, static_cast(0)); + validate(this->conn, SQL_DROP_TRANSLATION, static_cast(0)); this->disconnect(); } @@ -898,7 +900,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TRANSLATION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_VIEW) { this->connect(); - validate(conn, SQL_DROP_VIEW, static_cast(0)); + validate(this->conn, SQL_DROP_VIEW, static_cast(0)); this->disconnect(); } @@ -906,7 +908,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_VIEW) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_EXPRESSIONS_IN_ORDERBY) { this->connect(); - validate(conn, SQL_EXPRESSIONS_IN_ORDERBY, L"N"); + validate(this->conn, SQL_EXPRESSIONS_IN_ORDERBY, L"N"); this->disconnect(); } @@ -914,7 +916,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_EXPRESSIONS_IN_ORDERBY) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_GROUP_BY) { this->connect(); - validate(conn, SQL_GROUP_BY, + validate(this->conn, SQL_GROUP_BY, static_cast(SQL_GB_GROUP_BY_CONTAINS_SELECT)); this->disconnect(); @@ -923,7 +925,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_GROUP_BY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_CASE) { this->connect(); - validate(conn, SQL_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); + validate(this->conn, SQL_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); this->disconnect(); } @@ -931,7 +933,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_CASE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_QUOTE_CHAR) { this->connect(); - validate(conn, SQL_IDENTIFIER_QUOTE_CHAR, L"\""); + validate(this->conn, SQL_IDENTIFIER_QUOTE_CHAR, L"\""); this->disconnect(); } @@ -939,7 +941,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_QUOTE_CHAR) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INDEX_KEYWORDS) { this->connect(); - validate(conn, SQL_INDEX_KEYWORDS, static_cast(SQL_IK_NONE)); + validate(this->conn, SQL_INDEX_KEYWORDS, static_cast(SQL_IK_NONE)); this->disconnect(); } @@ -947,7 +949,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INDEX_KEYWORDS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INSERT_STATEMENT) { this->connect(); - validate(conn, SQL_INSERT_STATEMENT, + validate(this->conn, SQL_INSERT_STATEMENT, static_cast(SQL_IS_INSERT_LITERALS | SQL_IS_INSERT_SEARCHED | SQL_IS_SELECT_INTO)); @@ -957,7 +959,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INSERT_STATEMENT) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INTEGRITY) { this->connect(); - validate(conn, SQL_INTEGRITY, L"N"); + validate(this->conn, SQL_INTEGRITY, L"N"); this->disconnect(); } @@ -965,7 +967,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INTEGRITY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYWORDS) { this->connect(); - validateNotEmptySQLWCHAR(conn, SQL_KEYWORDS, true); + validateNotEmptySQLWCHAR(this->conn, SQL_KEYWORDS, true); this->disconnect(); } @@ -973,7 +975,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYWORDS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_LIKE_ESCAPE_CLAUSE) { this->connect(); - validate(conn, SQL_LIKE_ESCAPE_CLAUSE, L"Y"); + validate(this->conn, SQL_LIKE_ESCAPE_CLAUSE, L"Y"); this->disconnect(); } @@ -981,7 +983,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_LIKE_ESCAPE_CLAUSE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NON_NULLABLE_COLUMNS) { this->connect(); - validate(conn, SQL_NON_NULLABLE_COLUMNS, static_cast(SQL_NNC_NULL)); + validate(this->conn, SQL_NON_NULLABLE_COLUMNS, static_cast(SQL_NNC_NULL)); this->disconnect(); } @@ -998,7 +1000,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OJ_CAPABILITIES) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_ORDER_BY_COLUMNS_IN_SELECT) { this->connect(); - validate(conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, L"Y"); + validate(this->conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, L"Y"); this->disconnect(); } @@ -1006,7 +1008,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_ORDER_BY_COLUMNS_IN_SELECT) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OUTER_JOINS) { this->connect(); - validate(conn, SQL_OUTER_JOINS, L"N"); + validate(this->conn, SQL_OUTER_JOINS, L"N"); this->disconnect(); } @@ -1014,7 +1016,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OUTER_JOINS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PROCEDURES) { this->connect(); - validate(conn, SQL_PROCEDURES, L"N"); + validate(this->conn, SQL_PROCEDURES, L"N"); this->disconnect(); } @@ -1022,7 +1024,8 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PROCEDURES) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_QUOTED_IDENTIFIER_CASE) { this->connect(); - validate(conn, SQL_QUOTED_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); + validate(this->conn, SQL_QUOTED_IDENTIFIER_CASE, + static_cast(SQL_IC_MIXED)); this->disconnect(); } @@ -1030,7 +1033,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_QUOTED_IDENTIFIER_CASE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SCHEMA_USAGE) { this->connect(); - validate(conn, SQL_SCHEMA_USAGE, static_cast(SQL_SU_DML_STATEMENTS)); + validate(this->conn, SQL_SCHEMA_USAGE, static_cast(SQL_SU_DML_STATEMENTS)); this->disconnect(); } @@ -1038,7 +1041,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SCHEMA_USAGE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SPECIAL_CHARACTERS) { this->connect(); - validate(conn, SQL_SPECIAL_CHARACTERS, L""); + validate(this->conn, SQL_SPECIAL_CHARACTERS, L""); this->disconnect(); } @@ -1046,7 +1049,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SPECIAL_CHARACTERS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SQL_CONFORMANCE) { this->connect(); - validate(conn, SQL_SQL_CONFORMANCE, static_cast(SQL_SC_SQL92_ENTRY)); + validate(this->conn, SQL_SQL_CONFORMANCE, static_cast(SQL_SC_SQL92_ENTRY)); this->disconnect(); } @@ -1054,7 +1057,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SQL_CONFORMANCE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SUBQUERIES) { this->connect(); - validate(conn, SQL_SUBQUERIES, + validate(this->conn, SQL_SUBQUERIES, static_cast(SQL_SQ_CORRELATED_SUBQUERIES | SQL_SQ_COMPARISON | SQL_SQ_EXISTS | SQL_SQ_IN | SQL_SQ_QUANTIFIED)); @@ -1064,7 +1067,8 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SUBQUERIES) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_UNION) { this->connect(); - validate(conn, SQL_UNION, static_cast(SQL_U_UNION | SQL_U_UNION_ALL)); + validate(this->conn, SQL_UNION, + static_cast(SQL_U_UNION | SQL_U_UNION_ALL)); this->disconnect(); } @@ -1074,7 +1078,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_UNION) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_BINARY_LITERAL_LEN) { this->connect(); - validate(conn, SQL_MAX_BINARY_LITERAL_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_BINARY_LITERAL_LEN, static_cast(0)); this->disconnect(); } @@ -1082,7 +1086,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_BINARY_LITERAL_LEN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CATALOG_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_CATALOG_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_CATALOG_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1090,7 +1094,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CATALOG_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CHAR_LITERAL_LEN) { this->connect(); - validate(conn, SQL_MAX_CHAR_LITERAL_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_CHAR_LITERAL_LEN, static_cast(0)); this->disconnect(); } @@ -1098,7 +1102,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CHAR_LITERAL_LEN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_COLUMN_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_COLUMN_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMN_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1106,7 +1110,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_COLUMN_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_GROUP_BY) { this->connect(); - validate(conn, SQL_MAX_COLUMNS_IN_GROUP_BY, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMNS_IN_GROUP_BY, static_cast(0)); this->disconnect(); } @@ -1114,7 +1118,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_GROUP_BY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_INDEX) { this->connect(); - validate(conn, SQL_MAX_COLUMNS_IN_INDEX, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMNS_IN_INDEX, static_cast(0)); this->disconnect(); } @@ -1122,7 +1126,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_INDEX) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_ORDER_BY) { this->connect(); - validate(conn, SQL_MAX_COLUMNS_IN_ORDER_BY, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMNS_IN_ORDER_BY, static_cast(0)); this->disconnect(); } @@ -1130,7 +1134,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_ORDER_BY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_SELECT) { this->connect(); - validate(conn, SQL_MAX_COLUMNS_IN_SELECT, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMNS_IN_SELECT, static_cast(0)); this->disconnect(); } @@ -1138,7 +1142,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_SELECT) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_TABLE) { this->connect(); - validate(conn, SQL_MAX_COLUMNS_IN_TABLE, static_cast(0)); + validate(this->conn, SQL_MAX_COLUMNS_IN_TABLE, static_cast(0)); this->disconnect(); } @@ -1146,7 +1150,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_TABLE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CURSOR_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_CURSOR_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_CURSOR_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1154,7 +1158,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CURSOR_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_IDENTIFIER_LEN) { this->connect(); - validate(conn, SQL_MAX_IDENTIFIER_LEN, static_cast(65535)); + validate(this->conn, SQL_MAX_IDENTIFIER_LEN, static_cast(65535)); this->disconnect(); } @@ -1162,7 +1166,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_IDENTIFIER_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_INDEX_SIZE) { this->connect(); - validate(conn, SQL_MAX_INDEX_SIZE, static_cast(0)); + validate(this->conn, SQL_MAX_INDEX_SIZE, static_cast(0)); this->disconnect(); } @@ -1170,7 +1174,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_INDEX_SIZE) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_PROCEDURE_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_PROCEDURE_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_PROCEDURE_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1178,7 +1182,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_PROCEDURE_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ROW_SIZE) { this->connect(); - validate(conn, SQL_MAX_ROW_SIZE, L""); + validate(this->conn, SQL_MAX_ROW_SIZE, L""); this->disconnect(); } @@ -1186,7 +1190,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ROW_SIZE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_ROW_SIZE_INCLUDES_LONG) { this->connect(); - validate(conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, L"N"); + validate(this->conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, L"N"); this->disconnect(); } @@ -1194,7 +1198,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_ROW_SIZE_INCLUDES_LONG) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_SCHEMA_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_SCHEMA_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_SCHEMA_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1202,7 +1206,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_SCHEMA_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_STATEMENT_LEN) { this->connect(); - validate(conn, SQL_MAX_STATEMENT_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_STATEMENT_LEN, static_cast(0)); this->disconnect(); } @@ -1210,7 +1214,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_STATEMENT_LEN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_TABLE_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_TABLE_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_TABLE_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1218,7 +1222,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_TABLE_NAME_LEN) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_TABLES_IN_SELECT) { this->connect(); - validate(conn, SQL_MAX_TABLES_IN_SELECT, static_cast(0)); + validate(this->conn, SQL_MAX_TABLES_IN_SELECT, static_cast(0)); this->disconnect(); } @@ -1226,7 +1230,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_TABLES_IN_SELECT) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_USER_NAME_LEN) { this->connect(); - validate(conn, SQL_MAX_USER_NAME_LEN, static_cast(0)); + validate(this->conn, SQL_MAX_USER_NAME_LEN, static_cast(0)); this->disconnect(); } @@ -1236,7 +1240,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_USER_NAME_LEN) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FUNCTIONS) { this->connect(); - validate(conn, SQL_CONVERT_FUNCTIONS, static_cast(0)); + validate(this->conn, SQL_CONVERT_FUNCTIONS, static_cast(0)); this->disconnect(); } @@ -1244,7 +1248,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FUNCTIONS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NUMERIC_FUNCTIONS) { this->connect(); - validate(conn, SQL_NUMERIC_FUNCTIONS, static_cast(4058942)); + validate(this->conn, SQL_NUMERIC_FUNCTIONS, static_cast(4058942)); this->disconnect(); } @@ -1252,7 +1256,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NUMERIC_FUNCTIONS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_STRING_FUNCTIONS) { this->connect(); - validate(conn, SQL_STRING_FUNCTIONS, + validate(this->conn, SQL_STRING_FUNCTIONS, static_cast(SQL_FN_STR_LTRIM | SQL_FN_STR_LENGTH | SQL_FN_STR_REPLACE | SQL_FN_STR_RTRIM)); @@ -1262,7 +1266,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_STRING_FUNCTIONS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SYSTEM_FUNCTIONS) { this->connect(); - validate(conn, SQL_SYSTEM_FUNCTIONS, + validate(this->conn, SQL_SYSTEM_FUNCTIONS, static_cast(SQL_FN_SYS_IFNULL | SQL_FN_SYS_USERNAME)); this->disconnect(); @@ -1271,7 +1275,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SYSTEM_FUNCTIONS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_ADD_INTERVALS) { this->connect(); - validate(conn, SQL_TIMEDATE_ADD_INTERVALS, + validate(this->conn, SQL_TIMEDATE_ADD_INTERVALS, static_cast(SQL_FN_TSI_FRAC_SECOND | SQL_FN_TSI_SECOND | SQL_FN_TSI_MINUTE | SQL_FN_TSI_HOUR | SQL_FN_TSI_DAY | SQL_FN_TSI_WEEK | SQL_FN_TSI_MONTH | @@ -1283,7 +1287,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_ADD_INTERVALS) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_DIFF_INTERVALS) { this->connect(); - validate(conn, SQL_TIMEDATE_DIFF_INTERVALS, + validate(this->conn, SQL_TIMEDATE_DIFF_INTERVALS, static_cast(SQL_FN_TSI_FRAC_SECOND | SQL_FN_TSI_SECOND | SQL_FN_TSI_MINUTE | SQL_FN_TSI_HOUR | SQL_FN_TSI_DAY | SQL_FN_TSI_WEEK | SQL_FN_TSI_MONTH | @@ -1295,7 +1299,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_DIFF_INTERVALS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_TIMEDATE_FUNCTIONS) { this->connect(); - validate(conn, SQL_TIMEDATE_FUNCTIONS, + validate(this->conn, SQL_TIMEDATE_FUNCTIONS, static_cast( SQL_FN_TD_CURRENT_DATE | SQL_FN_TD_CURRENT_TIME | SQL_FN_TD_CURRENT_TIMESTAMP | SQL_FN_TD_CURDATE | SQL_FN_TD_CURTIME | @@ -1313,7 +1317,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_TIMEDATE_FUNCTIONS) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BIGINT) { this->connect(); - validate(conn, SQL_CONVERT_BIGINT, static_cast(8)); + validate(this->conn, SQL_CONVERT_BIGINT, static_cast(8)); this->disconnect(); } @@ -1321,7 +1325,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BIGINT) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BINARY) { this->connect(); - validate(conn, SQL_CONVERT_BINARY, static_cast(0)); + validate(this->conn, SQL_CONVERT_BINARY, static_cast(0)); this->disconnect(); } @@ -1329,7 +1333,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BINARY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_BIT) { this->connect(); - validate(conn, SQL_CONVERT_BIT, static_cast(0)); + validate(this->conn, SQL_CONVERT_BIT, static_cast(0)); this->disconnect(); } @@ -1337,7 +1341,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_BIT) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_CHAR) { this->connect(); - validate(conn, SQL_CONVERT_CHAR, static_cast(0)); + validate(this->conn, SQL_CONVERT_CHAR, static_cast(0)); this->disconnect(); } @@ -1345,7 +1349,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_CHAR) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DATE) { this->connect(); - validate(conn, SQL_CONVERT_DATE, static_cast(0)); + validate(this->conn, SQL_CONVERT_DATE, static_cast(0)); this->disconnect(); } @@ -1353,7 +1357,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DATE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DECIMAL) { this->connect(); - validate(conn, SQL_CONVERT_DECIMAL, static_cast(0)); + validate(this->conn, SQL_CONVERT_DECIMAL, static_cast(0)); this->disconnect(); } @@ -1361,7 +1365,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DECIMAL) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_DOUBLE) { this->connect(); - validate(conn, SQL_CONVERT_DOUBLE, static_cast(0)); + validate(this->conn, SQL_CONVERT_DOUBLE, static_cast(0)); this->disconnect(); } @@ -1369,7 +1373,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_DOUBLE) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FLOAT) { this->connect(); - validate(conn, SQL_CONVERT_FLOAT, static_cast(0)); + validate(this->conn, SQL_CONVERT_FLOAT, static_cast(0)); this->disconnect(); } @@ -1377,7 +1381,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FLOAT) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTEGER) { this->connect(); - validate(conn, SQL_CONVERT_INTEGER, static_cast(0)); + validate(this->conn, SQL_CONVERT_INTEGER, static_cast(0)); this->disconnect(); } @@ -1385,7 +1389,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTEGER) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_INTERVAL_DAY_TIME) { this->connect(); - validate(conn, SQL_CONVERT_INTERVAL_DAY_TIME, static_cast(0)); + validate(this->conn, SQL_CONVERT_INTERVAL_DAY_TIME, static_cast(0)); this->disconnect(); } @@ -1393,7 +1397,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_INTERVAL_DAY_TIME) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTERVAL_YEAR_MONTH) { this->connect(); - validate(conn, SQL_CONVERT_INTERVAL_YEAR_MONTH, static_cast(0)); + validate(this->conn, SQL_CONVERT_INTERVAL_YEAR_MONTH, static_cast(0)); this->disconnect(); } @@ -1401,7 +1405,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTERVAL_YEAR_MONTH) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARBINARY) { this->connect(); - validate(conn, SQL_CONVERT_LONGVARBINARY, static_cast(0)); + validate(this->conn, SQL_CONVERT_LONGVARBINARY, static_cast(0)); this->disconnect(); } @@ -1409,7 +1413,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARBINARY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARCHAR) { this->connect(); - validate(conn, SQL_CONVERT_LONGVARCHAR, static_cast(0)); + validate(this->conn, SQL_CONVERT_LONGVARCHAR, static_cast(0)); this->disconnect(); } @@ -1417,7 +1421,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARCHAR) { TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_NUMERIC) { this->connect(); - validate(conn, SQL_CONVERT_NUMERIC, static_cast(0)); + validate(this->conn, SQL_CONVERT_NUMERIC, static_cast(0)); this->disconnect(); } @@ -1425,7 +1429,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_NUMERIC) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_REAL) { this->connect(); - validate(conn, SQL_CONVERT_REAL, static_cast(0)); + validate(this->conn, SQL_CONVERT_REAL, static_cast(0)); this->disconnect(); } @@ -1433,7 +1437,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_REAL) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_SMALLINT) { this->connect(); - validate(conn, SQL_CONVERT_SMALLINT, static_cast(0)); + validate(this->conn, SQL_CONVERT_SMALLINT, static_cast(0)); this->disconnect(); } @@ -1441,7 +1445,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_SMALLINT) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIME) { this->connect(); - validate(conn, SQL_CONVERT_TIME, static_cast(0)); + validate(this->conn, SQL_CONVERT_TIME, static_cast(0)); this->disconnect(); } @@ -1449,7 +1453,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIME) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIMESTAMP) { this->connect(); - validate(conn, SQL_CONVERT_TIMESTAMP, static_cast(0)); + validate(this->conn, SQL_CONVERT_TIMESTAMP, static_cast(0)); this->disconnect(); } @@ -1457,7 +1461,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIMESTAMP) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TINYINT) { this->connect(); - validate(conn, SQL_CONVERT_TINYINT, static_cast(0)); + validate(this->conn, SQL_CONVERT_TINYINT, static_cast(0)); this->disconnect(); } @@ -1465,7 +1469,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TINYINT) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARBINARY) { this->connect(); - validate(conn, SQL_CONVERT_VARBINARY, static_cast(0)); + validate(this->conn, SQL_CONVERT_VARBINARY, static_cast(0)); this->disconnect(); } @@ -1473,7 +1477,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARBINARY) { TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARCHAR) { this->connect(); - validate(conn, SQL_CONVERT_VARCHAR, static_cast(0)); + validate(this->conn, SQL_CONVERT_VARCHAR, static_cast(0)); this->disconnect(); } From 34c6eed02d99da027ce8190ace02419a446cd521 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:08:22 -0700 Subject: [PATCH 25/74] SQLFetch & SQLGetData Implementation SQLGetStmtAttr stub implementation Stub call for SQLGetStmtAttr Enable statement handle allocation Implement SQLExecDirect - test hang issue Update odbc_api.cc Run `ShutdownProtobufLibrary` after all ODBC tests Resolves test hanging problem Fix missing break statement in SQLGetDiagRec Add tests Lint fixes Run ShutdownProtobufLibrary as part of test environment Add debug messages Draft code Remove drafts Add comment Add comment for GH-46889 Update statement test headers Pass call options to executed prepared statement `call_options_` contains the authentication token which is needed SQLFetch & SQLGetData initial impl EVERYTHING before this commit is for SQLExecDirect SQLFetch and SQLGetData TestSQLExecDirectSimpleQuery test Make GetData() return SQLRETURN `TestSQLExecDirectDataQuery` for remote and mock servers Remove unneeded test case Add more data types * Add SQL_GUID in getCTypeForSQLType * Add data types in query * Add checks for columns 1 to 25 * Update statement_test.cc Add varbinary test * Implement SQLNumResultCols Will leave tests for later * Implement SQLRowCount * Implement SQLMoreResults * Work on date retrieval fix The `TestSQLExecDirectDataQuery` checks for date is not working. * Fix date retrieval error by importing fix from Paul * imported fix from https://github.com/dremio/flightsql-odbc/commit/d44d862bd07f05328f5fcfd68a8f40357aa072fa, an Apache-Licensed repository * Add more checks for get data * Add more tests * Continue work on float truncation test * Disable float truncation test * Address comments from James - do not return errors for invalid rowCountPtr and columnCountPtr - add static cast for columnCount - Add tests with SQL_C_DEFAULT as target type - Add test checks for indicator - Fix SQL_NUMERIC, SQL_BIT, SQL_FLOAT target type * Fix build issues in CI * More fix for CI * Add test for invalid buffer length --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 41 +- .../accessors/date_array_accessor_test.cc | 5 +- .../accessors/timestamp_array_accessor.cc | 9 +- .../timestamp_array_accessor_test.cc | 45 +- .../odbc/flight_sql/flight_sql_result_set.cc | 16 +- .../odbc/flight_sql/flight_sql_result_set.h | 4 +- cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 75 +- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 6 + .../odbc_impl/odbc_statement.h | 16 +- .../include/odbcabstraction/spi/result_set.h | 10 +- .../odbc_impl/odbc_statement.cc | 46 +- .../flight/sql/odbc/tests/odbc_test_suite.cc | 100 ++ .../flight/sql/odbc/tests/odbc_test_suite.h | 8 + .../flight/sql/odbc/tests/statement_test.cc | 1320 ++++++++++++++++- testing | 2 +- 16 files changed, 1650 insertions(+), 54 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 722a3975186..c7bbc08e49e 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -135,6 +135,15 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, return arrow::SQLExecDirect(stmt, queryText, textLength); } +SQLRETURN SQL_API SQLFetch(SQLHSTMT stmt) { return arrow::SQLFetch(stmt); } + +SQLRETURN SQL_API SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, + SQLLEN* indicatorPtr) { + return arrow::SQLGetData(stmt, recordNumber, cType, dataPtr, bufferLength, + indicatorPtr); +} + SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { @@ -204,11 +213,6 @@ SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { return SQL_ERROR; } -SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { - LOG_DEBUG("SQLFetch called with statementHandle: {}", statementHandle); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, @@ -232,27 +236,13 @@ SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogNa return SQL_ERROR; } -SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLUSMALLINT col_or_Param_Num, - SQLSMALLINT targetType, SQLPOINTER targetValuePtr, - SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { - LOG_DEBUG( - "SQLGetData called with statementHandle: {}, col_or_Param_Num: {}, targetType: {}, " - "targetValuePtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", - statementHandle, col_or_Param_Num, targetType, targetValuePtr, bufferLength, - fmt::ptr(strLen_or_IndPtr)); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { LOG_DEBUG("SQLGetTypeInfoW called with statementHandle: {} dataType: {}", statementHandle, dataType); return SQL_ERROR; } -SQLRETURN SQL_API SQLMoreResults(SQLHSTMT statementHandle) { - LOG_DEBUG("SQLMoreResults called with statementHandle: {}", statementHandle); - return SQL_ERROR; -} +SQLRETURN SQL_API SQLMoreResults(SQLHSTMT stmt) { return arrow::SQLMoreResults(stmt); } SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, SQLINTEGER inStatementTextLength, @@ -267,11 +257,12 @@ SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementTe return SQL_ERROR; } -SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, - SQLSMALLINT* columnCountPtr) { - LOG_DEBUG("SQLNumResultCols called with statementHandle: {}, columnCountPtr: {}", - statementHandle, fmt::ptr(columnCountPtr)); - return SQL_ERROR; +SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { + return arrow::SQLNumResultCols(stmt, columnCountPtr); +} + +SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { + return arrow::SQLRowCount(stmt, rowCountPtr); } SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, SQLWCHAR* statementText, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc index fdd48d2706a..e6c0044587e 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc @@ -32,7 +32,6 @@ using arrow::NumericArray; using odbcabstraction::DATE_STRUCT; using odbcabstraction::OdbcVersion; -using odbcabstraction::tagDATE_STRUCT; using arrow::ArrayFromVector; using odbcabstraction::GetTimeForSecondsSinceEpoch; @@ -50,7 +49,7 @@ TEST(DateArrayAccessor, Test_Date32Array_CDataType_DATE) { DateArrayFlightSqlAccessor accessor( dynamic_cast*>(array.get())); - std::vector buffer(values.size()); + std::vector buffer(values.size()); std::vector strlen_buffer(values.size()); ColumnBinding binding(odbcabstraction::CDataType_DATE, 0, 0, buffer.data(), 0, @@ -88,7 +87,7 @@ TEST(DateArrayAccessor, Test_Date64Array_CDataType_DATE) { DateArrayFlightSqlAccessor accessor( dynamic_cast*>(array.get())); - std::vector buffer(values.size()); + std::vector buffer(values.size()); std::vector strlen_buffer(values.size()); ColumnBinding binding(odbcabstraction::CDataType_DATE, 0, 0, buffer.data(), 0, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor.cc index 68e9f64fffb..c0225edea01 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor.cc @@ -18,10 +18,13 @@ #include "arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/calendar_utils.h" +#include +#include + using arrow::TimeUnit; namespace { -int64_t GetConversionToSecondsDivisor(TimeUnit::type unit) { +inline int64_t GetConversionToSecondsDivisor(TimeUnit::type unit) { int64_t divisor = 1; switch (unit) { case TimeUnit::SECOND: @@ -85,6 +88,10 @@ RowStatus TimestampArrayFlightSqlAccessor::MoveSingleCell_imp ColumnBinding* binding, int64_t arrow_row, int64_t cell_counter, int64_t& value_offset, bool update_value_offset, odbcabstraction::Diagnostics& diagnostics) { + // Times less than the minimum integer number of seconds that can be represented + // for each time unit will not convert correctly. This is mostly interesting for + // nanoseconds as timestamps in other units are outside of the accepted range of + // Gregorian dates. auto* buffer = static_cast(binding->buffer); int64_t value = this->GetArray()->Value(arrow_row); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc index 930cc6a5654..248711bd861 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc @@ -98,8 +98,15 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MILLI) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_SECONDS) { - std::vector values = {86400, 172800, 259200, 1649793238, - 345600, 432000, 518400}; + std::vector values = {86400, 172800, 259200, 1649793238, 345600, + 432000, 518400, -86399, 0}; + std::vector expected = { + /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), + fraction(u32) */ + {1970, 1, 2, 0, 0, 0, 0}, {1970, 1, 3, 0, 0, 0, 0}, {1970, 1, 4, 0, 0, 0, 0}, + {2022, 4, 12, 19, 53, 58, 0}, {1970, 1, 5, 0, 0, 0, 0}, {1970, 1, 6, 0, 0, 0, 0}, + {1970, 1, 7, 0, 0, 0, 0}, {1969, 12, 31, 0, 0, 1, 0}, {1970, 1, 1, 0, 0, 0, 0}, + }; std::shared_ptr timestamp_array; @@ -140,7 +147,15 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_SECONDS) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MICRO) { - std::vector values = {86400000000, 1649793238000000}; + std::vector values = {0, 86400000000, 1649793238000000, -86399999999, + -86399000001}; + std::vector expected = { + /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), + fraction(u32) */ + {1970, 1, 1, 0, 0, 0, 0}, {1970, 1, 2, 0, 0, 0, 0}, + {2022, 4, 12, 19, 53, 58, 0}, {1969, 12, 31, 0, 0, 0, 1000}, + {1969, 12, 31, 0, 0, 0, 999999000}, + }; std::shared_ptr timestamp_array; @@ -184,7 +199,28 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MICRO) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_NANO) { - std::vector values = {86400000010000, 1649793238000000000}; + std::vector values = {86400000010000, + 1649793238000000000, + -86399999999999, + -86399000000001, + 86400000000001, + 86400999999999, + 0, + -9223372036000000001}; + std::vector expected = { + /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), + fraction(u32) */ + {1970, 1, 2, 0, 0, 0, 10000}, + {2022, 4, 12, 19, 53, 58, 0}, + {1969, 12, 31, 0, 0, 0, 1}, + {1969, 12, 31, 0, 0, 0, 999999999}, + {1970, 1, 2, 0, 0, 0, 1}, + {1970, 1, 2, 0, 0, 0, 999999999}, + {1970, 1, 1, 0, 0, 0, 0}, + /* Test within range where floor (seconds) value is below INT64_MIN in nanoseconds + */ + {1677, 9, 21, 0, 12, 43, 999999999}, + }; std::shared_ptr timestamp_array; @@ -209,7 +245,6 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_NANO) { for (size_t i = 0; i < values.size(); ++i) { ASSERT_EQ(sizeof(TIMESTAMP_STRUCT), strlen_buffer[i]); - tm date{}; auto converted_time = values[i] / odbcabstraction::NANO_TO_SECONDS_DIVISOR; GetTimeForSecondsSinceEpoch(converted_time, date); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc index 824260a6868..6744537ae5b 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc @@ -18,6 +18,8 @@ #include "arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" +#include + #include #include "arrow/flight/types.h" #include "arrow/scalar.h" @@ -226,14 +228,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* strlen_buffer) { +SQLRETURN FlightSqlResultSet::GetData(int column_n, int16_t target_type, int precision, + int scale, void* buffer, size_t buffer_length, + ssize_t* strlen_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(ConvertCDataTypeFromV2ToV3(target_type), precision, scale, buffer, @@ -249,7 +251,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/flight_sql/flight_sql_result_set.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h index d1f20979a24..c82a2d83543 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h @@ -73,8 +73,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* strlen_buffer) override; + SQLRETURN GetData(int column_n, int16_t target_type, int precision, int scale, + void* buffer, size_t buffer_length, ssize_t* strlen_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.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index 925109a5a9f..d5767c5b5c4 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -47,6 +47,7 @@ EXPORTS SQLGetInfoW SQLGetStmtAttrW SQLGetTypeInfoW + SQLRowCount SQLMoreResults SQLNativeSqlW SQLNumResultCols diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 3faf56bf290..786d295d97c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -25,6 +25,7 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/encoding_utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_environment.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/connection.h" @@ -340,7 +341,7 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT case SQL_DIAG_ROW_COUNT: { if (handleType == SQL_HANDLE_STMT) { if (diagInfoPtr) { - // Will always be 0 if only select supported + // Will always be 0 if only SELECT is supported *static_cast(diagInfoPtr) = 0; } @@ -904,4 +905,76 @@ SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLengt return SQL_SUCCESS; }); } + +SQLRETURN SQLFetch(SQLHSTMT stmt) { + LOG_DEBUG("SQLFetch called with stmt: {}", stmt); + 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 SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { + // GH-46979: support SQL_C_GUID data type + // GH-46980: support Interval data types + // GH-46985: return warning message instead of error on float truncation case + LOG_DEBUG( + "SQLGetData called with stmt: {}, recordNumber: {}, cType: {}, " + "dataPtr: {}, bufferLength: {}, indicatorPtr: {}", + stmt, recordNumber, cType, dataPtr, bufferLength, fmt::ptr(indicatorPtr)); + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + return statement->GetData(recordNumber, cType, dataPtr, bufferLength, indicatorPtr); + }); +} + +SQLRETURN SQLMoreResults(SQLHSTMT stmt) { + LOG_DEBUG("SQLMoreResults called with stmt: {}", stmt); + // TODO: write tests for SQLMoreResults + using ODBC::ODBCStatement; + // Multiple result sets not supported. Return SQL_NO_DATA by default. + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + return statement->getMoreResults(); + }); +} + +SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { + LOG_DEBUG("SQLNumResultCols called with stmt: {}, columnCountPtr: {}", stmt, + fmt::ptr(columnCountPtr)); + // TODO: write tests for SQLNumResultCols + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + statement->getColumnCount(columnCountPtr); + return SQL_SUCCESS; + }); +} + +SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { + LOG_DEBUG("SQLRowCount called with stmt: {}, columnCountPtr: {}", stmt, + fmt::ptr(rowCountPtr)); + // TODO: write tests for SQLRowCount + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + statement->getRowCount(rowCountPtr); + return SQL_SUCCESS; + }); +} + } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index af8fc4056ff..c41a913090c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -63,4 +63,10 @@ SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePt SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); +SQLRETURN SQLFetch(SQLHSTMT stmt); +SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); +SQLRETURN SQLMoreResults(SQLHSTMT stmt); +SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr); +SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h index 29efaec8280..df5ca5e34ab 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h @@ -83,8 +83,20 @@ class ODBCStatement : public ODBCHandle { inline SQLULEN GetRowsetSize() { return m_rowsetSize; } - bool GetData(SQLSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, - SQLLEN bufferLength, SQLLEN* indicatorPtr); + SQLRETURN GetData(SQLSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, + SQLLEN bufferLength, SQLLEN* indicatorPtr); + + SQLRETURN getMoreResults(); + + /** + * @brief Get number of columns from data set + */ + void getColumnCount(SQLSMALLINT* columnCountPtr); + + /** + * @brief Get number of rows affected by an UPDATE, INSERT, or DELETE statement + */ + void getRowCount(SQLLEN* rowCountPtr); /** * @brief Closes the cursor. This does _not_ un-prepare the statement or change diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h index 4c12a4b5934..c24c6424860 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/result_set.h @@ -24,6 +24,8 @@ #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/types.h" +#include + namespace driver { namespace odbcabstraction { @@ -88,10 +90,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 odbcabstraction diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index e5ac4f41408..49ad527bab0 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -129,6 +129,9 @@ SQLSMALLINT getCTypeForSQLType(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 getCTypeForSQLType(const DescriptorRecord& record) { case SQL_BIGINT: return record.m_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; @@ -693,9 +703,9 @@ void ODBCStatement::closeCursor(bool suppressErrors) { m_hasReachedEndOfResult = false; } -bool ODBCStatement::GetData(SQLSMALLINT recordNumber, SQLSMALLINT cType, - SQLPOINTER dataPtr, SQLLEN bufferLength, - SQLLEN* indicatorPtr) { +SQLRETURN ODBCStatement::GetData(SQLSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, + SQLLEN* indicatorPtr) { if (recordNumber == 0) { throw DriverException("Bookmarks are not supported", "07009"); } else if (recordNumber > m_ird->GetRecords().size()) { @@ -737,6 +747,34 @@ bool ODBCStatement::GetData(SQLSMALLINT recordNumber, SQLSMALLINT cType, bufferLength, indicatorPtr); } +SQLRETURN ODBCStatement::getMoreResults() { + // Multiple result sets are not supported. + if (m_currenResult) { + return SQL_NO_DATA; + } else { + throw DriverException("Function sequence error", "HY010"); + } +} + +void ODBCStatement::getColumnCount(SQLSMALLINT* columnCountPtr) { + if (!columnCountPtr) { + // columnCountPtr is not valid, do nothing as ODBC spec does not mention this as an + // error + return; + } + size_t columnCount = m_currentArd->GetRecords().size(); + *columnCountPtr = static_cast(columnCount); +} + +void ODBCStatement::getRowCount(SQLLEN* rowCountPtr) { + if (!rowCountPtr) { + // rowCountPtr is not valid, do nothing as ODBC spec does not mention this as an error + return; + } + // Will always be -1 (number of rows unknown) if only SELECT is supported + *rowCountPtr = -1; +} + void ODBCStatement::releaseStatement() { closeCursor(true); m_connection.dropStatement(this); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index f20cde44136..a8da14c6ecf 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -112,6 +112,57 @@ std::string FlightSQLODBCRemoteTestBase::getInvalidConnectionString() { return connect_str; } +std::wstring FlightSQLODBCRemoteTestBase::getQueryAllDataTypes() { + std::wstring wsql = + LR"( SELECT + -- Numeric types + -128 as stiny_int_min, 127 as stiny_int_max, + 0 as utiny_int_min, 255 as utiny_int_max, + + -32768 as ssmall_int_min, 32767 as ssmall_int_max, + 0 as usmall_int_min, 65535 as usmall_int_max, + + CAST(-2147483648 AS INTEGER) AS sinteger_min, + CAST(2147483647 AS INTEGER) AS sinteger_max, + CAST(0 AS BIGINT) AS uinteger_min, + CAST(4294967295 AS BIGINT) AS uinteger_max, + + CAST(-9223372036854775808 AS BIGINT) AS sbigint_min, + CAST(9223372036854775807 AS BIGINT) AS sbigint_max, + CAST(0 AS BIGINT) AS ubigint_min, + --Use string to represent unsigned big int due to lack of support from + --remote test server + '18446744073709551615' AS ubigint_max, + + CAST(-999999999 AS DECIMAL(38, 0)) AS decimal_negative, + CAST(999999999 AS DECIMAL(38, 0)) AS decimal_positive, + + CAST(-3.40282347E38 AS FLOAT) AS float_min, CAST(3.40282347E38 AS FLOAT) AS float_max, + + CAST(-1.7976931348623157E308 AS DOUBLE) AS double_min, + CAST(1.7976931348623157E308 AS DOUBLE) AS double_max, + + --Boolean + CAST(false AS BOOLEAN) AS bit_false, + CAST(true AS BOOLEAN) AS bit_true, + + --Character types + 'Z' AS c_char, '你' AS c_wchar, + + '你好' AS c_wvarchar, + + 'XYZ' AS c_varchar, + + --Date / timestamp + CAST(DATE '1400-01-01' AS DATE) AS date_min, + CAST(DATE '9999-12-31' AS DATE) AS date_max, + + CAST(TIMESTAMP '1400-01-01 00:00:00' AS TIMESTAMP) AS timestamp_min, + CAST(TIMESTAMP '9999-12-31 23:59:59' AS TIMESTAMP) AS timestamp_max; + )"; + return wsql; +} + void FlightSQLODBCRemoteTestBase::SetUp() { if (arrow::internal::GetEnvVar(TEST_CONNECT_STR).ValueOr("").empty()) { GTEST_SKIP() << "Skipping FlightSQLODBCRemoteTestBase test: TEST_CONNECT_STR not set"; @@ -173,6 +224,55 @@ std::string FlightSQLODBCMockTestBase::getInvalidConnectionString() { return connect_str; } +std::wstring FlightSQLODBCMockTestBase::getQueryAllDataTypes() { + std::wstring wsql = + LR"( SELECT + -- Numeric types + -128 AS stiny_int_min, 127 AS stiny_int_max, + 0 AS utiny_int_min, 255 AS utiny_int_max, + + -32768 AS ssmall_int_min, 32767 AS ssmall_int_max, + 0 AS usmall_int_min, 65535 AS usmall_int_max, + + CAST(-2147483648 AS INTEGER) AS sinteger_min, + CAST(2147483647 AS INTEGER) AS sinteger_max, + CAST(0 AS INTEGER) AS uinteger_min, + CAST(4294967295 AS INTEGER) AS uinteger_max, + + CAST(-9223372036854775808 AS INTEGER) AS sbigint_min, + CAST(9223372036854775807 AS INTEGER) AS sbigint_max, + CAST(0 AS INTEGER) AS ubigint_min, + -- stored as TEXT as SQLite doesn't support unsigned big int + '18446744073709551615' AS ubigint_max, + + CAST('-999999999' AS NUMERIC) AS decimal_negative, + CAST('999999999' AS NUMERIC) AS decimal_positive, + + CAST(-3.40282347E38 AS REAL) AS float_min, + CAST(3.40282347E38 AS REAL) AS float_max, + + CAST(-1.7976931348623157E308 AS REAL) AS double_min, + CAST(1.7976931348623157E308 AS REAL) AS double_max, + + -- Boolean + 0 AS bit_false, + 1 AS bit_true, + + -- Character types + 'Z' AS c_char, + '你' AS c_wchar, + '你好' AS c_wvarchar, + 'XYZ' AS c_varchar, + + DATE('1400-01-01') AS date_min, + DATE('9999-12-31') AS date_max, + + DATETIME('1400-01-01 00:00:00') AS timestamp_min, + DATETIME('9999-12-31 23:59:59') AS timestamp_max; + )"; + return wsql; +} + void FlightSQLODBCMockTestBase::SetUp() { ASSERT_OK_AND_ASSIGN(auto location, Location::ForGrpcTcp("0.0.0.0", 0)); arrow::flight::FlightServerOptions options(location); 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 5f8b3b9c757..fc9216c3e85 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 @@ -61,6 +61,8 @@ class FlightSQLODBCRemoteTestBase : public ::testing::Test { /// \brief Get invalid connection string based on connection string defined in /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN" std::string virtual getInvalidConnectionString(); + /// \brief Return a SQL query that selects all data types + std::wstring virtual getQueryAllDataTypes(); /** ODBC Environment. */ SQLHENV env; @@ -119,6 +121,8 @@ class FlightSQLODBCMockTestBase : public FlightSQLODBCRemoteTestBase { std::string getConnectionString() override; /// \brief Get invalid connection string for mock server std::string getInvalidConnectionString() override; + /// \brief Return a SQL query that selects all data types + std::wstring getQueryAllDataTypes() override; int port; @@ -151,8 +155,12 @@ bool compareConnPropertyMap(Connection::ConnPropertyMap map1, /// Get error message from ODBC driver using SQLGetDiagRec std::string GetOdbcErrorMessage(SQLSMALLINT handle_type, SQLHANDLE handle); +static constexpr std::string_view error_state_01004 = "01004"; +static constexpr std::string_view error_state_01S07 = "01S07"; static constexpr std::string_view error_state_01S02 = "01S02"; static constexpr std::string_view error_state_08003 = "08003"; +static constexpr std::string_view error_state_22002 = "22002"; +static constexpr std::string_view error_state_24000 = "24000"; static constexpr std::string_view error_state_28000 = "28000"; static constexpr std::string_view error_state_HY000 = "HY000"; static constexpr std::string_view error_state_HY024 = "HY024"; 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 5f9ee060760..4c4a753e703 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -24,6 +24,9 @@ #include #include +#include + +#include "gmock/gmock.h" #include "gtest/gtest.h" namespace arrow::flight::sql::odbc { @@ -38,7 +41,26 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectSimpleQuery) { SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); EXPECT_EQ(ret, SQL_SUCCESS); - // TODO: after SQLFetch and SQLGetData are implemented, fetch data to verify + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 1 is returned + EXPECT_EQ(val, 1); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_NO_DATA); + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + // Invalid cursor state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_24000); this->disconnect(); } @@ -58,4 +80,1300 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectInvalidQuery) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectDataQuery) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN buf_len = sizeof(stiny_int_val); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(stiny_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(stiny_int_val, std::numeric_limits::max()); + + // Unsigned Tiny Int + uint8_t utiny_int_val; + buf_len = sizeof(utiny_int_val); + + ret = SQLGetData(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(utiny_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(utiny_int_val, std::numeric_limits::max()); + + // Signed Small Int + int16_t ssmall_int_val; + buf_len = sizeof(ssmall_int_val); + + ret = SQLGetData(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ssmall_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ssmall_int_val, std::numeric_limits::max()); + + // Unsigned Small Int + uint16_t usmall_int_val; + buf_len = sizeof(usmall_int_val); + + ret = SQLGetData(this->stmt, 7, SQL_C_USHORT, &usmall_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(usmall_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 8, SQL_C_USHORT, &usmall_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(usmall_int_val, std::numeric_limits::max()); + + // Signed Integer + SQLINTEGER slong_val; + buf_len = sizeof(slong_val); + + ret = SQLGetData(this->stmt, 9, SQL_C_SLONG, &slong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 10, SQL_C_SLONG, &slong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::max()); + + // Unsigned Integer + SQLUINTEGER ulong_val; + buf_len = sizeof(ulong_val); + + ret = SQLGetData(this->stmt, 11, SQL_C_ULONG, &ulong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ulong_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 12, SQL_C_ULONG, &ulong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ulong_val, std::numeric_limits::max()); + + // Signed Big Int + SQLBIGINT sbig_int_val; + buf_len = sizeof(sbig_int_val); + + ret = SQLGetData(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::max()); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val; + buf_len = sizeof(ubig_int_val); + + ret = SQLGetData(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ubig_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ubig_int_val, std::numeric_limits::max()); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + buf_len = sizeof(SQL_NUMERIC_STRUCT); + + ret = SQLGetData(this->stmt, 17, SQL_C_NUMERIC, &decimal_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for negative decimal_val value + EXPECT_EQ(decimal_val.sign, 0); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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)); + ret = SQLGetData(this->stmt, 18, SQL_C_NUMERIC, &decimal_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for positive decimal_val value + EXPECT_EQ(decimal_val.sign, 1); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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); + + ret = SQLGetData(this->stmt, 19, SQL_C_FLOAT, &float_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative float value + EXPECT_EQ(float_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 20, SQL_C_FLOAT, &float_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(float_val, std::numeric_limits::max()); + + // Double + SQLDOUBLE double_val; + buf_len = sizeof(double_val); + + ret = SQLGetData(this->stmt, 21, SQL_C_DOUBLE, &double_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative double value + EXPECT_EQ(double_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 22, SQL_C_DOUBLE, &double_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(double_val, std::numeric_limits::max()); + + // Bit + bool bit_val; + buf_len = sizeof(bit_val); + + ret = SQLGetData(this->stmt, 23, SQL_C_BIT, &bit_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, false); + + ret = SQLGetData(this->stmt, 24, SQL_C_BIT, &bit_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, true); + + // Characters + + // Char + SQLCHAR char_val[2]; + buf_len = sizeof(SQLCHAR) * 2; + + ret = SQLGetData(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(char_val[0], 'Z'); + + // WChar + SQLWCHAR wchar_val[2]; + constexpr size_t wchar_size = driver::odbcabstraction::GetSqlWCharSize(); + buf_len = wchar_size * 2; + + ret = SQLGetData(this->stmt, 26, SQL_C_WCHAR, &wchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wchar_val[0], L'你'); + + // WVarchar + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + + ret = SQLGetData(this->stmt, 27, SQL_C_WCHAR, &wvarchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wvarchar_val[0], L'你'); + EXPECT_EQ(wvarchar_val[1], L'好'); + + // varchar + SQLCHAR varchar_val[4]; + buf_len = sizeof(SQLCHAR) * 4; + + ret = SQLGetData(this->stmt, 28, SQL_C_CHAR, &varchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(varchar_val[0], 'X'); + EXPECT_EQ(varchar_val[1], 'Y'); + EXPECT_EQ(varchar_val[2], 'Z'); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + buf_len = sizeof(date_var); + + ret = SQLGetData(this->stmt, 29, SQL_C_TYPE_DATE, &date_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(date_var.day, 1); + EXPECT_EQ(date_var.month, 1); + EXPECT_EQ(date_var.year, 1400); + + ret = SQLGetData(this->stmt, 30, SQL_C_TYPE_DATE, &date_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(date_var.day, 31); + EXPECT_EQ(date_var.month, 12); + EXPECT_EQ(date_var.year, 9999); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + buf_len = sizeof(timestamp_var); + + ret = SQLGetData(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, ×tamp_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(timestamp_var.day, 1); + EXPECT_EQ(timestamp_var.month, 1); + EXPECT_EQ(timestamp_var.year, 1400); + EXPECT_EQ(timestamp_var.hour, 0); + EXPECT_EQ(timestamp_var.minute, 0); + EXPECT_EQ(timestamp_var.second, 0); + EXPECT_EQ(timestamp_var.fraction, 0); + + ret = SQLGetData(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, ×tamp_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(timestamp_var.day, 31); + EXPECT_EQ(timestamp_var.month, 12); + EXPECT_EQ(timestamp_var.year, 9999); + EXPECT_EQ(timestamp_var.hour, 23); + EXPECT_EQ(timestamp_var.minute, 59); + EXPECT_EQ(timestamp_var.second, 59); + EXPECT_EQ(timestamp_var.fraction, 0); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectTimeQuery) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + this->connect(); + + 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()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQL_TIME_STRUCT time_var{}; + SQLLEN buf_len = sizeof(time_var); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_TYPE_TIME, &time_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for time. + EXPECT_EQ(time_var.hour, 0); + EXPECT_EQ(time_var.minute, 0); + EXPECT_EQ(time_var.second, 0); + + ret = SQLGetData(this->stmt, 2, SQL_C_TYPE_TIME, &time_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for time. + EXPECT_EQ(time_var.hour, 23); + EXPECT_EQ(time_var.minute, 59); + EXPECT_EQ(time_var.second, 59); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLExecDirectVarbinaryQuery) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + this->connect(); + + std::wstring wsql = L"SELECT X'ABCDEF' AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ret = SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val[0], buf_len, &ind); + EXPECT_EQ(varbinary_val[0], '\xAB'); + EXPECT_EQ(varbinary_val[1], '\xCD'); + EXPECT_EQ(varbinary_val[2], '\xEF'); + + this->disconnect(); +} + +// Tests with SQL_C_DEFAULT as the target type + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectDataQueryDefaultType) { + // Test with default types. Only testing target types supported by server. + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Numeric Types + // Signed Integer + SQLINTEGER slong_val; + SQLLEN buf_len = sizeof(slong_val); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 9, SQL_C_DEFAULT, &slong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 10, SQL_C_DEFAULT, &slong_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::max()); + + // Signed Big Int + SQLBIGINT sbig_int_val; + buf_len = sizeof(sbig_int_val); + + ret = SQLGetData(this->stmt, 13, SQL_C_DEFAULT, &sbig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 14, SQL_C_DEFAULT, &sbig_int_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::max()); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + buf_len = sizeof(SQL_NUMERIC_STRUCT); + + ret = SQLGetData(this->stmt, 17, SQL_C_DEFAULT, &decimal_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for negative decimal_val value + EXPECT_EQ(decimal_val.sign, 0); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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)); + ret = SQLGetData(this->stmt, 18, SQL_C_DEFAULT, &decimal_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for positive decimal_val value + EXPECT_EQ(decimal_val.sign, 1); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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); + + ret = SQLGetData(this->stmt, 19, SQL_C_DEFAULT, &float_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative float value + EXPECT_EQ(float_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 20, SQL_C_DEFAULT, &float_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(float_val, std::numeric_limits::max()); + + // Double + SQLDOUBLE double_val; + buf_len = sizeof(double_val); + + ret = SQLGetData(this->stmt, 21, SQL_C_DEFAULT, &double_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative double value + EXPECT_EQ(double_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 22, SQL_C_DEFAULT, &double_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(double_val, std::numeric_limits::max()); + + // Bit + bool bit_val; + buf_len = sizeof(bit_val); + + ret = SQLGetData(this->stmt, 23, SQL_C_DEFAULT, &bit_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, false); + + ret = SQLGetData(this->stmt, 24, SQL_C_DEFAULT, &bit_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, true); + + // Characters + + // Char will be fetched as wchar by default + SQLWCHAR wchar_val[2]; + constexpr size_t wchar_size = driver::odbcabstraction::GetSqlWCharSize(); + buf_len = wchar_size * 2; + + ret = SQLGetData(this->stmt, 25, SQL_C_DEFAULT, &wchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wchar_val[0], L'Z'); + + // WChar + SQLWCHAR wchar_val2[2]; + buf_len = wchar_size * 2; + ret = SQLGetData(this->stmt, 26, SQL_C_DEFAULT, &wchar_val2, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wchar_val2[0], L'你'); + + // WVarchar + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + + ret = SQLGetData(this->stmt, 27, SQL_C_DEFAULT, &wvarchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wvarchar_val[0], L'你'); + EXPECT_EQ(wvarchar_val[1], L'好'); + + // Varchar will be fetched as WVarchar by default + SQLWCHAR wvarchar_val2[4]; + buf_len = wchar_size * 4; + + ret = SQLGetData(this->stmt, 28, SQL_C_DEFAULT, &wvarchar_val2, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(wvarchar_val2[0], L'X'); + EXPECT_EQ(wvarchar_val2[1], L'Y'); + EXPECT_EQ(wvarchar_val2[2], L'Z'); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + buf_len = sizeof(date_var); + + ret = SQLGetData(this->stmt, 29, SQL_C_DEFAULT, &date_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(date_var.day, 1); + EXPECT_EQ(date_var.month, 1); + EXPECT_EQ(date_var.year, 1400); + + ret = SQLGetData(this->stmt, 30, SQL_C_DEFAULT, &date_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(date_var.day, 31); + EXPECT_EQ(date_var.month, 12); + EXPECT_EQ(date_var.year, 9999); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + buf_len = sizeof(timestamp_var); + + ret = SQLGetData(this->stmt, 31, SQL_C_DEFAULT, ×tamp_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(timestamp_var.day, 1); + EXPECT_EQ(timestamp_var.month, 1); + EXPECT_EQ(timestamp_var.year, 1400); + EXPECT_EQ(timestamp_var.hour, 0); + EXPECT_EQ(timestamp_var.minute, 0); + EXPECT_EQ(timestamp_var.second, 0); + EXPECT_EQ(timestamp_var.fraction, 0); + + ret = SQLGetData(this->stmt, 32, SQL_C_DEFAULT, ×tamp_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(timestamp_var.day, 31); + EXPECT_EQ(timestamp_var.month, 12); + EXPECT_EQ(timestamp_var.year, 9999); + EXPECT_EQ(timestamp_var.hour, 23); + EXPECT_EQ(timestamp_var.minute, 59); + EXPECT_EQ(timestamp_var.second, 59); + EXPECT_EQ(timestamp_var.fraction, 0); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectTimeQueryDefaultType) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + this->connect(); + + 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()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQL_TIME_STRUCT time_var{}; + SQLLEN buf_len = sizeof(time_var); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_DEFAULT, &time_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for time. + EXPECT_EQ(time_var.hour, 0); + EXPECT_EQ(time_var.minute, 0); + EXPECT_EQ(time_var.second, 0); + + ret = SQLGetData(this->stmt, 2, SQL_C_DEFAULT, &time_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for time. + EXPECT_EQ(time_var.hour, 23); + EXPECT_EQ(time_var.minute, 59); + EXPECT_EQ(time_var.second, 59); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, 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 + this->connect(); + + std::wstring wsql = L"SELECT from_hex('ABCDEF') AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ret = SQLGetData(this->stmt, 1, SQL_C_DEFAULT, &varbinary_val[0], buf_len, &ind); + EXPECT_EQ(varbinary_val[0], '\xAB'); + EXPECT_EQ(varbinary_val[1], '\xCD'); + EXPECT_EQ(varbinary_val[2], '\xEF'); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectGuidQueryUnsupported) { + this->connect(); + + // 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()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLGUID guid_var; + SQLLEN buf_len = sizeof(guid_var); + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_GUID, &guid_var, buf_len, &ind); + + EXPECT_EQ(ret, SQL_ERROR); + // GUID is not supported by ODBC + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY000); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectRowFetching) { + this->connect(); + + std::wstring wsql = + LR"( + SELECT 1 AS small_table + UNION ALL + SELECT 2 + UNION ALL + SELECT 3; + )"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Fetch row 1 + ret = SQLFetch(this->stmt); + 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); + + // Fetch row 2 + ret = SQLFetch(this->stmt); + 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); + // Verify 3 is returned + EXPECT_EQ(val, 3); + + // Verify result set has no more data beyond row 3 + 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, TestSQLExecDirectVarcharTruncation) { + this->connect(); + + std::wstring wsql = L"SELECT 'VERY LONG STRING here' AS string_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + const int len = 17; + SQLCHAR char_val[len]; + SQLLEN buf_len = sizeof(SQLCHAR) * len; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + EXPECT_EQ(ODBC::SqlStringToString(char_val), std::string("VERY LONG STRING")); + EXPECT_EQ(ind, 21); + + // Fetch same column 2nd time + const int len2 = 2; + SQLCHAR char_val2[len2]; + buf_len = sizeof(SQLCHAR) * len2; + + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val2, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + EXPECT_EQ(ODBC::SqlStringToString(char_val2), std::string(" ")); + EXPECT_EQ(ind, 5); + + // Fetch same column 3rd time + const int len3 = 5; + SQLCHAR char_val3[len3]; + buf_len = sizeof(SQLCHAR) * len3; + + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val3, buf_len, &ind); + + // Verify that there is no more truncation reports. The full string has been fetched. + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(ODBC::SqlStringToString(char_val3), std::string("here")); + EXPECT_EQ(ind, 4); + + // Attempt to fetch data 4th time + SQLCHAR char_val4[len]; + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val4, 0, &ind); + // Verify SQL_NO_DATA is returned + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectWVarcharTruncation) { + this->connect(); + + std::wstring wsql = L"SELECT 'VERY LONG Unicode STRING 句子 here' AS wstring_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + const int len = 28; + SQLWCHAR wchar_val[len]; + constexpr size_t wchar_size = driver::odbcabstraction::GetSqlWCharSize(); + SQLLEN buf_len = wchar_size * len; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + EXPECT_EQ(std::wstring(wchar_val), std::wstring(L"VERY LONG Unicode STRING 句子")); + EXPECT_EQ(ind, 32 * wchar_size); + + // Fetch same column 2nd time + const int len2 = 2; + SQLWCHAR wchar_val2[len2]; + buf_len = wchar_size * len2; + + ret = SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val2, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + EXPECT_EQ(std::wstring(wchar_val2), std::wstring(L" ")); + EXPECT_EQ(ind, 5 * wchar_size); + + // Fetch same column 3rd time + const int len3 = 5; + SQLWCHAR wchar_val3[len3]; + buf_len = wchar_size * len3; + + ret = SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val3, buf_len, &ind); + + // Verify that there is no more truncation reports. The full string has been fetched. + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(std::wstring(wchar_val3), std::wstring(L"here")); + EXPECT_EQ(ind, 4 * wchar_size); + + // Attempt to fetch data 4th time + SQLWCHAR wchar_val4[len]; + ret = SQLGetData(this->stmt, 1, SQL_C_WCHAR, &wchar_val4, 0, &ind); + // Verify SQL_NO_DATA is returned + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLExecDirectVarbinaryTruncation) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + this->connect(); + + std::wstring wsql = L"SELECT X'ABCDEFAB' AS c_varbinary;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + ret = SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val[0], buf_len, &ind); + // Verify binary truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + EXPECT_EQ(varbinary_val[0], '\xAB'); + EXPECT_EQ(varbinary_val[1], '\xCD'); + EXPECT_EQ(varbinary_val[2], '\xEF'); + EXPECT_EQ(ind, 4); + + // Fetch same column 2nd time + std::vector varbinary_val2(1); + buf_len = varbinary_val2.size(); + + ret = SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val2[0], buf_len, &ind); + + // Verify that there is no more truncation reports. The full binary has been fetched. + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(varbinary_val[0], '\xAB'); + EXPECT_EQ(ind, 1); + + // Attempt to fetch data 3rd time + std::vector varbinary_val3(1); + buf_len = varbinary_val3.size(); + ret = SQLGetData(this->stmt, 1, SQL_C_BINARY, &varbinary_val3[0], buf_len, &ind); + // Verify SQL_NO_DATA is returned + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectFloatTruncation) { + // Test is disabled until float truncation is supported. + // GH-46985: return warning message instead of error on float truncation case + GTEST_SKIP(); + this->connect(); + + 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()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + int16_t ssmall_int_val; + SQLLEN buf_len = sizeof(ssmall_int_val); + + ret = SQLGetData(this->stmt, 1, SQL_C_SSHORT, &ssmall_int_val, 0, 0); + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify float truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01S07); + + EXPECT_EQ(ssmall_int_val, 1); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectNullQuery) { + // Limitation on mock test server prevents null from working properly. + // Mock server has type `DENSE_UNION` for null column data. + this->connect(); + + std::wstring wsql = L"SELECT null as null_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify SQL_NULL_DATA is returned for indicator + EXPECT_EQ(ind, SQL_NULL_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, 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 + this->connect(); + + 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()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 1 is returned for non-truncation case. + EXPECT_EQ(val, 1); + + // Char + const int len = 17; + SQLCHAR char_val[len]; + SQLLEN buf_len = sizeof(SQLCHAR) * len; + + ret = SQLGetData(this->stmt, 2, SQL_C_CHAR, &char_val, buf_len, 0); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + // WChar + const int len2 = 28; + SQLWCHAR wchar_val[len2]; + constexpr size_t wchar_size = driver::odbcabstraction::GetSqlWCharSize(); + buf_len = wchar_size * len2; + + ret = SQLGetData(this->stmt, 3, SQL_C_WCHAR, &wchar_val, buf_len, 0); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + // Verify string truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + // varbinary + std::vector varbinary_val(3); + buf_len = varbinary_val.size(); + ret = SQLGetData(this->stmt, 4, SQL_C_BINARY, &varbinary_val[0], buf_len, 0); + // Verify binary truncation is reported + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_01004); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectNullQueryNullIndicator) { + // Limitation on mock test server prevents null from working properly. + // Mock server has type `DENSE_UNION` for null column data. + this->connect(); + + std::wstring wsql = L"SELECT null as null_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + // Verify invalid null indicator is reported, as it is required + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_22002); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectIgnoreInvalidBufLen) { + // Verify the driver ignores invalid buffer length for fixed data types + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN invalid_buf_len = -1; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(stiny_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(stiny_int_val, std::numeric_limits::max()); + + // Unsigned Tiny Int + uint8_t utiny_int_val; + + ret = SQLGetData(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(utiny_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(utiny_int_val, std::numeric_limits::max()); + + // Signed Small Int + int16_t ssmall_int_val; + + ret = SQLGetData(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ssmall_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ssmall_int_val, std::numeric_limits::max()); + + // Unsigned Small Int + uint16_t usmall_int_val; + + ret = SQLGetData(this->stmt, 7, SQL_C_USHORT, &usmall_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(usmall_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 8, SQL_C_USHORT, &usmall_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(usmall_int_val, std::numeric_limits::max()); + + // Signed Integer + SQLINTEGER slong_val; + + ret = SQLGetData(this->stmt, 9, SQL_C_SLONG, &slong_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 10, SQL_C_SLONG, &slong_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(slong_val, std::numeric_limits::max()); + + // Unsigned Integer + SQLUINTEGER ulong_val; + + ret = SQLGetData(this->stmt, 11, SQL_C_ULONG, &ulong_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ulong_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 12, SQL_C_ULONG, &ulong_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ulong_val, std::numeric_limits::max()); + + // Signed Big Int + SQLBIGINT sbig_int_val; + + ret = SQLGetData(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(sbig_int_val, std::numeric_limits::max()); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val; + + ret = SQLGetData(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ubig_int_val, std::numeric_limits::min()); + + ret = SQLGetData(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ubig_int_val, std::numeric_limits::max()); + + // Decimal + SQL_NUMERIC_STRUCT decimal_val; + memset(&decimal_val, 0, sizeof(decimal_val)); + + ret = SQLGetData(this->stmt, 17, SQL_C_NUMERIC, &decimal_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for negative decimal_val value + EXPECT_EQ(decimal_val.sign, 0); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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)); + ret = SQLGetData(this->stmt, 18, SQL_C_NUMERIC, &decimal_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check for positive decimal_val value + EXPECT_EQ(decimal_val.sign, 1); + EXPECT_EQ(decimal_val.scale, 0); + EXPECT_EQ(decimal_val.precision, 38); + 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; + + ret = SQLGetData(this->stmt, 19, SQL_C_FLOAT, &float_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative float value + EXPECT_EQ(float_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 20, SQL_C_FLOAT, &float_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(float_val, std::numeric_limits::max()); + + // Double + SQLDOUBLE double_val; + + ret = SQLGetData(this->stmt, 21, SQL_C_DOUBLE, &double_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Get minimum negative double value + EXPECT_EQ(double_val, -std::numeric_limits::max()); + + ret = SQLGetData(this->stmt, 22, SQL_C_DOUBLE, &double_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(double_val, std::numeric_limits::max()); + + // Bit + bool bit_val; + + ret = SQLGetData(this->stmt, 23, SQL_C_BIT, &bit_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, false); + + ret = SQLGetData(this->stmt, 24, SQL_C_BIT, &bit_val, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(bit_val, true); + + // Date and Timestamp + + // Date + SQL_DATE_STRUCT date_var{}; + + ret = SQLGetData(this->stmt, 29, SQL_C_TYPE_DATE, &date_var, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(date_var.day, 1); + EXPECT_EQ(date_var.month, 1); + EXPECT_EQ(date_var.year, 1400); + + ret = SQLGetData(this->stmt, 30, SQL_C_TYPE_DATE, &date_var, invalid_buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(date_var.day, 31); + EXPECT_EQ(date_var.month, 12); + EXPECT_EQ(date_var.year, 9999); + + // Timestamp + SQL_TIMESTAMP_STRUCT timestamp_var{}; + + ret = SQLGetData(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, ×tamp_var, invalid_buf_len, + &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check min values for date. Min valid year is 1400. + EXPECT_EQ(timestamp_var.day, 1); + EXPECT_EQ(timestamp_var.month, 1); + EXPECT_EQ(timestamp_var.year, 1400); + EXPECT_EQ(timestamp_var.hour, 0); + EXPECT_EQ(timestamp_var.minute, 0); + EXPECT_EQ(timestamp_var.second, 0); + EXPECT_EQ(timestamp_var.fraction, 0); + + ret = SQLGetData(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, ×tamp_var, invalid_buf_len, + &ind); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Check max values for date. Max valid year is 9999. + EXPECT_EQ(timestamp_var.day, 31); + EXPECT_EQ(timestamp_var.month, 12); + EXPECT_EQ(timestamp_var.year, 9999); + EXPECT_EQ(timestamp_var.hour, 23); + EXPECT_EQ(timestamp_var.minute, 59); + EXPECT_EQ(timestamp_var.second, 59); + EXPECT_EQ(timestamp_var.fraction, 0); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc diff --git a/testing b/testing index fbf6b703dc9..d2a13712303 160000 --- a/testing +++ b/testing @@ -1 +1 @@ -Subproject commit fbf6b703dc93d17d75fa3664c5aa2c7873ebaf06 +Subproject commit d2a13712303498963395318a4eb42872e66aead7 From 8a45b989f68d7606335180b5aa43f84f2005e284 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:56:42 -0700 Subject: [PATCH 26/74] SQLPrepare & SQLExecute Implementation * Add tests * Address comments from Rob --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 19 ++----- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 33 ++++++++++- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 2 + .../flight/sql/odbc/tests/odbc_test_suite.h | 1 + .../flight/sql/odbc/tests/statement_test.cc | 56 +++++++++++++++++++ 5 files changed, 95 insertions(+), 16 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index c7bbc08e49e..1eb421812ab 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -144,6 +144,12 @@ SQLRETURN SQL_API SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLI indicatorPtr); } +SQLRETURN SQL_API SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength) { + return arrow::SQLPrepare(stmt, queryText, textLength); +} + +SQLRETURN SQL_API SQLExecute(SQLHSTMT stmt) { return arrow::SQLExecute(stmt); } + SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { @@ -208,11 +214,6 @@ SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, return SQL_ERROR; } -SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { - LOG_DEBUG("SQLExecute called with statementHandle: {}", statementHandle); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, @@ -265,14 +266,6 @@ SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { return arrow::SQLRowCount(stmt, rowCountPtr); } -SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, SQLWCHAR* statementText, - SQLINTEGER textLength) { - LOG_DEBUG( - "SQLPrepareW called with statementHandle: {}, statementText: {}, textLength: {}", - statementHandle, fmt::ptr(statementText), textLength); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT statementHandle, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 786d295d97c..352072ef21a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -314,7 +314,7 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT case SQL_DIAG_CURSOR_ROW_COUNT: { if (handleType == SQL_HANDLE_STMT) { if (diagInfoPtr) { - // Will always be 0 if only select supported + // Will always be 0 if only SELECT supported *static_cast(diagInfoPtr) = 0; } @@ -894,7 +894,7 @@ SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLengt LOG_DEBUG("SQLExecDirectW called with stmt: {}, queryText: {}, textLength: {}", stmt, fmt::ptr(queryText), textLength); using ODBC::ODBCStatement; - // The driver is built to handle select statements only. + // The driver is built to handle SELECT statements only. return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); std::string query = ODBC::SqlWcharToString(queryText, textLength); @@ -906,6 +906,34 @@ SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLengt }); } +SQLRETURN SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength) { + LOG_DEBUG("SQLPrepareW called with stmt: {}, queryText: {}, textLength: {}", stmt, + fmt::ptr(queryText), textLength); + using ODBC::ODBCStatement; + // The driver is built to handle SELECT statements only. + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + std::string query = ODBC::SqlWcharToString(queryText, textLength); + + statement->Prepare(query); + + return SQL_SUCCESS; + }); +} + +SQLRETURN SQLExecute(SQLHSTMT stmt) { + LOG_DEBUG("SQLExecute called with stmt: {}", stmt); + using ODBC::ODBCStatement; + // The driver is built to handle SELECT statements only. + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + statement->ExecutePrepared(); + + return SQL_SUCCESS; + }); +} + SQLRETURN SQLFetch(SQLHSTMT stmt) { LOG_DEBUG("SQLFetch called with stmt: {}", stmt); using ODBC::ODBCDescriptor; @@ -976,5 +1004,4 @@ SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { return SQL_SUCCESS; }); } - } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index c41a913090c..07614ec84ba 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -63,6 +63,8 @@ SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePt SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); +SQLRETURN SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); +SQLRETURN SQLExecute(SQLHSTMT stmt); SQLRETURN SQLFetch(SQLHSTMT stmt); SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); 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 fc9216c3e85..30017b01937 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 @@ -163,6 +163,7 @@ static constexpr std::string_view error_state_22002 = "22002"; static constexpr std::string_view error_state_24000 = "24000"; static constexpr std::string_view error_state_28000 = "28000"; static constexpr std::string_view error_state_HY000 = "HY000"; +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"; 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 4c4a753e703..3335d5f7246 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -81,6 +81,62 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectInvalidQuery) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecuteSimpleQuery) { + this->connect(); + + std::wstring wsql = L"SELECT 1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = SQLPrepare(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLExecute(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Fetch data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLINTEGER val; + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + // Verify 1 is returned + EXPECT_EQ(val, 1); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_NO_DATA); + + ret = SQLGetData(this->stmt, 1, SQL_C_LONG, &val, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + // Invalid cursor state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_24000); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLPrepareInvalidQuery) { + this->connect(); + + std::wstring wsql = L"SELECT;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = SQLPrepare(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_ERROR); + // ODBC provides generic error code HY000 to all statement errors + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY000); + + ret = SQLExecute(this->stmt); + // Verify function sequence error state is returned + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY010); + + this->disconnect(); +} + TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectDataQuery) { this->connect(); From f51131f72cae7ce36259672fc0d5105dfe5b92c0 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:48:37 -0700 Subject: [PATCH 27/74] Code Style fixes * Move ifdefs outside of test cases * use `stmt` * Move logs to first line * fix test for `SQL_DRIVER_HSTMT` * Use camel case for tests --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 64 ++-- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 89 ++--- .../sql/odbc/tests/connection_attr_test.cc | 40 +-- .../sql/odbc/tests/connection_info_test.cc | 336 +++++++++--------- 4 files changed, 267 insertions(+), 262 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 1eb421812ab..9afac8ea28e 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -150,53 +150,53 @@ SQLRETURN SQL_API SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER text SQLRETURN SQL_API SQLExecute(SQLHSTMT stmt) { return arrow::SQLExecute(stmt); } -SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, +SQLRETURN SQL_API SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { LOG_DEBUG( - "SQLBindCol called with statementHandle: {}, columnNumber: {}, targetType: {}, " + "SQLBindCol called with stmt: {}, columnNumber: {}, targetType: {}, " "targetValuePtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", - statementHandle, columnNumber, targetType, targetValuePtr, bufferLength, + stmt, columnNumber, targetType, targetValuePtr, bufferLength, fmt::ptr(strLen_or_IndPtr)); return SQL_ERROR; } -SQLRETURN SQL_API SQLCancel(SQLHSTMT statementHandle) { - LOG_DEBUG("SQLCancel called with statementHandle: {}", statementHandle); +SQLRETURN SQL_API SQLCancel(SQLHSTMT stmt) { + LOG_DEBUG("SQLCancel called with stmt: {}", stmt); return SQL_ERROR; } -SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { - LOG_DEBUG("SQLCloseCursor called with statementHandle: {}", statementHandle); +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT stmt) { + LOG_DEBUG("SQLCloseCursor called with stmt: {}", stmt); return SQL_ERROR; } -SQLRETURN SQL_API SQLColAttribute(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, +SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, SQLLEN* numericAttributePtr) { LOG_DEBUG( - "SQLColAttributeW called with statementHandle: {}, columnNumber: {}, " + "SQLColAttributeW called with stmt: {}, columnNumber: {}, " "fieldIdentifier: {}, characterAttributePtr: {}, bufferLength: {}, " "stringLengthPtr: {}, numericAttributePtr: {}", - statementHandle, columnNumber, fieldIdentifier, characterAttributePtr, bufferLength, + stmt, columnNumber, fieldIdentifier, characterAttributePtr, bufferLength, fmt::ptr(stringLengthPtr), fmt::ptr(numericAttributePtr)); return SQL_ERROR; } -SQLRETURN SQL_API SQLColumns(SQLHSTMT statementHandle, SQLWCHAR* catalogName, +SQLRETURN SQL_API SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* columnName, SQLSMALLINT columnNameLength) { LOG_DEBUG( - "SQLColumnsW called with statementHandle: {}, catalogName: {}, catalogNameLength: " + "SQLColumnsW called with stmt: {}, catalogName: {}, catalogNameLength: " "{}, " "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " "columnName: {}, " "columnNameLength: {}", - statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(columnName), columnNameLength); return SQL_ERROR; @@ -214,7 +214,7 @@ SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, return SQL_ERROR; } -SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogName, +SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT stmt, SQLWCHAR* pKCatalogName, SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, SQLSMALLINT pKTableNameLength, SQLWCHAR* fKCatalogName, @@ -222,24 +222,22 @@ SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, SQLWCHAR* pKCatalogNa SQLSMALLINT fKSchemaNameLength, SQLWCHAR* fKTableName, SQLSMALLINT fKTableNameLength) { LOG_DEBUG( - "SQLForeignKeysW called with statementHandle: {}, pKCatalogName: {}, " + "SQLForeignKeysW called with stmt: {}, pKCatalogName: {}, " "pKCatalogNameLength: " "{}, pKSchemaName: {}, pKSchemaNameLength: {}, pKTableName: {}, pKTableNameLength: " "{}, " "fKCatalogName: {}, fKCatalogNameLength: {}, fKSchemaName: {}, fKSchemaNameLength: " "{}, " "fKTableName: {}, fKTableNameLength : {}", - statementHandle, fmt::ptr(pKCatalogName), pKCatalogNameLength, - fmt::ptr(pKSchemaName), pKSchemaNameLength, fmt::ptr(pKTableName), - pKTableNameLength, fmt::ptr(fKCatalogName), fKCatalogNameLength, - fmt::ptr(fKSchemaName), fKSchemaNameLength, fmt::ptr(fKTableName), - fKTableNameLength); + stmt, fmt::ptr(pKCatalogName), pKCatalogNameLength, fmt::ptr(pKSchemaName), + pKSchemaNameLength, fmt::ptr(pKTableName), pKTableNameLength, + fmt::ptr(fKCatalogName), fKCatalogNameLength, fmt::ptr(fKSchemaName), + fKSchemaNameLength, fmt::ptr(fKTableName), fKTableNameLength); return SQL_ERROR; } -SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { - LOG_DEBUG("SQLGetTypeInfoW called with statementHandle: {} dataType: {}", - statementHandle, dataType); +SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType) { + LOG_DEBUG("SQLGetTypeInfoW called with stmt: {} dataType: {}", stmt, dataType); return SQL_ERROR; } @@ -266,40 +264,40 @@ SQLRETURN SQL_API SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { return arrow::SQLRowCount(stmt, rowCountPtr); } -SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT statementHandle, SQLWCHAR* catalogName, +SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength) { LOG_DEBUG( - "SQLPrimaryKeysW called with statementHandle: {}, catalogName: {}, " + "SQLPrimaryKeysW called with stmt: {}, catalogName: {}, " "catalogNameLength: " "{}, schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}", - statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), schemaNameLength, fmt::ptr(tableName), tableNameLength); return SQL_ERROR; } -SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, - SQLPOINTER valuePtr, SQLINTEGER stringLength) { +SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER stringLength) { LOG_DEBUG( - "SQLSetStmtAttrW called with statementHandle: {}, attribute: {}, valuePtr: {}, " + "SQLSetStmtAttrW called with stmt: {}, attribute: {}, valuePtr: {}, " "stringLength: {}", - statementHandle, attribute, valuePtr, stringLength); + stmt, attribute, valuePtr, stringLength); return SQL_ERROR; } -SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, SQLWCHAR* catalogName, +SQLRETURN SQL_API SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* tableType, SQLSMALLINT tableTypeLength) { LOG_DEBUG( - "SQLTablesW called with statementHandle: {}, catalogName: {}, catalogNameLength: " + "SQLTablesW called with stmt: {}, catalogName: {}, catalogNameLength: " "{}, " "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " "tableType: {}, " "tableTypeLength: {}", - statementHandle, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(tableType), tableTypeLength); return SQL_ERROR; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 352072ef21a..d00e5d2fbf1 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -230,18 +230,18 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { // TODO: Implement additional fields types // https://github.com/apache/arrow/issues/46573 - using driver::odbcabstraction::Diagnostics; - using ODBC::GetStringAttribute; - using ODBC::ODBCConnection; - using ODBC::ODBCEnvironment; - using ODBC::ODBCStatement; - LOG_DEBUG( "SQLGetDiagFieldW called with handleType: {}, handle: {}, recNumber: {}, " "diagIdentifier: {}, diagInfoPtr: {}, bufferLength: {}, stringLengthPtr: {}", handleType, handle, recNumber, diagIdentifier, diagInfoPtr, bufferLength, fmt::ptr(stringLengthPtr)); + using driver::odbcabstraction::Diagnostics; + using ODBC::GetStringAttribute; + using ODBC::ODBCConnection; + using ODBC::ODBCEnvironment; + using ODBC::ODBCStatement; + if (!handle) { return SQL_INVALID_HANDLE; } @@ -482,12 +482,6 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re SQLWCHAR* sqlState, SQLINTEGER* nativeErrorPtr, SQLWCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLengthPtr) { - using driver::odbcabstraction::Diagnostics; - using ODBC::GetStringAttribute; - using ODBC::ODBCConnection; - using ODBC::ODBCEnvironment; - using ODBC::ODBCStatement; - LOG_DEBUG( "SQLGetDiagRecW called with handleType: {}, handle: {}, recNumber: {}, " "sqlState: {}, nativeErrorPtr: {}, messageText: {}, bufferLength: {}, " @@ -495,6 +489,12 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re handleType, handle, recNumber, fmt::ptr(sqlState), fmt::ptr(nativeErrorPtr), fmt::ptr(messageText), bufferLength, fmt::ptr(textLengthPtr)); + using driver::odbcabstraction::Diagnostics; + using ODBC::GetStringAttribute; + using ODBC::ODBCConnection; + using ODBC::ODBCEnvironment; + using ODBC::ODBCStatement; + if (!handle) { return SQL_INVALID_HANDLE; } @@ -567,14 +567,14 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* strLenPtr) { - using driver::odbcabstraction::DriverException; - using ODBC::ODBCEnvironment; - LOG_DEBUG( "SQLGetEnvAttr called with env: {}, attr: {}, valuePtr: {}, " "bufferLength: {}, strLenPtr: {}", env, attr, valuePtr, bufferLength, fmt::ptr(strLenPtr)); + using driver::odbcabstraction::DriverException; + using ODBC::ODBCEnvironment; + ODBCEnvironment* environment = reinterpret_cast(env); return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { @@ -628,14 +628,14 @@ SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER strLen) { - using driver::odbcabstraction::DriverException; - using ODBC::ODBCEnvironment; - LOG_DEBUG( "SQLSetEnvAttr called with env: {}, attr: {}, valuePtr: {}, " "strLen: {}", env, attr, valuePtr, strLen); + using driver::odbcabstraction::DriverException; + using ODBC::ODBCEnvironment; + ODBCEnvironment* environment = reinterpret_cast(env); return ODBCEnvironment::ExecuteWithDiagnostics(environment, SQL_ERROR, [=]() { @@ -680,14 +680,14 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, SQLRETURN SQLGetConnectAttr(SQLHDBC conn, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { - using driver::odbcabstraction::Connection; - using ODBC::ODBCConnection; - LOG_DEBUG( "SQLGetConnectAttrW called with conn: {}, attribute: {}, valuePtr: {}, " "bufferLength: {}, stringLengthPtr: {}", conn, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { const bool isUnicode = true; ODBCConnection* connection = reinterpret_cast(conn); @@ -698,13 +698,13 @@ SQLRETURN SQLGetConnectAttr(SQLHDBC conn, SQLINTEGER attribute, SQLPOINTER value SQLRETURN SQLSetConnectAttr(SQLHDBC conn, SQLINTEGER attr, SQLPOINTER valuePtr, SQLINTEGER valueLen) { - using driver::odbcabstraction::Connection; - using ODBC::ODBCConnection; - LOG_DEBUG( "SQLSetConnectAttrW called with conn: {}, attr: {}, valuePtr: {}, valueLen: {}", conn, attr, valuePtr, valueLen); + using driver::odbcabstraction::Connection; + using ODBC::ODBCConnection; + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { const bool isUnicode = true; ODBCConnection* connection = reinterpret_cast(conn); @@ -720,6 +720,14 @@ SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, SQLSMALLINT outConnectionStringBufferLen, SQLSMALLINT* outConnectionStringLen, SQLUSMALLINT driverCompletion) { + LOG_DEBUG( + "SQLDriverConnectW called with conn: {}, windowHandle: {}, inConnectionString: {}, " + "inConnectionStringLen: {}, outConnectionString: {}, outConnectionStringBufferLen: " + "{}, outConnectionStringLen: {}, driverCompletion: {}", + conn, fmt::ptr(windowHandle), fmt::ptr(inConnectionString), inConnectionStringLen, + fmt::ptr(outConnectionString), outConnectionStringBufferLen, + fmt::ptr(outConnectionStringLen), driverCompletion); + // TODO: Implement FILEDSN and SAVEFILE keywords according to the spec // https://github.com/apache/arrow/issues/46449 @@ -730,14 +738,6 @@ SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, using driver::odbcabstraction::DriverException; using ODBC::ODBCConnection; - LOG_DEBUG( - "SQLDriverConnectW called with conn: {}, windowHandle: {}, inConnectionString: {}, " - "inConnectionStringLen: {}, outConnectionString: {}, outConnectionStringBufferLen: " - "{}, outConnectionStringLen: {}, driverCompletion: {}", - conn, fmt::ptr(windowHandle), fmt::ptr(inConnectionString), inConnectionStringLen, - fmt::ptr(outConnectionString), outConnectionStringBufferLen, - fmt::ptr(outConnectionStringLen), driverCompletion); - return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); std::string connection_string = @@ -796,18 +796,18 @@ SQLRETURN SQLDriverConnect(SQLHDBC conn, SQLHWND windowHandle, SQLRETURN SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, SQLWCHAR* userName, SQLSMALLINT userNameLen, SQLWCHAR* password, SQLSMALLINT passwordLen) { - using driver::flight_sql::FlightSqlConnection; - using driver::flight_sql::config::Configuration; - using ODBC::ODBCConnection; - - using ODBC::SqlWcharToString; - LOG_DEBUG( "SQLConnectW called with conn: {}, dsnName: {}, dsnNameLen: {}, userName: {}, " "userNameLen: {}, password: {}, passwordLen: {}", conn, fmt::ptr(dsnName), dsnNameLen, fmt::ptr(userName), userNameLen, fmt::ptr(password), passwordLen); + using driver::flight_sql::FlightSqlConnection; + using driver::flight_sql::config::Configuration; + using ODBC::ODBCConnection; + + using ODBC::SqlWcharToString; + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); std::string dsn = SqlWcharToString(dsnName, dsnNameLen); @@ -834,10 +834,10 @@ SQLRETURN SQLConnect(SQLHDBC conn, SQLWCHAR* dsnName, SQLSMALLINT dsnNameLen, } SQLRETURN SQLDisconnect(SQLHDBC conn) { - using ODBC::ODBCConnection; - LOG_DEBUG("SQLDisconnect called with conn: {}", conn); + using ODBC::ODBCConnection; + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); @@ -849,13 +849,13 @@ SQLRETURN SQLDisconnect(SQLHDBC conn) { SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, SQLSMALLINT bufLen, SQLSMALLINT* stringLengthPtr) { - using ODBC::ODBCConnection; - LOG_DEBUG( "SQLGetInfo called with conn: {}, infoType: {}, infoValuePtr: {}, bufLen: {}, " "stringLengthPtr: {}", conn, infoType, infoValuePtr, bufLen, fmt::ptr(stringLengthPtr)); + using ODBC::ODBCConnection; + return ODBCConnection::ExecuteWithDiagnostics(conn, SQL_ERROR, [=]() { ODBCConnection* connection = reinterpret_cast(conn); @@ -923,6 +923,7 @@ SQLRETURN SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength) SQLRETURN SQLExecute(SQLHSTMT stmt) { LOG_DEBUG("SQLExecute called with stmt: {}", stmt); + using ODBC::ODBCStatement; // The driver is built to handle SELECT statements only. return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { @@ -936,8 +937,10 @@ SQLRETURN SQLExecute(SQLHSTMT stmt) { SQLRETURN SQLFetch(SQLHSTMT stmt) { LOG_DEBUG("SQLFetch called with stmt: {}", stmt); + using ODBC::ODBCDescriptor; using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); @@ -963,7 +966,9 @@ SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType "SQLGetData called with stmt: {}, recordNumber: {}, cType: {}, " "dataPtr: {}, bufferLength: {}, indicatorPtr: {}", stmt, recordNumber, cType, dataPtr, bufferLength, fmt::ptr(indicatorPtr)); + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); return statement->GetData(recordNumber, cType, dataPtr, bufferLength, indicatorPtr); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc index 3310709e901..b725711f56e 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc @@ -28,58 +28,58 @@ namespace arrow::flight::sql::odbc { +#ifdef SQL_ATTR_ASYNC_DBC_EVENT TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAsyncDbcEventUnsupported) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_EVENT SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_EVENT, 0, 0); EXPECT_EQ(ret, SQL_ERROR); // Driver Manager on Windows returns error code HY118 VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY118); -#endif this->disconnect(); } +#endif +#ifdef SQL_ATTR_ASYNC_ENABLE TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncEnableUnsupported) { this->connect(); -#ifdef SQL_ATTR_ASYNC_ENABLE SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_ENABLE, 0, 0); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); -#endif this->disconnect(); } +#endif +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncDbcPcCallbackUnsupported) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCALLBACK, 0, 0); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); -#endif this->disconnect(); } +#endif +#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAyncDbcPcContextUnsupported) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCONTEXT, 0, 0); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); -#endif this->disconnect(); } +#endif TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrAutoIpdReadOnly) { this->connect(); @@ -105,18 +105,18 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrConnectionDeadReadOnly) { this->disconnect(); } +#ifdef SQL_ATTR_DBC_INFO_TOKEN TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrDbcInfoTokenUnsupported) { this->connect(); -#ifdef SQL_ATTR_DBC_INFO_TOKEN SQLRETURN ret = SQLSetConnectAttr(this->conn, SQL_ATTR_DBC_INFO_TOKEN, 0, 0); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HYC00); -#endif this->disconnect(); } +#endif TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrEnlistInDtcUnsupported) { this->connect(); @@ -218,20 +218,20 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetConnectAttrTxnIsolationUnsupported) this->disconnect(); } +#ifdef SQL_ATTR_DBC_INFO_TOKEN TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrDbcInfoTokenSetOnly) { this->connect(); -#ifdef SQL_ATTR_DBC_INFO_TOKEN // Verify that set-only attribute cannot be read SQLPOINTER ptr = NULL; SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_DBC_INFO_TOKEN, ptr, 0, 0); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY092); -#endif this->disconnect(); } +#endif TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrOdbcCursorsDMOnly) { this->connect(); @@ -318,11 +318,11 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrTxnIsolationUnsupported) this->disconnect(); } +#ifdef SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcFunctionsEnableUnsupported) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE // Verifies that the Windows driver manager returns HY114 for unsupported functionality SQLUINTEGER enable; SQLRETURN ret = @@ -330,54 +330,54 @@ TYPED_TEST(FlightSQLODBCTestBase, EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY114); -#endif this->disconnect(); } +#endif // Tests for supported attributes +#ifdef SQL_ATTR_ASYNC_DBC_EVENT TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcEventDefault) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_EVENT SQLPOINTER ptr = NULL; SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_EVENT, ptr, 0, 0); EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_EQ(ptr, reinterpret_cast(NULL)); -#endif this->disconnect(); } +#endif +#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcPcallbackDefault) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_PCALLBACK SQLPOINTER ptr = NULL; SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCALLBACK, ptr, 0, 0); EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_EQ(ptr, reinterpret_cast(NULL)); -#endif this->disconnect(); } +#endif +#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncDbcPcontextDefault) { this->connect(); -#ifdef SQL_ATTR_ASYNC_DBC_PCONTEXT SQLPOINTER ptr = NULL; SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_ASYNC_DBC_PCONTEXT, ptr, 0, 0); EXPECT_EQ(ret, SQL_SUCCESS); EXPECT_EQ(ptr, reinterpret_cast(NULL)); -#endif this->disconnect(); } +#endif TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetConnectAttrAsyncEnableDefault) { this->connect(); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc index 1585e5ad75e..82774a55d8c 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc @@ -125,7 +125,7 @@ void validateNotEmptySQLWCHAR(SQLHDBC connection, SQLUSMALLINT infoType, // Driver Information -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACTIVE_ENVIRONMENTS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoActiveEnvironments) { this->connect(); validate(this->conn, SQL_ACTIVE_ENVIRONMENTS, static_cast(0)); @@ -133,18 +133,18 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACTIVE_ENVIRONMENTS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_DBC_FUNCTIONS) { +#ifdef SQL_ASYNC_DBC_FUNCTIONS +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAsyncDbcFunctions) { this->connect(); -#ifdef SQL_ASYNC_DBC_FUNCTIONS validate(this->conn, SQL_ASYNC_DBC_FUNCTIONS, static_cast(SQL_ASYNC_DBC_NOT_CAPABLE)); -#endif this->disconnect(); } +#endif -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_MODE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAsyncMode) { this->connect(); validate(this->conn, SQL_ASYNC_MODE, static_cast(SQL_AM_NONE)); @@ -152,18 +152,18 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_MODE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ASYNC_NOTIFICATION) { +#ifdef SQL_ASYNC_NOTIFICATION +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAsyncNotification) { this->connect(); -#ifdef SQL_ASYNC_NOTIFICATION validate(this->conn, SQL_ASYNC_NOTIFICATION, static_cast(SQL_ASYNC_NOTIFICATION_NOT_CAPABLE)); -#endif this->disconnect(); } +#endif -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_ROW_COUNT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoBatchRowCount) { this->connect(); validate(this->conn, SQL_BATCH_ROW_COUNT, static_cast(0)); @@ -171,7 +171,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_ROW_COUNT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_SUPPORT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoBatchSupport) { this->connect(); validate(this->conn, SQL_BATCH_SUPPORT, static_cast(0)); @@ -179,7 +179,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BATCH_SUPPORT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDataSourceName) { this->connect(); validate(this->conn, SQL_DATA_SOURCE_NAME, L""); @@ -187,7 +187,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_AWARE_POOLING_SUPPORTED) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverAwarePoolingSupported) { // A driver does not need to implement SQL_DRIVER_AWARE_POOLING_SUPPORTED and the // Driver Manager will not honor to the driver's return value. this->connect(); @@ -199,7 +199,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_AWARE_POOLING_SUPPORTED) { } // These information types are implemented by the Driver Manager alone. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDBC) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHdbc) { this->connect(); // Value returned from driver manager is the connection address @@ -209,8 +209,9 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDBC) { } // These information types are implemented by the Driver Manager alone. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDESC) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHdesc) { // TODO This is failing due to no descriptor being created + // enable after SQL_HANDLE_DESC is supported GTEST_SKIP(); this->connect(); @@ -220,7 +221,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HDESC) { } // These information types are implemented by the Driver Manager alone. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HENV) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHenv) { this->connect(); // Value returned from driver manager is the env address @@ -230,7 +231,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HENV) { } // These information types are implemented by the Driver Manager alone. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HLIB) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHlib) { this->connect(); validateGreaterThan(this->conn, SQL_DRIVER_HLIB, static_cast(0)); @@ -239,18 +240,19 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HLIB) { } // These information types are implemented by the Driver Manager alone. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_HSTMT) { - // TODO This is failing due to no statement being created - // This should run after SQLGetStmtAttr is implemented - GTEST_SKIP(); +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHstmt) { this->connect(); - validate(this->conn, SQL_DRIVER_HSTMT, static_cast(0)); + // Value returned from driver manager is the stmt address + SQLHSTMT local_stmt = this->stmt; + SQLRETURN ret = SQLGetInfo(this->conn, SQL_DRIVER_HSTMT, &local_stmt, 0, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_GT(local_stmt, static_cast(0)); this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverName) { this->connect(); validate(this->conn, SQL_DRIVER_NAME, L"Arrow Flight ODBC Driver"); @@ -258,7 +260,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_ODBC_VER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverOdbcVer) { this->connect(); validate(this->conn, SQL_DRIVER_ODBC_VER, L"03.80"); @@ -266,7 +268,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_ODBC_VER) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_VER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverVer) { this->connect(); validate(this->conn, SQL_DRIVER_VER, L"00.09.0000.0"); @@ -274,7 +276,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DRIVER_VER) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES1) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDynamicCursorAttributes1) { this->connect(); validate(this->conn, SQL_DYNAMIC_CURSOR_ATTRIBUTES1, static_cast(0)); @@ -282,7 +284,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES1) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES2) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDynamicCursorAttributes2) { this->connect(); validate(this->conn, SQL_DYNAMIC_CURSOR_ATTRIBUTES2, static_cast(0)); @@ -290,7 +292,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DYNAMIC_CURSOR_ATTRIBUTES2) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoForwardOnlyCursorAttributes1) { this->connect(); validate(this->conn, SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1, @@ -299,7 +301,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoForwardOnlyCursorAttributes2) { this->connect(); validate(this->conn, SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2, @@ -308,7 +310,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FILE_USAGE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoFileUsage) { this->connect(); validate(this->conn, SQL_FILE_USAGE, static_cast(SQL_FILE_NOT_SUPPORTED)); @@ -316,7 +318,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_FILE_USAGE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_GETDATA_EXTENSIONS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoGetDataExtensions) { this->connect(); validate(this->conn, SQL_GETDATA_EXTENSIONS, @@ -325,7 +327,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_GETDATA_EXTENSIONS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INFO_SCHEMA_VIEWS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSchemaViews) { this->connect(); validate(this->conn, SQL_INFO_SCHEMA_VIEWS, @@ -334,7 +336,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INFO_SCHEMA_VIEWS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES1) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoKeysetCursorAttributes1) { this->connect(); validate(this->conn, SQL_KEYSET_CURSOR_ATTRIBUTES1, static_cast(0)); @@ -342,7 +344,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES1) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES2) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoKeysetCursorAttributes2) { this->connect(); validate(this->conn, SQL_KEYSET_CURSOR_ATTRIBUTES2, static_cast(0)); @@ -350,7 +352,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYSET_CURSOR_ATTRIBUTES2) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ASYNC_CONCURRENT_STATEMENTS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxAsyncConcurrentStatements) { this->connect(); validate(this->conn, SQL_MAX_ASYNC_CONCURRENT_STATEMENTS, static_cast(0)); @@ -358,7 +360,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ASYNC_CONCURRENT_STATEMENTS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CONCURRENT_ACTIVITIES) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxConcurrentActivities) { this->connect(); validate(this->conn, SQL_MAX_CONCURRENT_ACTIVITIES, static_cast(0)); @@ -366,7 +368,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CONCURRENT_ACTIVITIES) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_DRIVER_CONNECTIONS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxDriverConnections) { this->connect(); validate(this->conn, SQL_MAX_DRIVER_CONNECTIONS, static_cast(0)); @@ -374,7 +376,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_DRIVER_CONNECTIONS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_INTERFACE_CONFORMANCE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoOdbcInterfaceConformance) { this->connect(); validate(this->conn, SQL_ODBC_INTERFACE_CONFORMANCE, @@ -385,7 +387,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_INTERFACE_CONFORMANCE) { // case SQL_ODBC_STANDARD_CLI_CONFORMANCE: - mentioned in SQLGetInfo spec with no // description and there is no constant for this. -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_STANDARD_CLI_CONFORMANCE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoOdbcStandardCliConformance) { // Type commented out in odbc_connection.cc GTEST_SKIP(); this->connect(); @@ -397,7 +399,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_STANDARD_CLI_CONFORMANCE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_VER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoOdbcVer) { // This is implemented only in the Driver Manager. this->connect(); @@ -406,7 +408,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ODBC_VER) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_ROW_COUNTS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoParamArrayRowCounts) { this->connect(); validate(this->conn, SQL_PARAM_ARRAY_ROW_COUNTS, @@ -415,7 +417,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_ROW_COUNTS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_SELECTS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoParamArraySelects) { this->connect(); validate(this->conn, SQL_PARAM_ARRAY_SELECTS, @@ -424,7 +426,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PARAM_ARRAY_SELECTS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ROW_UPDATES) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoRowUpdates) { this->connect(); validate(this->conn, SQL_ROW_UPDATES, L"N"); @@ -432,7 +434,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ROW_UPDATES) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SEARCH_PATTERN_ESCAPE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSearchPatternEscape) { this->connect(); validate(this->conn, SQL_SEARCH_PATTERN_ESCAPE, L"\\"); @@ -440,7 +442,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SEARCH_PATTERN_ESCAPE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SERVER_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoServerName) { this->connect(); validateNotEmptySQLWCHAR(this->conn, SQL_SERVER_NAME, false); @@ -448,7 +450,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SERVER_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES1) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoStaticCursorAttributes1) { this->connect(); validate(this->conn, SQL_STATIC_CURSOR_ATTRIBUTES1, static_cast(0)); @@ -456,7 +458,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES1) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES2) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoStaticCursorAttributes2) { this->connect(); validate(this->conn, SQL_STATIC_CURSOR_ATTRIBUTES2, static_cast(0)); @@ -466,7 +468,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_STATIC_CURSOR_ATTRIBUTES2) { // DBMS Product Information -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATABASE_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDatabaseName) { this->connect(); validate(this->conn, SQL_DATABASE_NAME, L""); @@ -474,7 +476,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATABASE_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDbmsName) { this->connect(); validateNotEmptySQLWCHAR(this->conn, SQL_DBMS_NAME, false); @@ -482,7 +484,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_VER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDbmsVer) { this->connect(); validateNotEmptySQLWCHAR(this->conn, SQL_DBMS_VER, false); @@ -492,7 +494,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DBMS_VER) { // Data Source Information -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_PROCEDURES) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAccessibleProcedures) { this->connect(); validate(this->conn, SQL_ACCESSIBLE_PROCEDURES, L"N"); @@ -500,7 +502,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_PROCEDURES) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_TABLES) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAccessibleTables) { this->connect(); validate(this->conn, SQL_ACCESSIBLE_TABLES, L"Y"); @@ -508,7 +510,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ACCESSIBLE_TABLES) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BOOKMARK_PERSISTENCE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoBookmarkPersistence) { this->connect(); validate(this->conn, SQL_BOOKMARK_PERSISTENCE, static_cast(0)); @@ -516,7 +518,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_BOOKMARK_PERSISTENCE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_TERM) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogTerm) { this->connect(); validate(this->conn, SQL_CATALOG_TERM, L""); @@ -524,7 +526,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_TERM) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLLATION_SEQ) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCollationSeq) { this->connect(); validate(this->conn, SQL_COLLATION_SEQ, L""); @@ -532,7 +534,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLLATION_SEQ) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONCAT_NULL_BEHAVIOR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConcatNullBehavior) { this->connect(); validate(this->conn, SQL_CONCAT_NULL_BEHAVIOR, static_cast(SQL_CB_NULL)); @@ -540,7 +542,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONCAT_NULL_BEHAVIOR) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_COMMIT_BEHAVIOR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCursorCommitBehavior) { this->connect(); validate(this->conn, SQL_CURSOR_COMMIT_BEHAVIOR, @@ -549,7 +551,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_COMMIT_BEHAVIOR) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_ROLLBACK_BEHAVIOR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCursorRollbackBehavior) { this->connect(); validate(this->conn, SQL_CURSOR_ROLLBACK_BEHAVIOR, @@ -558,7 +560,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_ROLLBACK_BEHAVIOR) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_SENSITIVITY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCursorSensitivity) { this->connect(); validate(this->conn, SQL_CURSOR_SENSITIVITY, static_cast(SQL_UNSPECIFIED)); @@ -566,7 +568,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CURSOR_SENSITIVITY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_READ_ONLY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDataSourceReadOnly) { this->connect(); validate(this->conn, SQL_DATA_SOURCE_READ_ONLY, L"N"); @@ -574,7 +576,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DATA_SOURCE_READ_ONLY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DEFAULT_TXN_ISOLATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDefaultTxnIsolation) { this->connect(); validate(this->conn, SQL_DEFAULT_TXN_ISOLATION, static_cast(0)); @@ -582,7 +584,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DEFAULT_TXN_ISOLATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DESCRIBE_PARAMETER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDescribeParameter) { this->connect(); validate(this->conn, SQL_DESCRIBE_PARAMETER, L"N"); @@ -590,7 +592,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DESCRIBE_PARAMETER) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULT_RESULT_SETS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultResultSets) { this->connect(); validate(this->conn, SQL_MULT_RESULT_SETS, L"N"); @@ -598,7 +600,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULT_RESULT_SETS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULTIPLE_ACTIVE_TXN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultipleActiveTxn) { this->connect(); validate(this->conn, SQL_MULTIPLE_ACTIVE_TXN, L"N"); @@ -606,7 +608,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MULTIPLE_ACTIVE_TXN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_NEED_LONG_DATA_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoNeedLongDataLen) { this->connect(); validate(this->conn, SQL_NEED_LONG_DATA_LEN, L"N"); @@ -614,7 +616,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_NEED_LONG_DATA_LEN) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NULL_COLLATION) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoNullCollation) { this->connect(); validate(this->conn, SQL_NULL_COLLATION, static_cast(SQL_NC_START)); @@ -622,7 +624,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NULL_COLLATION) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_PROCEDURE_TERM) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoProcedureTerm) { this->connect(); validate(this->conn, SQL_PROCEDURE_TERM, L""); @@ -630,7 +632,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_PROCEDURE_TERM) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCHEMA_TERM) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSchemaTerm) { this->connect(); validate(this->conn, SQL_SCHEMA_TERM, L"schema"); @@ -638,7 +640,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCHEMA_TERM) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCROLL_OPTIONS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoScrollOptions) { this->connect(); validate(this->conn, SQL_SCROLL_OPTIONS, static_cast(SQL_SO_FORWARD_ONLY)); @@ -646,7 +648,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SCROLL_OPTIONS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TABLE_TERM) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTableTerm) { this->connect(); validate(this->conn, SQL_TABLE_TERM, L"table"); @@ -654,7 +656,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TABLE_TERM) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_CAPABLE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTxnCapable) { this->connect(); validate(this->conn, SQL_TXN_CAPABLE, static_cast(SQL_TC_NONE)); @@ -662,7 +664,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_CAPABLE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_ISOLATION_OPTION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTxnIsolationOption) { this->connect(); validate(this->conn, SQL_TXN_ISOLATION_OPTION, static_cast(0)); @@ -670,7 +672,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TXN_ISOLATION_OPTION) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_USER_NAME) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoUserName) { this->connect(); validate(this->conn, SQL_USER_NAME, L""); @@ -680,7 +682,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_USER_NAME) { // Supported SQL -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_AGGREGATE_FUNCTIONS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAggregateFunctions) { this->connect(); validate( @@ -691,7 +693,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_AGGREGATE_FUNCTIONS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_DOMAIN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAlterDomain) { this->connect(); validate(this->conn, SQL_ALTER_DOMAIN, static_cast(0)); @@ -699,7 +701,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_DOMAIN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_SCHEMA) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAlterSchema) { // Type commented out in odbc_connection.cc GTEST_SKIP(); this->connect(); @@ -710,7 +712,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_SCHEMA) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_TABLE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAlterTable) { this->connect(); validate(this->conn, SQL_ALTER_TABLE, static_cast(0)); @@ -718,7 +720,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ALTER_TABLE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ANSI_SQL_DATETIME_LITERALS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAnsiSqlDatetimeLiterals) { // Type commented out in odbc_connection.cc GTEST_SKIP(); this->connect(); @@ -729,7 +731,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_ANSI_SQL_DATETIME_LITERALS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_LOCATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogLocation) { this->connect(); validate(this->conn, SQL_CATALOG_LOCATION, static_cast(0)); @@ -737,7 +739,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_LOCATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogName) { this->connect(); validate(this->conn, SQL_CATALOG_NAME, L"N"); @@ -745,7 +747,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME_SEPARATOR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogNameSeparator) { this->connect(); validate(this->conn, SQL_CATALOG_NAME_SEPARATOR, L""); @@ -753,7 +755,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CATALOG_NAME_SEPARATOR) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CATALOG_USAGE) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoCatalogUsage) { this->connect(); validate(this->conn, SQL_CATALOG_USAGE, static_cast(0)); @@ -761,7 +763,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CATALOG_USAGE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLUMN_ALIAS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoColumnAlias) { this->connect(); validate(this->conn, SQL_COLUMN_ALIAS, L"Y"); @@ -769,7 +771,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_COLUMN_ALIAS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CORRELATION_NAME) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoCorrelationName) { this->connect(); validate(this->conn, SQL_CORRELATION_NAME, static_cast(SQL_CN_NONE)); @@ -777,7 +779,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CORRELATION_NAME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_ASSERTION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCreateAssertion) { this->connect(); validate(this->conn, SQL_CREATE_ASSERTION, static_cast(0)); @@ -785,7 +787,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_ASSERTION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_CHARACTER_SET) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCreateCharacterSet) { this->connect(); validate(this->conn, SQL_CREATE_CHARACTER_SET, static_cast(0)); @@ -793,7 +795,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_CHARACTER_SET) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_COLLATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCreateCollation) { this->connect(); validate(this->conn, SQL_CREATE_COLLATION, static_cast(0)); @@ -801,7 +803,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_COLLATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_DOMAIN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCreateDomain) { this->connect(); validate(this->conn, SQL_CREATE_DOMAIN, static_cast(0)); @@ -809,7 +811,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_DOMAIN) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_SCHEMA) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoCreateSchema) { this->connect(); validate(this->conn, SQL_CREATE_SCHEMA, static_cast(1)); @@ -817,7 +819,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_SCHEMA) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_TABLE) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoCreateTable) { this->connect(); validate(this->conn, SQL_CREATE_TABLE, static_cast(1)); @@ -825,7 +827,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CREATE_TABLE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_TRANSLATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCreateTranslation) { this->connect(); validate(this->conn, SQL_CREATE_TRANSLATION, static_cast(0)); @@ -833,7 +835,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CREATE_TRANSLATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DDL_INDEX) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDdlIndex) { this->connect(); validate(this->conn, SQL_DDL_INDEX, static_cast(0)); @@ -841,7 +843,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DDL_INDEX) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_ASSERTION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropAssertion) { this->connect(); validate(this->conn, SQL_DROP_ASSERTION, static_cast(0)); @@ -849,7 +851,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_ASSERTION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_CHARACTER_SET) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropCharacterSet) { this->connect(); validate(this->conn, SQL_DROP_CHARACTER_SET, static_cast(0)); @@ -857,7 +859,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_CHARACTER_SET) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_COLLATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropCollation) { this->connect(); validate(this->conn, SQL_DROP_COLLATION, static_cast(0)); @@ -865,7 +867,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_COLLATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_DOMAIN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropDomain) { this->connect(); validate(this->conn, SQL_DROP_DOMAIN, static_cast(0)); @@ -873,7 +875,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_DOMAIN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_SCHEMA) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropSchema) { this->connect(); validate(this->conn, SQL_DROP_SCHEMA, static_cast(0)); @@ -881,7 +883,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_SCHEMA) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TABLE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropTable) { this->connect(); validate(this->conn, SQL_DROP_TABLE, static_cast(0)); @@ -889,7 +891,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TABLE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TRANSLATION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropTranslation) { this->connect(); validate(this->conn, SQL_DROP_TRANSLATION, static_cast(0)); @@ -897,7 +899,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_TRANSLATION) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_VIEW) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropView) { this->connect(); validate(this->conn, SQL_DROP_VIEW, static_cast(0)); @@ -905,7 +907,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_DROP_VIEW) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_EXPRESSIONS_IN_ORDERBY) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoExpressionsInOrderby) { this->connect(); validate(this->conn, SQL_EXPRESSIONS_IN_ORDERBY, L"N"); @@ -913,7 +915,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_EXPRESSIONS_IN_ORDERBY) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_GROUP_BY) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoGroupBy) { this->connect(); validate(this->conn, SQL_GROUP_BY, @@ -922,7 +924,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_GROUP_BY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_CASE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIdentifierCase) { this->connect(); validate(this->conn, SQL_IDENTIFIER_CASE, static_cast(SQL_IC_MIXED)); @@ -930,7 +932,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_CASE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_QUOTE_CHAR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIdentifierQuoteChar) { this->connect(); validate(this->conn, SQL_IDENTIFIER_QUOTE_CHAR, L"\""); @@ -938,7 +940,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_IDENTIFIER_QUOTE_CHAR) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INDEX_KEYWORDS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIndexKeywords) { this->connect(); validate(this->conn, SQL_INDEX_KEYWORDS, static_cast(SQL_IK_NONE)); @@ -946,7 +948,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INDEX_KEYWORDS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INSERT_STATEMENT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoInsertStatement) { this->connect(); validate(this->conn, SQL_INSERT_STATEMENT, @@ -956,7 +958,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INSERT_STATEMENT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INTEGRITY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIntegrity) { this->connect(); validate(this->conn, SQL_INTEGRITY, L"N"); @@ -964,7 +966,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_INTEGRITY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYWORDS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoKeywords) { this->connect(); validateNotEmptySQLWCHAR(this->conn, SQL_KEYWORDS, true); @@ -972,7 +974,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_KEYWORDS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_LIKE_ESCAPE_CLAUSE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoLikeEscapeClause) { this->connect(); validate(this->conn, SQL_LIKE_ESCAPE_CLAUSE, L"Y"); @@ -980,7 +982,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_LIKE_ESCAPE_CLAUSE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NON_NULLABLE_COLUMNS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoNonNullableColumns) { this->connect(); validate(this->conn, SQL_NON_NULLABLE_COLUMNS, static_cast(SQL_NNC_NULL)); @@ -988,7 +990,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NON_NULLABLE_COLUMNS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OJ_CAPABILITIES) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOjCapabilities) { this->connect(); validate(this->conn, SQL_OJ_CAPABILITIES, @@ -997,7 +999,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OJ_CAPABILITIES) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_ORDER_BY_COLUMNS_IN_SELECT) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOrderByColumnsInSelect) { this->connect(); validate(this->conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, L"Y"); @@ -1005,7 +1007,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_ORDER_BY_COLUMNS_IN_SELECT) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OUTER_JOINS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOuterJoins) { this->connect(); validate(this->conn, SQL_OUTER_JOINS, L"N"); @@ -1013,7 +1015,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_OUTER_JOINS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PROCEDURES) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoProcedures) { this->connect(); validate(this->conn, SQL_PROCEDURES, L"N"); @@ -1021,7 +1023,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_PROCEDURES) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_QUOTED_IDENTIFIER_CASE) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoQuotedIdentifierCase) { this->connect(); validate(this->conn, SQL_QUOTED_IDENTIFIER_CASE, @@ -1030,7 +1032,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_QUOTED_IDENTIFIER_CASE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SCHEMA_USAGE) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoSchemaUsage) { this->connect(); validate(this->conn, SQL_SCHEMA_USAGE, static_cast(SQL_SU_DML_STATEMENTS)); @@ -1038,7 +1040,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SCHEMA_USAGE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SPECIAL_CHARACTERS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSpecialCharacters) { this->connect(); validate(this->conn, SQL_SPECIAL_CHARACTERS, L""); @@ -1046,7 +1048,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SPECIAL_CHARACTERS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SQL_CONFORMANCE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSqlConformance) { this->connect(); validate(this->conn, SQL_SQL_CONFORMANCE, static_cast(SQL_SC_SQL92_ENTRY)); @@ -1054,7 +1056,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_SQL_CONFORMANCE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SUBQUERIES) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoSubqueries) { this->connect(); validate(this->conn, SQL_SUBQUERIES, @@ -1064,7 +1066,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SUBQUERIES) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_UNION) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoUnion) { this->connect(); validate(this->conn, SQL_UNION, @@ -1075,7 +1077,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_UNION) { // SQL Limits -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_BINARY_LITERAL_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxBinaryLiteralLen) { this->connect(); validate(this->conn, SQL_MAX_BINARY_LITERAL_LEN, static_cast(0)); @@ -1083,7 +1085,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_BINARY_LITERAL_LEN) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CATALOG_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxCatalogNameLen) { this->connect(); validate(this->conn, SQL_MAX_CATALOG_NAME_LEN, static_cast(0)); @@ -1091,7 +1093,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CATALOG_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CHAR_LITERAL_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxCharLiteralLen) { this->connect(); validate(this->conn, SQL_MAX_CHAR_LITERAL_LEN, static_cast(0)); @@ -1099,7 +1101,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_CHAR_LITERAL_LEN) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_COLUMN_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxColumnNameLen) { this->connect(); validate(this->conn, SQL_MAX_COLUMN_NAME_LEN, static_cast(0)); @@ -1107,7 +1109,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_COLUMN_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_GROUP_BY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxColumnsInGroupBy) { this->connect(); validate(this->conn, SQL_MAX_COLUMNS_IN_GROUP_BY, static_cast(0)); @@ -1115,7 +1117,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_GROUP_BY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_INDEX) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxColumnsInIndex) { this->connect(); validate(this->conn, SQL_MAX_COLUMNS_IN_INDEX, static_cast(0)); @@ -1123,7 +1125,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_INDEX) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_ORDER_BY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxColumnsInOrderBy) { this->connect(); validate(this->conn, SQL_MAX_COLUMNS_IN_ORDER_BY, static_cast(0)); @@ -1131,7 +1133,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_ORDER_BY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_SELECT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxColumnsInSelect) { this->connect(); validate(this->conn, SQL_MAX_COLUMNS_IN_SELECT, static_cast(0)); @@ -1139,7 +1141,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_SELECT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_TABLE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxColumnsInTable) { this->connect(); validate(this->conn, SQL_MAX_COLUMNS_IN_TABLE, static_cast(0)); @@ -1147,7 +1149,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_COLUMNS_IN_TABLE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CURSOR_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxCursorNameLen) { this->connect(); validate(this->conn, SQL_MAX_CURSOR_NAME_LEN, static_cast(0)); @@ -1155,7 +1157,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_CURSOR_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_IDENTIFIER_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxIdentifierLen) { this->connect(); validate(this->conn, SQL_MAX_IDENTIFIER_LEN, static_cast(65535)); @@ -1163,7 +1165,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_IDENTIFIER_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_INDEX_SIZE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxIndexSize) { this->connect(); validate(this->conn, SQL_MAX_INDEX_SIZE, static_cast(0)); @@ -1171,7 +1173,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_INDEX_SIZE) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_PROCEDURE_NAME_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxProcedureNameLen) { this->connect(); validate(this->conn, SQL_MAX_PROCEDURE_NAME_LEN, static_cast(0)); @@ -1179,7 +1181,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_PROCEDURE_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ROW_SIZE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxRowSize) { this->connect(); validate(this->conn, SQL_MAX_ROW_SIZE, L""); @@ -1187,7 +1189,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_ROW_SIZE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_ROW_SIZE_INCLUDES_LONG) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxRowSizeIncludesLong) { this->connect(); validate(this->conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, L"N"); @@ -1195,7 +1197,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_ROW_SIZE_INCLUDES_LONG) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_SCHEMA_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxSchemaNameLen) { this->connect(); validate(this->conn, SQL_MAX_SCHEMA_NAME_LEN, static_cast(0)); @@ -1203,7 +1205,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_SCHEMA_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_STATEMENT_LEN) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxStatementLen) { this->connect(); validate(this->conn, SQL_MAX_STATEMENT_LEN, static_cast(0)); @@ -1211,7 +1213,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_STATEMENT_LEN) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_TABLE_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxTableNameLen) { this->connect(); validate(this->conn, SQL_MAX_TABLE_NAME_LEN, static_cast(0)); @@ -1219,7 +1221,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_TABLE_NAME_LEN) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_TABLES_IN_SELECT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxTablesInSelect) { this->connect(); validate(this->conn, SQL_MAX_TABLES_IN_SELECT, static_cast(0)); @@ -1227,7 +1229,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_MAX_TABLES_IN_SELECT) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_USER_NAME_LEN) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxUserNameLen) { this->connect(); validate(this->conn, SQL_MAX_USER_NAME_LEN, static_cast(0)); @@ -1237,7 +1239,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_MAX_USER_NAME_LEN) { // Scalar Function Information -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FUNCTIONS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertFunctions) { this->connect(); validate(this->conn, SQL_CONVERT_FUNCTIONS, static_cast(0)); @@ -1245,7 +1247,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FUNCTIONS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NUMERIC_FUNCTIONS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoNumericFunctions) { this->connect(); validate(this->conn, SQL_NUMERIC_FUNCTIONS, static_cast(4058942)); @@ -1253,7 +1255,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_NUMERIC_FUNCTIONS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_STRING_FUNCTIONS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoStringFunctions) { this->connect(); validate(this->conn, SQL_STRING_FUNCTIONS, @@ -1263,7 +1265,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_STRING_FUNCTIONS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SYSTEM_FUNCTIONS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoSystemFunctions) { this->connect(); validate(this->conn, SQL_SYSTEM_FUNCTIONS, @@ -1272,7 +1274,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_SYSTEM_FUNCTIONS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_ADD_INTERVALS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTimedateAddIntervals) { this->connect(); validate(this->conn, SQL_TIMEDATE_ADD_INTERVALS, @@ -1284,7 +1286,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_ADD_INTERVALS) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_DIFF_INTERVALS) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTimedateDiffIntervals) { this->connect(); validate(this->conn, SQL_TIMEDATE_DIFF_INTERVALS, @@ -1296,7 +1298,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_TIMEDATE_DIFF_INTERVALS) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_TIMEDATE_FUNCTIONS) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoTimedateFunctions) { this->connect(); validate(this->conn, SQL_TIMEDATE_FUNCTIONS, @@ -1314,7 +1316,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_TIMEDATE_FUNCTIONS) { // Conversion Information -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BIGINT) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertBigint) { this->connect(); validate(this->conn, SQL_CONVERT_BIGINT, static_cast(8)); @@ -1322,7 +1324,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BIGINT) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BINARY) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertBinary) { this->connect(); validate(this->conn, SQL_CONVERT_BINARY, static_cast(0)); @@ -1330,7 +1332,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_BINARY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_BIT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertBit) { this->connect(); validate(this->conn, SQL_CONVERT_BIT, static_cast(0)); @@ -1338,7 +1340,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_BIT) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_CHAR) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertChar) { this->connect(); validate(this->conn, SQL_CONVERT_CHAR, static_cast(0)); @@ -1346,7 +1348,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_CHAR) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DATE) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertDate) { this->connect(); validate(this->conn, SQL_CONVERT_DATE, static_cast(0)); @@ -1354,7 +1356,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DATE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DECIMAL) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertDecimal) { this->connect(); validate(this->conn, SQL_CONVERT_DECIMAL, static_cast(0)); @@ -1362,7 +1364,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_DECIMAL) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_DOUBLE) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertDouble) { this->connect(); validate(this->conn, SQL_CONVERT_DOUBLE, static_cast(0)); @@ -1370,7 +1372,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_DOUBLE) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FLOAT) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertFloat) { this->connect(); validate(this->conn, SQL_CONVERT_FLOAT, static_cast(0)); @@ -1378,7 +1380,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_FLOAT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTEGER) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertInteger) { this->connect(); validate(this->conn, SQL_CONVERT_INTEGER, static_cast(0)); @@ -1386,7 +1388,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTEGER) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_INTERVAL_DAY_TIME) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertIntervalDayTime) { this->connect(); validate(this->conn, SQL_CONVERT_INTERVAL_DAY_TIME, static_cast(0)); @@ -1394,7 +1396,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_INTERVAL_DAY_TIME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTERVAL_YEAR_MONTH) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertIntervalYearMonth) { this->connect(); validate(this->conn, SQL_CONVERT_INTERVAL_YEAR_MONTH, static_cast(0)); @@ -1402,7 +1404,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_INTERVAL_YEAR_MONTH) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARBINARY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertLongvarbinary) { this->connect(); validate(this->conn, SQL_CONVERT_LONGVARBINARY, static_cast(0)); @@ -1410,7 +1412,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARBINARY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARCHAR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertLongvarchar) { this->connect(); validate(this->conn, SQL_CONVERT_LONGVARCHAR, static_cast(0)); @@ -1418,7 +1420,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_LONGVARCHAR) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_NUMERIC) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoConvertNumeric) { this->connect(); validate(this->conn, SQL_CONVERT_NUMERIC, static_cast(0)); @@ -1426,7 +1428,7 @@ TEST_F(FlightSQLODBCMockTestBase, Test_SQL_CONVERT_NUMERIC) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_REAL) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertReal) { this->connect(); validate(this->conn, SQL_CONVERT_REAL, static_cast(0)); @@ -1434,7 +1436,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_REAL) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_SMALLINT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertSmallint) { this->connect(); validate(this->conn, SQL_CONVERT_SMALLINT, static_cast(0)); @@ -1442,7 +1444,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_SMALLINT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIME) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertTime) { this->connect(); validate(this->conn, SQL_CONVERT_TIME, static_cast(0)); @@ -1450,7 +1452,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIME) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIMESTAMP) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertTimestamp) { this->connect(); validate(this->conn, SQL_CONVERT_TIMESTAMP, static_cast(0)); @@ -1458,7 +1460,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TIMESTAMP) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TINYINT) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertTinyint) { this->connect(); validate(this->conn, SQL_CONVERT_TINYINT, static_cast(0)); @@ -1466,7 +1468,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_TINYINT) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARBINARY) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertVarbinary) { this->connect(); validate(this->conn, SQL_CONVERT_VARBINARY, static_cast(0)); @@ -1474,7 +1476,7 @@ TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARBINARY) { this->disconnect(); } -TYPED_TEST(FlightSQLODBCTestBase, Test_SQL_CONVERT_VARCHAR) { +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoConvertVarchar) { this->connect(); validate(this->conn, SQL_CONVERT_VARCHAR, static_cast(0)); From a08babb230a02f17f66ca3da84af1e6153b1555f Mon Sep 17 00:00:00 2001 From: rscales Date: Mon, 14 Jul 2025 11:34:35 -0700 Subject: [PATCH 28/74] Implement SQLGetStmtAttr and SQLSetStmtAttr --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 6 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 27 +- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 2 + .../odbc_impl/odbc_statement.cc | 2 +- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/odbc_test_suite.h | 1 + .../sql/odbc/tests/statement_attr_test.cc | 893 ++++++++++++++++++ 7 files changed, 922 insertions(+), 10 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/statement_attr_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 9afac8ea28e..e7c55d5a73a 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -279,11 +279,7 @@ SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER stringLength) { - LOG_DEBUG( - "SQLSetStmtAttrW called with stmt: {}, attribute: {}, valuePtr: {}, " - "stringLength: {}", - stmt, attribute, valuePtr, stringLength); - return SQL_ERROR; + return arrow::SQLSetStmtAttr(stmt, attribute, valuePtr, stringLength); } SQLRETURN SQL_API SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index d00e5d2fbf1..91121726e8f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -873,19 +873,38 @@ SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePt SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { - using ODBC::ODBCStatement; - // TODO: complete implementation of SQLGetStmtAttrW and write tests LOG_DEBUG( "SQLGetStmtAttrW called with stmt: {}, attribute: {}, valuePtr: {}, " "bufferLength: {}, stringLengthPtr: {}", stmt, attribute, valuePtr, bufferLength, fmt::ptr(stringLengthPtr)); + using ODBC::ODBCStatement; return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); + bool isUnicode = true; + statement->GetStmtAttr(attribute, valuePtr, bufferLength, stringLengthPtr, isUnicode); - // TODO: change GetStmtAttr to return SQLRETURN instead of void, - // and return value from GetStmtAttr instead + + return SQL_SUCCESS; + }); +} + +SQLRETURN SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER stringLength) { + LOG_DEBUG( + "SQLSetStmtAttrW called with stmt: {}, attribute: {}, valuePtr: {}, " + "stringLength: {}", + stmt, attribute, valuePtr, stringLength); + using ODBC::ODBCStatement; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + bool isUnicode = true; + + statement->SetStmtAttr(attribute, valuePtr, stringLength, isUnicode); + return SQL_SUCCESS; }); } diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 07614ec84ba..504a8f545f8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -62,6 +62,8 @@ SQLRETURN SQLGetInfo(SQLHDBC conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePt SQLSMALLINT bufLen, SQLSMALLINT* length); SQLRETURN SQLGetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); +SQLRETURN SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, + SQLINTEGER stringLength); SQLRETURN SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); SQLRETURN SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER textLength); SQLRETURN SQLExecute(SQLHSTMT stmt); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index 49ad527bab0..1714850323a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -639,7 +639,7 @@ void ODBCStatement::SetStmtAttr(SQLINTEGER statementAttribute, SQLPOINTER value, CheckIfAttributeIsSetToOnlyValidValue(value, static_cast(SQL_UB_OFF)); return; case SQL_ATTR_RETRIEVE_DATA: - CheckIfAttributeIsSetToOnlyValidValue(value, static_cast(SQL_TRUE)); + CheckIfAttributeIsSetToOnlyValidValue(value, static_cast(SQL_RD_ON)); return; case SQL_ROWSET_SIZE: SetAttribute(value, m_rowsetSize); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 1a3da7fad31..952b1d36cc8 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -33,6 +33,7 @@ add_arrow_test(flight_sql_odbc_test SOURCES connection_attr_test.cc connection_info_test.cc + statement_attr_test.cc statement_test.cc # Connection test needs to be put last to resolve segfault issue connection_test.cc 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 30017b01937..1dd71e12f73 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 @@ -168,6 +168,7 @@ 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_HY114 = "HY114"; +static constexpr std::string_view error_state_HY017 = "HY017"; static constexpr std::string_view error_state_HY118 = "HY118"; /// Verify ODBC Error State diff --git a/cpp/src/arrow/flight/sql/odbc/tests/statement_attr_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/statement_attr_test.cc new file mode 100644 index 00000000000..aff661dec00 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_attr_test.cc @@ -0,0 +1,893 @@ +// 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/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/statement.h" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +// Helper Functions + +// Validate SQLULEN return value +void validateGetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, + SQLULEN expected_value) { + SQLULEN value = 0; + SQLINTEGER stringLength = 0; + + SQLRETURN ret = + SQLGetStmtAttr(statement, attribute, &value, sizeof(value), &stringLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, expected_value); +} + +// Validate SQLLEN return value +void validateGetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, + SQLLEN expected_value) { + SQLLEN value = 0; + SQLINTEGER stringLength = 0; + + SQLRETURN ret = + SQLGetStmtAttr(statement, attribute, &value, sizeof(value), &stringLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, expected_value); +} + +// Validate SQLPOINTER return value +void validateGetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, + SQLPOINTER expected_value) { + SQLPOINTER value = nullptr; + SQLINTEGER stringLength = 0; + + SQLRETURN ret = + SQLGetStmtAttr(statement, attribute, &value, sizeof(value), &stringLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, expected_value); +} + +// Validate unsigned length SQLULEN return value is greater than +void validateGetStmtAttrGreaterThan(SQLHSTMT statement, SQLINTEGER attribute, + SQLULEN compared_value) { + SQLULEN value = 0; + SQLINTEGER stringLengthPtr; + + SQLRETURN ret = SQLGetStmtAttr(statement, attribute, &value, 0, &stringLengthPtr); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(value, compared_value); +} + +// Validate error return value and code +void validateGetStmtAttrErrorCode(SQLHSTMT statement, SQLINTEGER attribute, + std::string_view error_code) { + SQLULEN value = 0; + SQLINTEGER stringLengthPtr; + + SQLRETURN ret = SQLGetStmtAttr(statement, attribute, &value, 0, &stringLengthPtr); + + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, statement, error_code); +} + +// Validate return value for call to SQLSetStmtAttr with SQLULEN +void validateSetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, SQLULEN new_value) { + SQLINTEGER stringLengthPtr = sizeof(SQLULEN); + + SQLRETURN ret = SQLSetStmtAttr( + statement, attribute, reinterpret_cast(new_value), stringLengthPtr); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +// Validate return value for call to SQLSetStmtAttr with SQLLEN +void validateSetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, SQLLEN new_value) { + SQLINTEGER stringLengthPtr = sizeof(SQLLEN); + + SQLRETURN ret = SQLSetStmtAttr( + statement, attribute, reinterpret_cast(new_value), stringLengthPtr); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +// Validate return value for call to SQLSetStmtAttr with SQLPOINTER +void validateSetStmtAttr(SQLHSTMT statement, SQLINTEGER attribute, SQLPOINTER value) { + SQLRETURN ret = SQLSetStmtAttr(statement, attribute, value, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +// Validate error return value and code +void validateSetStmtAttrErrorCode(SQLHSTMT statement, SQLINTEGER attribute, + SQLULEN new_value, std::string_view error_code) { + SQLINTEGER stringLengthPtr = sizeof(SQLULEN); + + SQLRETURN ret = SQLSetStmtAttr( + statement, attribute, reinterpret_cast(new_value), stringLengthPtr); + + EXPECT_EQ(ret, SQL_ERROR); + + VerifyOdbcErrorState(SQL_HANDLE_STMT, statement, error_code); +} + +// Test Cases + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAppParamDesc) { + this->connect(); + + validateGetStmtAttrGreaterThan(this->stmt, SQL_ATTR_APP_PARAM_DESC, + static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAppRowDesc) { + this->connect(); + + validateGetStmtAttrGreaterThan(this->stmt, SQL_ATTR_APP_ROW_DESC, + static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAsyncEnable) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ASYNC_ENABLE, + static_cast(SQL_ASYNC_ENABLE_OFF)); + + this->disconnect(); +} + +#ifdef SQL_ATTR_ASYNC_STMT_EVENT +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAsyncStmtEventUnsupported) { + this->connect(); + + // Optional feature not implemented + validateGetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_EVENT, error_state_HYC00); + + this->disconnect(); +} +#endif + +#ifdef SQL_ATTR_ASYNC_STMT_PCALLBACK +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAsyncStmtPCCallbackUnsupported) { + this->connect(); + + // Optional feature not implemented + validateGetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_PCALLBACK, + error_state_HYC00); + + this->disconnect(); +} +#endif + +#ifdef SQL_ATTR_ASYNC_STMT_PCONTEXT +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrAsyncStmtPCContextUnsupported) { + this->connect(); + + // Optional feature not implemented + validateGetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_PCONTEXT, + error_state_HYC00); + + this->disconnect(); +} +#endif + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrConcurrency) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_CONCURRENCY, + static_cast(SQL_CONCUR_READ_ONLY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrCursorScrollable) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_CURSOR_SCROLLABLE, + static_cast(SQL_NONSCROLLABLE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrCursorSensitivity) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_CURSOR_SENSITIVITY, + static_cast(SQL_UNSPECIFIED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrCursorType) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_CURSOR_TYPE, + static_cast(SQL_CURSOR_FORWARD_ONLY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrEnableAutoIPD) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ENABLE_AUTO_IPD, + static_cast(SQL_FALSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrFetchBookmarkPointer) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_FETCH_BOOKMARK_PTR, static_cast(NULL)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrIMPParamDesc) { + this->connect(); + + validateGetStmtAttrGreaterThan(this->stmt, SQL_ATTR_IMP_PARAM_DESC, + static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrIMPRowDesc) { + this->connect(); + + validateGetStmtAttrGreaterThan(this->stmt, SQL_ATTR_IMP_ROW_DESC, + static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrKeysetSize) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_KEYSET_SIZE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrMaxLength) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_MAX_LENGTH, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrMaxRows) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_MAX_ROWS, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrMetadataID) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_METADATA_ID, static_cast(SQL_FALSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrNoscan) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_NOSCAN, static_cast(SQL_NOSCAN_OFF)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamBindOffsetPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamBindType) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_BIND_TYPE, + static_cast(SQL_PARAM_BIND_BY_COLUMN)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamOperationPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_OPERATION_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamStatusPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_STATUS_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamsProcessedPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrParamsetSize) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAMSET_SIZE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrQueryTimeout) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_QUERY_TIMEOUT, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRetrieveData) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_RETRIEVE_DATA, + static_cast(SQL_RD_ON)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowArraySize) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_ARRAY_SIZE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowBindOffsetPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowBindType) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_BIND_TYPE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowNumber) { + 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 = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_NUMBER, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowOperationPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_OPERATION_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowStatusPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_STATUS_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowsFetchedPtr) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROWS_FETCHED_PTR, + static_cast(nullptr)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrSimulateCursor) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_SIMULATE_CURSOR, + static_cast(SQL_SC_UNIQUE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrUseBookmarks) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ATTR_USE_BOOKMARKS, + static_cast(SQL_UB_OFF)); + + this->disconnect(); +} + +// This is a pre ODBC 3 attribute +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetStmtAttrRowsetSize) { + this->connect(); + + validateGetStmtAttr(this->stmt, SQL_ROWSET_SIZE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAppParamDesc) { + SQLULEN app_param_desc = 0; + SQLINTEGER stringLengthPtr; + this->connect(); + + SQLRETURN ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, &app_param_desc, 0, + &stringLengthPtr); + + EXPECT_EQ(ret, SQL_SUCCESS); + + validateSetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, static_cast(0)); + + validateSetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, + static_cast(app_param_desc)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAppRowDesc) { + SQLULEN app_row_desc = 0; + SQLINTEGER stringLengthPtr; + this->connect(); + + SQLRETURN ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, &app_row_desc, 0, + &stringLengthPtr); + + EXPECT_EQ(ret, SQL_SUCCESS); + + validateSetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, static_cast(0)); + + validateSetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, + static_cast(app_row_desc)); + + this->disconnect(); +} + +#ifdef SQL_ATTR_ASYNC_ENABLE +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAsyncEnableUnsupported) { + this->connect(); + + // Optional feature not implemented + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_ENABLE, SQL_ASYNC_ENABLE_OFF, + error_state_HYC00); + + this->disconnect(); +} +#endif + +#ifdef SQL_ATTR_ASYNC_STMT_EVENT +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAsyncStmtEventUnsupported) { + this->connect(); + + // Driver does not support asynchronous notification + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_EVENT, 0, + error_state_HY118); + + this->disconnect(); +} +#endif + +#ifdef SQL_ATTR_ASYNC_STMT_PCALLBACK +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAsyncStmtPCCallbackUnsupported) { + this->connect(); + + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_PCALLBACK, 0, + error_state_HYC00); + + this->disconnect(); +} +#endif + +#ifdef SQL_ATTR_ASYNC_STMT_PCONTEXT +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrAsyncStmtPCContextUnsupported) { + this->connect(); + + // Optional feature not implemented + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_ASYNC_STMT_PCONTEXT, 0, + error_state_HYC00); + + this->disconnect(); +} +#endif + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrConcurrency) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_CONCURRENCY, + static_cast(SQL_CONCUR_READ_ONLY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrCursorScrollable) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_CURSOR_SCROLLABLE, + static_cast(SQL_NONSCROLLABLE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrCursorSensitivity) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_CURSOR_SENSITIVITY, + static_cast(SQL_UNSPECIFIED)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrCursorType) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_CURSOR_TYPE, + static_cast(SQL_CURSOR_FORWARD_ONLY)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrEnableAutoIPD) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_ENABLE_AUTO_IPD, + static_cast(SQL_FALSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrFetchBookmarkPointer) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_FETCH_BOOKMARK_PTR, static_cast(NULL)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrIMPParamDesc) { + this->connect(); + + // Invalid use of an automatically allocated descriptor handle + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_IMP_PARAM_DESC, + static_cast(0), error_state_HY017); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrIMPRowDesc) { + this->connect(); + + // Invalid use of an automatically allocated descriptor handle + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_IMP_ROW_DESC, static_cast(0), + error_state_HY017); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrKeysetSizeUnsupported) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_KEYSET_SIZE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrMaxLength) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_MAX_LENGTH, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrMaxRows) { + this->connect(); + + // Cannot set read-only attribute + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_MAX_ROWS, static_cast(0), + error_state_HY092); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrMetadataID) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_METADATA_ID, static_cast(SQL_FALSE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrNoscan) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_NOSCAN, static_cast(SQL_NOSCAN_OFF)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamBindOffsetPtr) { + this->connect(); + + SQLULEN offset = 1000; + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, + static_cast(&offset)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, + static_cast(&offset)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamBindType) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAM_BIND_TYPE, + static_cast(SQL_PARAM_BIND_BY_COLUMN)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamOperationPtr) { + this->connect(); + + constexpr SQLULEN param_set_size = 4; + SQLUSMALLINT param_operations[param_set_size] = {SQL_PARAM_PROCEED, SQL_PARAM_IGNORE, + SQL_PARAM_PROCEED, SQL_PARAM_IGNORE}; + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAM_OPERATION_PTR, + static_cast(param_operations)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_OPERATION_PTR, + static_cast(param_operations)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamStatusPtr) { + this->connect(); + + // Driver does not support parameters, so just check array can be saved/retrieved + constexpr SQLULEN param_status_size = 4; + SQLUSMALLINT param_status[param_status_size] = {SQL_PARAM_PROCEED, SQL_PARAM_IGNORE, + SQL_PARAM_PROCEED, SQL_PARAM_IGNORE}; + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAM_STATUS_PTR, + static_cast(param_status)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAM_STATUS_PTR, + static_cast(param_status)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamsProcessedPtr) { + this->connect(); + + SQLULEN processed_count = 0; + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, + static_cast(&processed_count)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, + static_cast(&processed_count)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrParamsetSize) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_PARAMSET_SIZE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrQueryTimeout) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_QUERY_TIMEOUT, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRetrieveData) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_RETRIEVE_DATA, + static_cast(SQL_RD_ON)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowArraySize) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROW_ARRAY_SIZE, static_cast(1)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowBindOffsetPtr) { + this->connect(); + + SQLULEN offset = 1000; + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, + static_cast(&offset)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, + static_cast(&offset)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowBindType) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROW_BIND_TYPE, static_cast(0)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowNumber) { + this->connect(); + + // Cannot set read-only attribute + validateSetStmtAttrErrorCode(this->stmt, SQL_ATTR_ROW_NUMBER, static_cast(0), + error_state_HY092); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowOperationPtr) { + this->connect(); + + constexpr SQLULEN param_set_size = 4; + SQLUSMALLINT row_operations[param_set_size] = {SQL_ROW_PROCEED, SQL_ROW_IGNORE, + SQL_ROW_PROCEED, SQL_ROW_IGNORE}; + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROW_OPERATION_PTR, + static_cast(row_operations)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_OPERATION_PTR, + static_cast(row_operations)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowStatusPtr) { + this->connect(); + + constexpr SQLULEN row_status_size = 4; + SQLUSMALLINT values[4] = {0, 0, 0, 0}; + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROW_STATUS_PTR, + static_cast(values)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROW_STATUS_PTR, + static_cast(values)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowsFetchedPtr) { + this->connect(); + + SQLULEN rows_fetched = 1; + + validateSetStmtAttr(this->stmt, SQL_ATTR_ROWS_FETCHED_PTR, + static_cast(&rows_fetched)); + + validateGetStmtAttr(this->stmt, SQL_ATTR_ROWS_FETCHED_PTR, + static_cast(&rows_fetched)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrSimulateCursor) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_SIMULATE_CURSOR, + static_cast(SQL_SC_UNIQUE)); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrUseBookmarks) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ATTR_USE_BOOKMARKS, + static_cast(SQL_UB_OFF)); + + this->disconnect(); +} + +// This is a pre ODBC 3 attribute +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrRowsetSize) { + this->connect(); + + validateSetStmtAttr(this->stmt, SQL_ROWSET_SIZE, static_cast(1)); + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc From c513be903510398b6ab110b62d543d0b3cac73ba Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:35:12 -0700 Subject: [PATCH 29/74] SQLBindCol Implementation * SQL_UNBIND implementation + tests * Add tests for `SQL_ATTR_ROW_ARRAY_SIZE` * Add check for `SQL_ATTR_ROWS_FETCHED_PTR` --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 14 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 28 +- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 2 + .../flight/sql/odbc/tests/statement_test.cc | 604 +++++++++++++++++- 4 files changed, 633 insertions(+), 15 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index e7c55d5a73a..ec30405870f 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -150,15 +150,11 @@ SQLRETURN SQL_API SQLPrepare(SQLHSTMT stmt, SQLWCHAR* queryText, SQLINTEGER text SQLRETURN SQL_API SQLExecute(SQLHSTMT stmt) { return arrow::SQLExecute(stmt); } -SQLRETURN SQL_API SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, - SQLSMALLINT targetType, SQLPOINTER targetValuePtr, - SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { - LOG_DEBUG( - "SQLBindCol called with stmt: {}, columnNumber: {}, targetType: {}, " - "targetValuePtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", - stmt, columnNumber, targetType, targetValuePtr, bufferLength, - fmt::ptr(strLen_or_IndPtr)); - return SQL_ERROR; +SQLRETURN SQL_API SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, + SQLLEN* indicatorPtr) { + return arrow::SQLBindCol(stmt, recordNumber, cType, dataPtr, bufferLength, + indicatorPtr); } SQLRETURN SQL_API SQLCancel(SQLHSTMT stmt) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 91121726e8f..8a823ee2086 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -197,9 +197,16 @@ SQLRETURN SQLFreeStmt(SQLHSTMT handle, SQLUSMALLINT option) { return SQLFreeHandle(SQL_HANDLE_STMT, handle); } - // TODO Implement SQLBindCol case SQL_UNBIND: { - return SQL_SUCCESS; + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(handle, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(handle); + ODBCDescriptor* ard = statement->GetARD(); + // Unbind columns + ard->SetHeaderField(SQL_DESC_COUNT, (void*)0, 0); + return SQL_SUCCESS; + }); } // SQLBindParameter is not supported @@ -976,6 +983,23 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { }); } +SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { + LOG_DEBUG( + "SQLBindCol called with stmt: {}, recordNumber: {}, cType: {}, " + "dataPtr: {}, bufferLength: {}, strLen_or_IndPtr: {}", + stmt, recordNumber, cType, dataPtr, bufferLength, fmt::ptr(indicatorPtr)); + 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(recordNumber, cType, dataPtr, bufferLength, indicatorPtr); + return SQL_SUCCESS; + }); +} + SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { // GH-46979: support SQL_C_GUID data type diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 504a8f545f8..91c45b88738 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -68,6 +68,8 @@ 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 SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, + SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); SQLRETURN SQLMoreResults(SQLHSTMT stmt); 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 3335d5f7246..5585dba5dd0 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -1077,8 +1077,8 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectFloatTruncation) { } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectNullQuery) { - // Limitation on mock test server prevents null from working properly. - // Mock server has type `DENSE_UNION` for null column data. + // 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. this->connect(); std::wstring wsql = L"SELECT null as null_col;"; @@ -1166,8 +1166,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLExecDirectTruncationQueryNullIndicator) } TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExecDirectNullQueryNullIndicator) { - // Limitation on mock test server prevents null from working properly. - // Mock server has type `DENSE_UNION` for null column data. + // 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. this->connect(); std::wstring wsql = L"SELECT null as null_col;"; @@ -1432,4 +1432,600 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectIgnoreInvalidBufLen) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColDataQuery) { + this->connect(); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val_min; + int8_t stiny_int_val_max; + SQLLEN buf_len = 0; + SQLLEN ind; + + SQLRETURN ret = + SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 2, SQL_C_STINYINT, &stiny_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Unsigned Tiny Int + uint8_t utiny_int_val_min; + uint8_t utiny_int_val_max; + + ret = SQLBindCol(this->stmt, 3, SQL_C_UTINYINT, &utiny_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 4, SQL_C_UTINYINT, &utiny_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Signed Small Int + int16_t ssmall_int_val_min; + int16_t ssmall_int_val_max; + + ret = SQLBindCol(this->stmt, 5, SQL_C_SSHORT, &ssmall_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 6, SQL_C_SSHORT, &ssmall_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Unsigned Small Int + uint16_t usmall_int_val_min; + uint16_t usmall_int_val_max; + + ret = SQLBindCol(this->stmt, 7, SQL_C_USHORT, &usmall_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 8, SQL_C_USHORT, &usmall_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Signed Integer + SQLINTEGER slong_val_min; + SQLINTEGER slong_val_max; + + ret = SQLBindCol(this->stmt, 9, SQL_C_SLONG, &slong_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 10, SQL_C_SLONG, &slong_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Unsigned Integer + SQLUINTEGER ulong_val_min; + SQLUINTEGER ulong_val_max; + + ret = SQLBindCol(this->stmt, 11, SQL_C_ULONG, &ulong_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 12, SQL_C_ULONG, &ulong_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Signed Big Int + SQLBIGINT sbig_int_val_min; + SQLBIGINT sbig_int_val_max; + + ret = SQLBindCol(this->stmt, 13, SQL_C_SBIGINT, &sbig_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 14, SQL_C_SBIGINT, &sbig_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Unsigned Big Int + SQLUBIGINT ubig_int_val_min; + SQLUBIGINT ubig_int_val_max; + + ret = SQLBindCol(this->stmt, 15, SQL_C_UBIGINT, &ubig_int_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 16, SQL_C_UBIGINT, &ubig_int_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // 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)); + + ret = SQLBindCol(this->stmt, 17, SQL_C_NUMERIC, &decimal_val_neg, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 18, SQL_C_NUMERIC, &decimal_val_pos, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Float + float float_val_min; + float float_val_max; + + ret = SQLBindCol(this->stmt, 19, SQL_C_FLOAT, &float_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 20, SQL_C_FLOAT, &float_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Double + SQLDOUBLE double_val_min; + SQLDOUBLE double_val_max; + + ret = SQLBindCol(this->stmt, 21, SQL_C_DOUBLE, &double_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 22, SQL_C_DOUBLE, &double_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Bit + bool bit_val_false; + bool bit_val_true; + + ret = SQLBindCol(this->stmt, 23, SQL_C_BIT, &bit_val_false, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 24, SQL_C_BIT, &bit_val_true, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Characters + SQLCHAR char_val[2]; + buf_len = sizeof(SQLCHAR) * 2; + + ret = SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLWCHAR wchar_val[2]; + constexpr size_t wchar_size = driver::odbcabstraction::GetSqlWCharSize(); + buf_len = wchar_size * 2; + + ret = SQLBindCol(this->stmt, 26, SQL_C_WCHAR, &wchar_val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLWCHAR wvarchar_val[3]; + buf_len = wchar_size * 3; + + ret = SQLBindCol(this->stmt, 27, SQL_C_WCHAR, &wvarchar_val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLCHAR varchar_val[4]; + buf_len = sizeof(SQLCHAR) * 4; + + ret = SQLBindCol(this->stmt, 28, SQL_C_CHAR, &varchar_val, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Date and Timestamp + SQL_DATE_STRUCT date_val_min{}, date_val_max{}; + buf_len = 0; + + ret = SQLBindCol(this->stmt, 29, SQL_C_TYPE_DATE, &date_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 30, SQL_C_TYPE_DATE, &date_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQL_TIMESTAMP_STRUCT timestamp_val_min{}, timestamp_val_max{}; + + ret = + SQLBindCol(this->stmt, 31, SQL_C_TYPE_TIMESTAMP, ×tamp_val_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = + SQLBindCol(this->stmt, 32, SQL_C_TYPE_TIMESTAMP, ×tamp_val_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Execute query and fetch data once since there is only 1 row. + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Data verification + + // Signed Tiny Int + EXPECT_EQ(stiny_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(stiny_int_val_max, std::numeric_limits::max()); + + // Unsigned Tiny Int + EXPECT_EQ(utiny_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(utiny_int_val_max, std::numeric_limits::max()); + + // Signed Small Int + EXPECT_EQ(ssmall_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(ssmall_int_val_max, std::numeric_limits::max()); + + // Unsigned Small Int + EXPECT_EQ(usmall_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(usmall_int_val_max, std::numeric_limits::max()); + + // Signed Long + EXPECT_EQ(slong_val_min, std::numeric_limits::min()); + EXPECT_EQ(slong_val_max, std::numeric_limits::max()); + + // Unsigned Long + EXPECT_EQ(ulong_val_min, std::numeric_limits::min()); + EXPECT_EQ(ulong_val_max, std::numeric_limits::max()); + + // Signed Big Int + EXPECT_EQ(sbig_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(sbig_int_val_max, std::numeric_limits::max()); + + // Unsigned Big Int + EXPECT_EQ(ubig_int_val_min, std::numeric_limits::min()); + EXPECT_EQ(ubig_int_val_max, std::numeric_limits::max()); + + // Decimal + EXPECT_EQ(decimal_val_neg.sign, 0); + EXPECT_EQ(decimal_val_neg.scale, 0); + EXPECT_EQ(decimal_val_neg.precision, 38); + 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(decimal_val_pos.sign, 1); + EXPECT_EQ(decimal_val_pos.scale, 0); + EXPECT_EQ(decimal_val_pos.precision, 38); + 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(float_val_min, -std::numeric_limits::max()); + EXPECT_EQ(float_val_max, std::numeric_limits::max()); + + // Double + EXPECT_EQ(double_val_min, -std::numeric_limits::max()); + EXPECT_EQ(double_val_max, std::numeric_limits::max()); + + // Bit + EXPECT_EQ(bit_val_false, false); + EXPECT_EQ(bit_val_true, true); + + // Characters + EXPECT_EQ(char_val[0], 'Z'); + EXPECT_EQ(wchar_val[0], L'你'); + EXPECT_EQ(wvarchar_val[0], L'你'); + EXPECT_EQ(wvarchar_val[1], L'好'); + + EXPECT_EQ(varchar_val[0], 'X'); + EXPECT_EQ(varchar_val[1], 'Y'); + EXPECT_EQ(varchar_val[2], 'Z'); + + // Date + EXPECT_EQ(date_val_min.day, 1); + EXPECT_EQ(date_val_min.month, 1); + EXPECT_EQ(date_val_min.year, 1400); + + EXPECT_EQ(date_val_max.day, 31); + EXPECT_EQ(date_val_max.month, 12); + EXPECT_EQ(date_val_max.year, 9999); + + // Timestamp + EXPECT_EQ(timestamp_val_min.day, 1); + EXPECT_EQ(timestamp_val_min.month, 1); + EXPECT_EQ(timestamp_val_min.year, 1400); + EXPECT_EQ(timestamp_val_min.hour, 0); + EXPECT_EQ(timestamp_val_min.minute, 0); + EXPECT_EQ(timestamp_val_min.second, 0); + EXPECT_EQ(timestamp_val_min.fraction, 0); + + EXPECT_EQ(timestamp_val_max.day, 31); + EXPECT_EQ(timestamp_val_max.month, 12); + EXPECT_EQ(timestamp_val_max.year, 9999); + EXPECT_EQ(timestamp_val_max.hour, 23); + EXPECT_EQ(timestamp_val_max.minute, 59); + EXPECT_EQ(timestamp_val_max.second, 59); + EXPECT_EQ(timestamp_val_max.fraction, 0); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLBindColTimeQuery) { + // Mock server test is skipped due to limitation on the mock server. + // Time type from mock server does not include the fraction + this->connect(); + + SQL_TIME_STRUCT time_var_min{}; + SQL_TIME_STRUCT time_var_max{}; + SQLLEN buf_len = sizeof(time_var_min); + SQLLEN ind; + + SQLRETURN ret = + SQLBindCol(this->stmt, 1, SQL_C_TYPE_TIME, &time_var_min, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLBindCol(this->stmt, 2, SQL_C_TYPE_TIME, &time_var_max, buf_len, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + 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()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check min values for time. + EXPECT_EQ(time_var_min.hour, 0); + EXPECT_EQ(time_var_min.minute, 0); + EXPECT_EQ(time_var_min.second, 0); + + // Check max values for time. + EXPECT_EQ(time_var_max.hour, 23); + EXPECT_EQ(time_var_max.minute, 59); + EXPECT_EQ(time_var_max.second, 59); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLBindColVarbinaryQuery) { + // Have binary test on mock test base as remote test servers tend to have different + // formats for binary data + this->connect(); + + // varbinary + std::vector varbinary_val(3); + SQLLEN buf_len = varbinary_val.size(); + SQLLEN ind; + SQLRETURN ret = + 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()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check varbinary values + EXPECT_EQ(varbinary_val[0], '\xAB'); + EXPECT_EQ(varbinary_val[1], '\xCD'); + EXPECT_EQ(varbinary_val[2], '\xEF'); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, 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. + this->connect(); + + SQLINTEGER val; + SQLLEN ind; + + SQLRETURN ret = SQLBindCol(this->stmt, 1, SQL_C_LONG, &val, 0, &ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::wstring wsql = L"SELECT null as null_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify SQL_NULL_DATA is returned for indicator + EXPECT_EQ(ind, SQL_NULL_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, 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. + this->connect(); + + SQLINTEGER val; + + SQLRETURN ret = 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()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_ERROR); + // Verify invalid null indicator is reported, as it is required + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_22002); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColRowFetching) { + this->connect(); + + 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. + SQLRETURN ret = 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()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Fetch row 1 + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 1 is returned + EXPECT_EQ(val, 1); + + // Fetch row 2 + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 2 is returned + EXPECT_EQ(val, 2); + + // Fetch row 3 + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 3 is returned + EXPECT_EQ(val, 3); + + // Verify result set has no more data beyond row 3 + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColRowArraySize) { + // Set SQL_ATTR_ROW_ARRAY_SIZE to fetch 3 rows at once + this->connect(); + + 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. + SQLRETURN ret = SQLBindCol(this->stmt, 1, SQL_C_LONG, val, buf_len, ind); + + SQLLEN rows_fetched; + 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); + + ret = SQLSetStmtAttr(this->stmt, SQL_ATTR_ROW_ARRAY_SIZE, + reinterpret_cast(rows), 0); + + // Fetch 3 rows at once + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify 3 rows are fetched + EXPECT_EQ(rows_fetched, 3); + + // Verify 1 is returned + EXPECT_EQ(val[0], 1); + // Verify 2 is returned + EXPECT_EQ(val[1], 2); + // Verify 3 is returned + EXPECT_EQ(val[2], 3); + + // Verify result set has no more data beyond row 3 + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColIndicatorOnly) { + // GH-47021: implement driver to return indicator value when data pointer is null + GTEST_SKIP(); + // Verify driver supports null data pointer with valid indicator pointer + this->connect(); + + // Numeric Types + + // Signed Tiny Int + SQLLEN stiny_int_ind; + + SQLRETURN ret = SQLBindCol(this->stmt, 1, SQL_C_STINYINT, 0, 0, &stiny_int_ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Characters + SQLLEN buf_len = sizeof(SQLCHAR) * 2; + SQLLEN char_val_ind; + + ret = SQLBindCol(this->stmt, 25, SQL_C_CHAR, 0, buf_len, &char_val_ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Execute query and fetch data once since there is only 1 row. + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify values for indicator pointer + // Signed Tiny Int + EXPECT_EQ(stiny_int_ind, 1); + + // Char array + EXPECT_EQ(char_val_ind, 1); + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColIndicatorOnlySQLUnbind) { + // Verify driver supports valid indicator pointer after unbinding all columns + this->connect(); + + // Numeric Types + + // Signed Tiny Int + int8_t stiny_int_val; + SQLLEN stiny_int_ind; + + SQLRETURN ret = + SQLBindCol(this->stmt, 1, SQL_C_STINYINT, &stiny_int_val, 0, &stiny_int_ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Characters + SQLCHAR char_val[2]; + SQLLEN buf_len = sizeof(SQLCHAR) * 2; + SQLLEN char_val_ind; + + ret = SQLBindCol(this->stmt, 25, SQL_C_CHAR, &char_val, buf_len, &char_val_ind); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver should still be able to execute queries after unbinding columns + ret = SQLFreeStmt(this->stmt, SQL_UNBIND); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Execute query and fetch data once since there is only 1 row. + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // 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(stiny_int_ind, 1); + + // Char array + // EXPECT_EQ(char_val_ind, 1); + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc From 44f720205f5b0ee63f7a1fc56b6074305ce5bf5d Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 16 Jul 2025 12:50:20 -0700 Subject: [PATCH 30/74] Enable TestCloseConnectionWithOpenStatement test case --- cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 22fd4a5ea12..471a2fffd80 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -1016,9 +1016,6 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLAllocFreeStmt) { } TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { - // Test is disabled as disconnecting without closing statement fails on Windows. - // This test case can be potentially used on macOS/Linux. - GTEST_SKIP(); // ODBC Environment SQLHENV env; SQLHDBC conn; From 557f79c1c39626f6046d87d1c2156bd05730ea10 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:12:38 -0700 Subject: [PATCH 31/74] SQLFetchScroll Implementation * Add checks for SQL_ATTR_ROWS_FETCHED_PTR * SQLFetchScroll is supposed to return the number of rows fetched using SQL_ATTR_ROWS_FETCHED_PTR --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 5 + cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 29 +++- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 1 + .../flight/sql/odbc/tests/odbc_test_suite.h | 1 + .../flight/sql/odbc/tests/statement_test.cc | 129 +++++++++++++++++- 6 files changed, 162 insertions(+), 4 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index ec30405870f..204bd0ffb74 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 d5767c5b5c4..c837b782d22 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 8a823ee2086..50c5462f227 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 91c45b88738..2a5b3e80a92 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 1dd71e12f73..21e2d899ef1 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 5585dba5dd0..4b188b6f002 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(); From ac50792a7665d54f3496c4ec02e001cf3127b217 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:49:17 -0700 Subject: [PATCH 32/74] SQLExtendedFetch Implementation * Implement rowCountPtr and rowStatusArray for SQLExtendedFetch * Add tests for SQLExtendedFetch * `SQLExtendedFetch` doesn't return `SQL_SUCCESS_WITH_INFO` for error state 22002. * Add tests for `SQL_ROWSET_SIZE` * Address comments from James --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 7 ++ cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 33 +++++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 3 + .../odbc_impl/odbc_statement.h | 4 +- .../odbc_impl/odbc_statement.cc | 22 ++++- .../flight/sql/odbc/tests/statement_test.cc | 87 ++++++++++++++++++- 7 files changed, 151 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 204bd0ffb74..54d22ade54c 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -137,6 +137,13 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT stmt, SQLWCHAR* queryText, SQLRETURN SQL_API SQLFetch(SQLHSTMT stmt) { return arrow::SQLFetch(stmt); } +SQLRETURN SQL_API SQLExtendedFetch(SQLHSTMT stmt, SQLUSMALLINT fetchOrientation, + SQLLEN fetchOffset, SQLULEN* rowCountPtr, + SQLUSMALLINT* rowStatusArray) { + return arrow::SQLExtendedFetch(stmt, fetchOrientation, fetchOffset, rowCountPtr, + rowStatusArray); +} + SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset) { return arrow::SQLFetchScroll(stmt, fetchOrientation, fetchOffset); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index c837b782d22..1581e51b558 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -33,6 +33,7 @@ EXPORTS SQLErrorW SQLExecDirectW SQLExecute + SQLExtendedFetch SQLFetch SQLFetchScroll SQLForeignKeysW diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 50c5462f227..89f08ffe8d8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -983,6 +983,39 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { }); } +SQLRETURN SQLExtendedFetch(SQLHSTMT stmt, SQLUSMALLINT fetchOrientation, + SQLLEN fetchOffset, SQLULEN* rowCountPtr, + SQLUSMALLINT* rowStatusArray) { + // GH-47110: SQLExtendedFetch should return SQL_SUCCESS_WITH_INFO for certain diag + // states + LOG_DEBUG( + "SQLExtendedFetch called with stmt: {}, fetchOrientation: {}, fetchOffset: {}, " + "rowCountPtr: {}, rowStatusArray: {}", + stmt, fetchOrientation, fetchOffset, fmt::ptr(rowCountPtr), + fmt::ptr(rowStatusArray)); + 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_ROWSET_SIZE statement attribute specifies the number of rows in the + // rowset. + SQLULEN rowSetSize = statement->GetRowsetSize(); + LOG_DEBUG("SQL_ROWSET_SIZE value for SQLExtendedFetch: {}", rowSetSize); + if (statement->Fetch(static_cast(rowSetSize), rowCountPtr, rowStatusArray)) { + return SQL_SUCCESS; + } else { + // Reached the end of rowset + return SQL_NO_DATA; + } + }); +} + SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset) { LOG_DEBUG("SQLFetchScroll called with stmt: {}, fetchOrientation: {}, fetchOffset: {}", diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 2a5b3e80a92..e31a06c1666 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -68,6 +68,9 @@ 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 SQLExtendedFetch(SQLHSTMT stmt, SQLUSMALLINT fetchOrientation, + SQLLEN fetchOffset, SQLULEN* rowCountPtr, + SQLUSMALLINT* rowStatusArray); SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset); SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h index df5ca5e34ab..73cdc2448f8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h @@ -66,8 +66,10 @@ class ODBCStatement : public ODBCHandle { /** * @brief Returns true if the number of rows fetch was greater than zero. + * rowCountPtr and rowStatusArray are optional arguments, they are only needed for + * SQLExtendedFetch */ - bool Fetch(size_t rows); + bool Fetch(size_t rows, SQLULEN* rowCountPtr = 0, SQLUSMALLINT* rowStatusArray = 0); bool isPrepared() const; void GetStmtAttr(SQLINTEGER statementAttribute, SQLPOINTER output, diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index 1714850323a..3940bdccc77 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -317,7 +317,8 @@ void ODBCStatement::ExecuteDirect(const std::string& query) { m_isPrepared = false; } -bool ODBCStatement::Fetch(size_t rows) { +bool ODBCStatement::Fetch(size_t rows, SQLULEN* rowCountPtr, + SQLUSMALLINT* rowStatusArray) { if (m_hasReachedEndOfResult) { m_ird->SetRowsProcessed(0); return false; @@ -350,11 +351,24 @@ bool ODBCStatement::Fetch(size_t rows) { m_currentArd->NotifyBindingsHavePropagated(); } - size_t rowsFetched = m_currenResult->Move(rows, m_currentArd->GetBindOffset(), - m_currentArd->GetBoundStructOffset(), - m_ird->GetArrayStatusPtr()); + uint16_t* arrayStatusPtr; + if (rowStatusArray) { + // For SQLExtendedFetch only + arrayStatusPtr = rowStatusArray; + } else { + arrayStatusPtr = m_ird->GetArrayStatusPtr(); + } + + size_t rowsFetched = + m_currenResult->Move(rows, m_currentArd->GetBindOffset(), + m_currentArd->GetBoundStructOffset(), arrayStatusPtr); m_ird->SetRowsProcessed(static_cast(rowsFetched)); + if (rowCountPtr) { + // For SQLExtendedFetch only + *rowCountPtr = rowsFetched; + } + m_rowNumber += rowsFetched; m_hasReachedEndOfResult = rowsFetched != rows; return rowsFetched != 0; 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 4b188b6f002..a490457c3a3 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -1187,7 +1187,6 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectFloatTruncation) { EXPECT_EQ(ret, SQL_SUCCESS); int16_t ssmall_int_val; - SQLLEN buf_len = sizeof(ssmall_int_val); ret = SQLGetData(this->stmt, 1, SQL_C_SSHORT, &ssmall_int_val, 0, 0); EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); @@ -2151,4 +2150,90 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLBindColIndicatorOnlySQLUnbind) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLExtendedFetchRowFetching) { + // Set SQL_ROWSET_SIZE to fetch 3 rows at once + this->connect(); + + 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. + SQLRETURN ret = SQLBindCol(this->stmt, 1, SQL_C_LONG, val, buf_len, ind); + + ret = + SQLSetStmtAttr(this->stmt, SQL_ROWSET_SIZE, reinterpret_cast(rows), 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-3. + SQLULEN row_count; + SQLUSMALLINT row_status[rows]; + + ret = SQLExtendedFetch(this->stmt, SQL_FETCH_NEXT, 0, &row_count, row_status); + EXPECT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(row_count, 3); + + for (int i = 0; i < rows; i++) { + EXPECT_EQ(row_status[i], SQL_SUCCESS); + } + + // Verify 1 is returned for row 1 + EXPECT_EQ(val[0], 1); + // Verify 2 is returned for row 2 + EXPECT_EQ(val[1], 2); + // Verify 3 is returned for row 3 + EXPECT_EQ(val[2], 3); + + // Verify result set has no more data beyond row 3 + SQLULEN row_count2; + SQLUSMALLINT row_status2[rows]; + ret = SQLExtendedFetch(this->stmt, SQL_FETCH_NEXT, 0, &row_count2, row_status2); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExtendedFetchQueryNullIndicator) { + // GH-47110: SQLExtendedFetch should return SQL_SUCCESS_WITH_INFO for 22002 + // 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. + GTEST_SKIP(); + this->connect(); + + SQLINTEGER val; + + SQLRETURN ret = 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()); + + ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLULEN row_count1; + SQLUSMALLINT row_status1[1]; + + // SQLExtendedFetch should return SQL_SUCCESS_WITH_INFO for 22002 state + ret = SQLExtendedFetch(this->stmt, SQL_FETCH_NEXT, 0, &row_count1, row_status1); + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_22002); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc From f173df601775be6a7ba23459353080a79e41a201 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:58:52 -0700 Subject: [PATCH 33/74] GH-46584 Iterate endpoint * use suggestion from James for one-liner change to return `Status::OK` directly * fix for iterating endpoints, it is based on JDBC's fix for the same bug * save value of `FlightClientOptions` * Use `TestFlightServer` Add `arrow_flight_sql_shared` to enable usages for `TestFlightServer` * Fix build errors from missing `gmock` * Add checks for array data Update flight_sql_stream_chunk_buffer_test.cc Add `FlightStreamChunkBufferTest` unit test Draft test with `FlightSQLODBCMockEndpointTestBase` * Reference GH-47117 and Clean up code * Add driver and DSN to built-in properties to not pass driver/dsn as attributes to server * Allow `=` in values inside connection strings, fixes connectivity problems * Address comments from Rob --- cpp/src/arrow/flight/CMakeLists.txt | 4 +- .../flight/sql/odbc/flight_sql/CMakeLists.txt | 4 +- .../odbc/flight_sql/flight_sql_auth_method.cc | 4 +- .../odbc/flight_sql/flight_sql_connection.cc | 12 +- .../odbc/flight_sql/flight_sql_result_set.cc | 3 +- .../odbc/flight_sql/flight_sql_result_set.h | 1 + .../odbc/flight_sql/flight_sql_statement.cc | 55 +++---- .../odbc/flight_sql/flight_sql_statement.h | 2 + .../flight_sql_statement_get_tables.cc | 44 +++--- .../flight_sql_statement_get_tables.h | 25 ++-- .../flight_sql_stream_chunk_buffer.cc | 45 +++++- .../flight_sql_stream_chunk_buffer.h | 4 +- .../flight_sql_stream_chunk_buffer_test.cc | 137 ++++++++++++++++++ .../sql/odbc/flight_sql/get_info_cache.cc | 12 +- .../sql/odbc/flight_sql/get_info_cache.h | 4 +- .../odbc_impl/odbc_connection.cc | 2 +- 16 files changed, 280 insertions(+), 78 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer_test.cc diff --git a/cpp/src/arrow/flight/CMakeLists.txt b/cpp/src/arrow/flight/CMakeLists.txt index a827a7307f8..bf7d9c7f1f9 100644 --- a/cpp/src/arrow/flight/CMakeLists.txt +++ b/cpp/src/arrow/flight/CMakeLists.txt @@ -295,7 +295,9 @@ if(ARROW_TESTING) STATIC_INSTALL_INTERFACE_LIBS ${ARROW_FLIGHT_TESTING_STATIC_INSTALL_INTERFACE_LIBS} PRIVATE_INCLUDES - "${Protobuf_INCLUDE_DIRS}") + "${Protobuf_INCLUDE_DIRS}" + SHARED_PRIVATE_LINK_LIBS + GTest::gmock) foreach(LIB_TARGET ${ARROW_FLIGHT_TESTING_LIBRARIES}) target_compile_definitions(${LIB_TARGET} PRIVATE ARROW_FLIGHT_EXPORTING) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt index 895eff8ed9a..bd876804279 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/CMakeLists.txt @@ -136,9 +136,11 @@ add_arrow_test(arrow_odbc_spi_impl_test accessors/time_array_accessor_test.cc accessors/timestamp_array_accessor_test.cc flight_sql_connection_test.cc + flight_sql_stream_chunk_buffer_test.cc parse_table_types_test.cc json_converter_test.cc record_batch_transformer_test.cc utils_test.cc EXTRA_LINK_LIBS - arrow_odbc_spi_impl) + arrow_odbc_spi_impl + arrow_flight_testing_shared) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc index 6c265673837..b2d57e5df85 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc @@ -58,9 +58,9 @@ class NoOpClientAuthHandler : public arrow::flight::ClientAuthHandler { arrow::Status Authenticate(arrow::flight::ClientAuthSender* outgoing, arrow::flight::ClientAuthReader* incoming) override { - // Write a blank string. The server should ignore this and just accept any Handshake + // Return OK Status. The server should ignore this and just accept any Handshake // request. - return outgoing->Write(std::string()); + return arrow::Status::OK(); } arrow::Status GetToken(std::string* token) override { diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc index 6d5d95865ba..c87c394fc31 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_connection.cc @@ -112,6 +112,8 @@ inline std::string GetCerts() { return ""; } #endif const std::set BUILT_IN_PROPERTIES = { + FlightSqlConnection::DRIVER, + FlightSqlConnection::DSN, FlightSqlConnection::HOST, FlightSqlConnection::PORT, FlightSqlConnection::USER, @@ -165,15 +167,15 @@ void FlightSqlConnection::Connect(const ConnPropertyMap& properties, auto flight_ssl_configs = LoadFlightSslConfigs(properties); Location location = BuildLocation(properties, missing_attr, flight_ssl_configs); - FlightClientOptions client_options = + client_options_ = BuildFlightClientOptions(properties, missing_attr, flight_ssl_configs); const std::shared_ptr& cookie_factory = arrow::flight::GetCookieFactory(); - client_options.middleware.push_back(cookie_factory); + client_options_.middleware.push_back(cookie_factory); std::unique_ptr flight_client; - ThrowIfNotOK(FlightClient::Connect(location, client_options).Value(&flight_client)); + ThrowIfNotOK(FlightClient::Connect(location, client_options_).Value(&flight_client)); PopulateMetadataSettings(properties); PopulateCallOptions(properties); @@ -377,7 +379,7 @@ void FlightSqlConnection::Close() { std::shared_ptr FlightSqlConnection::CreateStatement() { return std::shared_ptr(new FlightSqlStatement( - diagnostics_, *sql_client_, call_options_, metadata_settings_)); + diagnostics_, *sql_client_, client_options_, call_options_, metadata_settings_)); } bool FlightSqlConnection::SetAttribute(Connection::AttributeId attribute, @@ -423,7 +425,7 @@ FlightSqlConnection::FlightSqlConnection(OdbcVersion odbc_version, const std::string& driver_version) : diagnostics_("Apache Arrow", "Flight SQL", odbc_version), odbc_version_(odbc_version), - info_(call_options_, sql_client_, driver_version), + info_(client_options_, call_options_, sql_client_, driver_version), closed_(true) { attribute_[CONNECTION_DEAD] = static_cast(SQL_TRUE); attribute_[LOGIN_TIMEOUT] = static_cast(0); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc index 6744537ae5b..258c810996a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.cc @@ -44,13 +44,14 @@ using odbcabstraction::DriverException; FlightSqlResultSet::FlightSqlResultSet( FlightSqlClient& flight_sql_client, + const arrow::flight::FlightClientOptions& client_options, const arrow::flight::FlightCallOptions& call_options, const std::shared_ptr& flight_info, const std::shared_ptr& transformer, odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings) : metadata_settings_(metadata_settings), - chunk_buffer_(flight_sql_client, call_options, flight_info, + chunk_buffer_(flight_sql_client, client_options, call_options, flight_info, metadata_settings_.chunk_buffer_capacity_), transformer_(transformer), metadata_(transformer diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h index c82a2d83543..5a03b16f066 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set.h @@ -63,6 +63,7 @@ class FlightSqlResultSet : public ResultSet { ~FlightSqlResultSet() override; FlightSqlResultSet(FlightSqlClient& flight_sql_client, + const arrow::flight::FlightClientOptions& client_options, const arrow::flight::FlightCallOptions& call_options, const std::shared_ptr& flight_info, const std::shared_ptr& transformer, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc index 4f94ef70168..efe333d836a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.cc @@ -64,11 +64,12 @@ void ClosePreparedStatementIfAny( FlightSqlStatement::FlightSqlStatement( const odbcabstraction::Diagnostics& diagnostics, FlightSqlClient& sql_client, - FlightCallOptions call_options, + arrow::flight::FlightClientOptions client_options, FlightCallOptions call_options, const odbcabstraction::MetadataSettings& metadata_settings) : diagnostics_("Apache Arrow", diagnostics.GetDataSourceComponent(), diagnostics.GetOdbcVersion()), sql_client_(sql_client), + client_options_(std::move(client_options)), call_options_(std::move(call_options)), metadata_settings_(metadata_settings) { attribute_[METADATA_ID] = static_cast(SQL_FALSE); @@ -135,8 +136,8 @@ bool FlightSqlStatement::ExecutePrepared() { ThrowIfNotOK(result.status()); current_result_set_ = std::make_shared( - sql_client_, call_options_, result.ValueOrDie(), nullptr, diagnostics_, - metadata_settings_); + sql_client_, client_options_, call_options_, result.ValueOrDie(), nullptr, + diagnostics_, metadata_settings_); return true; } @@ -148,8 +149,8 @@ bool FlightSqlStatement::Execute(const std::string& query) { ThrowIfNotOK(result.status()); current_result_set_ = std::make_shared( - sql_client_, call_options_, result.ValueOrDie(), nullptr, diagnostics_, - metadata_settings_); + sql_client_, client_options_, call_options_, result.ValueOrDie(), nullptr, + diagnostics_, metadata_settings_); return true; } @@ -170,27 +171,29 @@ std::shared_ptr FlightSqlStatement::GetTables( if ((catalog_name && *catalog_name == "%") && (schema_name && schema_name->empty()) && (table_name && table_name->empty())) { - current_result_set_ = GetTablesForSQLAllCatalogs( - column_names, call_options_, sql_client_, diagnostics_, metadata_settings_); + current_result_set_ = + GetTablesForSQLAllCatalogs(column_names, client_options_, call_options_, + sql_client_, diagnostics_, metadata_settings_); } else if ((catalog_name && catalog_name->empty()) && (schema_name && *schema_name == "%") && (table_name && table_name->empty())) { - current_result_set_ = - GetTablesForSQLAllDbSchemas(column_names, call_options_, sql_client_, schema_name, - diagnostics_, metadata_settings_); + current_result_set_ = GetTablesForSQLAllDbSchemas( + column_names, client_options_, call_options_, sql_client_, schema_name, + diagnostics_, metadata_settings_); } else if ((catalog_name && catalog_name->empty()) && (schema_name && schema_name->empty()) && (table_name && table_name->empty()) && (table_type && *table_type == "%")) { - current_result_set_ = GetTablesForSQLAllTableTypes( - column_names, call_options_, sql_client_, diagnostics_, metadata_settings_); + current_result_set_ = + GetTablesForSQLAllTableTypes(column_names, client_options_, call_options_, + sql_client_, diagnostics_, metadata_settings_); } else { if (table_type) { ParseTableTypes(*table_type, table_types); } current_result_set_ = GetTablesForGenericUse( - column_names, call_options_, sql_client_, catalog_name, schema_name, table_name, - table_types, diagnostics_, metadata_settings_); + column_names, client_options_, call_options_, sql_client_, catalog_name, + schema_name, table_name, table_types, diagnostics_, metadata_settings_); } return current_result_set_; @@ -228,9 +231,9 @@ std::shared_ptr FlightSqlStatement::GetColumns_V2( auto transformer = std::make_shared( metadata_settings_, odbcabstraction::V_2, column_name); - current_result_set_ = - std::make_shared(sql_client_, call_options_, flight_info, - transformer, diagnostics_, metadata_settings_); + current_result_set_ = std::make_shared( + sql_client_, client_options_, call_options_, flight_info, transformer, diagnostics_, + metadata_settings_); return current_result_set_; } @@ -249,9 +252,9 @@ std::shared_ptr FlightSqlStatement::GetColumns_V3( auto transformer = std::make_shared( metadata_settings_, odbcabstraction::V_3, column_name); - current_result_set_ = - std::make_shared(sql_client_, call_options_, flight_info, - transformer, diagnostics_, metadata_settings_); + current_result_set_ = std::make_shared( + sql_client_, client_options_, call_options_, flight_info, transformer, diagnostics_, + metadata_settings_); return current_result_set_; } @@ -267,9 +270,9 @@ std::shared_ptr FlightSqlStatement::GetTypeInfo_V2(int16_t data_type) auto transformer = std::make_shared( metadata_settings_, odbcabstraction::V_2, data_type); - current_result_set_ = - std::make_shared(sql_client_, call_options_, flight_info, - transformer, diagnostics_, metadata_settings_); + current_result_set_ = std::make_shared( + sql_client_, client_options_, call_options_, flight_info, transformer, diagnostics_, + metadata_settings_); return current_result_set_; } @@ -285,9 +288,9 @@ std::shared_ptr FlightSqlStatement::GetTypeInfo_V3(int16_t data_type) auto transformer = std::make_shared( metadata_settings_, odbcabstraction::V_3, data_type); - current_result_set_ = - std::make_shared(sql_client_, call_options_, flight_info, - transformer, diagnostics_, metadata_settings_); + current_result_set_ = std::make_shared( + sql_client_, client_options_, call_options_, flight_info, transformer, diagnostics_, + metadata_settings_); return current_result_set_; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h index 56341223259..00fe9137f51 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement.h @@ -33,6 +33,7 @@ class FlightSqlStatement : public odbcabstraction::Statement { private: odbcabstraction::Diagnostics diagnostics_; std::map attribute_; + arrow::flight::FlightClientOptions client_options_; arrow::flight::FlightCallOptions call_options_; arrow::flight::sql::FlightSqlClient& sql_client_; std::shared_ptr current_result_set_; @@ -48,6 +49,7 @@ class FlightSqlStatement : public odbcabstraction::Statement { public: FlightSqlStatement(const odbcabstraction::Diagnostics& diagnostics, arrow::flight::sql::FlightSqlClient& sql_client, + arrow::flight::FlightClientOptions client_options, arrow::flight::FlightCallOptions call_options, const odbcabstraction::MetadataSettings& metadata_settings); ~FlightSqlStatement(); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc index a3cdf9768d2..ba3389ccba9 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc @@ -72,8 +72,9 @@ void ParseTableTypes(const std::string& table_type, } std::shared_ptr GetTablesForSQLAllCatalogs( - const ColumnNames& names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings) { Result> result = sql_client.GetCatalogs(call_options); @@ -92,14 +93,15 @@ std::shared_ptr GetTablesForSQLAllCatalogs( .AddFieldOfNulls(names.remarks_column, arrow::utf8()) .Build(); - return std::make_shared( - sql_client, call_options, flight_info, transformer, diagnostics, metadata_settings); + return std::make_shared(sql_client, client_options, call_options, + flight_info, transformer, diagnostics, + metadata_settings); } std::shared_ptr GetTablesForSQLAllDbSchemas( - const ColumnNames& names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, const std::string* schema_name, - odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + const std::string* schema_name, odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings) { Result> result = sql_client.GetDbSchemas(call_options, nullptr, schema_name); @@ -119,13 +121,15 @@ std::shared_ptr GetTablesForSQLAllDbSchemas( .AddFieldOfNulls(names.remarks_column, arrow::utf8()) .Build(); - return std::make_shared( - sql_client, call_options, flight_info, transformer, diagnostics, metadata_settings); + return std::make_shared(sql_client, client_options, call_options, + flight_info, transformer, diagnostics, + metadata_settings); } std::shared_ptr GetTablesForSQLAllTableTypes( - const ColumnNames& names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings) { Result> result = sql_client.GetTableTypes(call_options); @@ -144,15 +148,16 @@ std::shared_ptr GetTablesForSQLAllTableTypes( .AddFieldOfNulls(names.remarks_column, arrow::utf8()) .Build(); - return std::make_shared( - sql_client, call_options, flight_info, transformer, diagnostics, metadata_settings); + return std::make_shared(sql_client, client_options, call_options, + flight_info, transformer, diagnostics, + metadata_settings); } std::shared_ptr GetTablesForGenericUse( - const ColumnNames& names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, const std::string* catalog_name, - const std::string* schema_name, const std::string* table_name, - const std::vector& table_types, + const ColumnNames& names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + const std::string* catalog_name, const std::string* schema_name, + const std::string* table_name, const std::vector& table_types, odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings) { Result> result = sql_client.GetTables( @@ -173,8 +178,9 @@ std::shared_ptr GetTablesForGenericUse( .AddFieldOfNulls(names.remarks_column, arrow::utf8()) .Build(); - return std::make_shared( - sql_client, call_options, flight_info, transformer, diagnostics, metadata_settings); + return std::make_shared(sql_client, client_options, call_options, + flight_info, transformer, diagnostics, + metadata_settings); } } // namespace flight_sql diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.h index 8f0dc5fef6d..0f5ac461f3f 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.h @@ -30,6 +30,7 @@ namespace driver { namespace flight_sql { using arrow::flight::FlightCallOptions; +using arrow::flight::FlightClientOptions; using arrow::flight::sql::FlightSqlClient; using odbcabstraction::MetadataSettings; using odbcabstraction::ResultSet; @@ -46,26 +47,28 @@ void ParseTableTypes(const std::string& table_type, std::vector& table_types); std::shared_ptr GetTablesForSQLAllCatalogs( - const ColumnNames& column_names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& column_names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings); std::shared_ptr GetTablesForSQLAllDbSchemas( - const ColumnNames& column_names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, const std::string* schema_name, - odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& column_names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + const std::string* schema_name, odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings); std::shared_ptr GetTablesForSQLAllTableTypes( - const ColumnNames& column_names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, odbcabstraction::Diagnostics& diagnostics, + const ColumnNames& column_names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings); std::shared_ptr GetTablesForGenericUse( - const ColumnNames& column_names, FlightCallOptions& call_options, - FlightSqlClient& sql_client, const std::string* catalog_name, - const std::string* schema_name, const std::string* table_name, - const std::vector& table_types, + const ColumnNames& column_names, FlightClientOptions& client_options, + FlightCallOptions& call_options, FlightSqlClient& sql_client, + const std::string* catalog_name, const std::string* schema_name, + const std::string* table_name, const std::vector& table_types, odbcabstraction::Diagnostics& diagnostics, const odbcabstraction::MetadataSettings& metadata_settings); } // namespace flight_sql diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.cc index 093a46dfe83..7da2d6ca89d 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.cc @@ -21,38 +21,71 @@ namespace driver { namespace flight_sql { +using arrow::flight::FlightClient; using arrow::flight::FlightEndpoint; FlightStreamChunkBuffer::FlightStreamChunkBuffer( FlightSqlClient& flight_sql_client, + const arrow::flight::FlightClientOptions& client_options, const arrow::flight::FlightCallOptions& call_options, const std::shared_ptr& flight_info, size_t queue_capacity) : queue_(queue_capacity) { - // FIXME: Endpoint iteration should consider endpoints may be at different hosts for (const auto& endpoint : flight_info->endpoints()) { const arrow::flight::Ticket& ticket = endpoint.ticket; - auto result = flight_sql_client.DoGet(call_options, ticket); + arrow::Result> result; + std::shared_ptr temp_flight_sql_client; + auto endpoint_locations = endpoint.locations; + if (endpoint_locations.empty()) { + // list of Locations needs to be empty to proceed + result = flight_sql_client.DoGet(call_options, ticket); + } else { + // If it is non-empty, the driver should create a FlightSqlClient to connect to one + // of the specified Locations directly. + + // GH-47117: Currently a new FlightClient will be made for each partition that + // returns a non-empty Location, which is then disposed of. It may be better to + // cache clients because a server may report the same Locations. It would also be + // good to identify when the reported Location is the same as the original + // connection's Location and skip creating a FlightClient in that scenario. + + std::unique_ptr temp_flight_client; + ThrowIfNotOK(FlightClient::Connect(endpoint_locations[0], client_options) + .Value(&temp_flight_client)); + temp_flight_sql_client.reset(new FlightSqlClient(std::move(temp_flight_client))); + + result = temp_flight_sql_client->DoGet(call_options, ticket); + } + ThrowIfNotOK(result.status()); std::shared_ptr stream_reader_ptr(std::move(result.ValueOrDie())); - BlockingQueue>::Supplier supplier = [=] { + BlockingQueue, + std::shared_ptr>>::Supplier supplier = [=] { auto result = stream_reader_ptr->Next(); bool isNotOk = !result.ok(); bool isNotEmpty = result.ok() && (result.ValueOrDie().data != nullptr); - return boost::make_optional(isNotOk || isNotEmpty, std::move(result)); + // If result is valid, save the temp Flight SQL Client for future stream reader + // call. temp_flight_sql_client is intentionally null if the list of endpoint + // Locations is empty. + // After all data is fetched from reader, the temp client is closed. + return boost::make_optional( + isNotOk || isNotEmpty, + std::make_pair(std::move(result), temp_flight_sql_client)); }; queue_.AddProducer(std::move(supplier)); } } bool FlightStreamChunkBuffer::GetNext(FlightStreamChunk* chunk) { - Result result; - if (!queue_.Pop(&result)) { + std::pair, std::shared_ptr> + closeableEndpointStreamPair; + if (!queue_.Pop(&closeableEndpointStreamPair)) { return false; } + Result result = closeableEndpointStreamPair.first; if (!result.status().ok()) { Close(); throw odbcabstraction::DriverException(result.status().message()); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h index 864c025d8b3..5d5616a4f02 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h @@ -32,10 +32,12 @@ using arrow::flight::sql::FlightSqlClient; using driver::odbcabstraction::BlockingQueue; class FlightStreamChunkBuffer { - BlockingQueue> queue_; + BlockingQueue, std::shared_ptr>> + queue_; public: FlightStreamChunkBuffer(FlightSqlClient& flight_sql_client, + const arrow::flight::FlightClientOptions& client_options, const arrow::flight::FlightCallOptions& call_options, const std::shared_ptr& flight_info, size_t queue_capacity = 5); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer_test.cc new file mode 100644 index 00000000000..6857b53f5c2 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer_test.cc @@ -0,0 +1,137 @@ +// 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/array.h" + +#include "arrow/testing/gtest_util.h" + +#include "arrow/flight/sql/odbc/flight_sql/flight_sql_stream_chunk_buffer.h" +#include "arrow/flight/sql/odbc/flight_sql/json_converter.h" +#include "arrow/flight/test_flight_server.h" +#include "arrow/flight/test_util.h" +#include "gtest/gtest.h" + +namespace driver { +namespace flight_sql { + +using arrow::Array; +using arrow::flight::FlightCallOptions; +using arrow::flight::FlightClientOptions; +using arrow::flight::FlightDescriptor; +using arrow::flight::FlightEndpoint; +using arrow::flight::Location; +using arrow::flight::Ticket; +using arrow::flight::sql::FlightSqlClient; + +class FlightStreamChunkBufferTest : public ::testing::Test { + // Sets up two mock servers for each test case. + // This is for testing endpoint iteration only. + + protected: + void SetUp() override { + // Set up server 1 + server1 = std::make_shared(); + ASSERT_OK_AND_ASSIGN(auto location1, Location::ForGrpcTcp("0.0.0.0", 0)); + arrow::flight::FlightServerOptions options1(location1); + ASSERT_OK(server1->Init(options1)); + ASSERT_OK_AND_ASSIGN(server_location1, + Location::ForGrpcTcp("localhost", server1->port())); + + // Set up server 2 + server2 = std::make_shared(); + ASSERT_OK_AND_ASSIGN(auto location2, Location::ForGrpcTcp("0.0.0.0", 0)); + arrow::flight::FlightServerOptions options2(location2); + ASSERT_OK(server2->Init(options2)); + ASSERT_OK_AND_ASSIGN(server_location2, + Location::ForGrpcTcp("localhost", server2->port())); + + // Make SQL Client that is connected to server 1 + ASSERT_OK_AND_ASSIGN(auto client, arrow::flight::FlightClient::Connect(location1)); + sql_client.reset(new FlightSqlClient(std::move(client))); + } + + void TearDown() override { + ASSERT_OK(server1->Shutdown()); + ASSERT_OK(server2->Shutdown()); + } + + public: + arrow::flight::Location server_location1; + std::shared_ptr server1; + arrow::flight::Location server_location2; + std::shared_ptr server2; + std::shared_ptr sql_client; +}; + +FlightInfo MultipleEndpointsFlightInfo(Location location1, Location location2) { + // Sever will generate random data for `ticket-ints-1` + FlightEndpoint endpoint1({Ticket{"ticket-ints-1"}, {location1}, std::nullopt, {}}); + FlightEndpoint endpoint2({Ticket{"ticket-ints-1"}, {location2}, std::nullopt, {}}); + + FlightDescriptor descr1{FlightDescriptor::PATH, "", {"examples", "ints"}}; + + auto schema1 = arrow::flight::ExampleIntSchema(); + + return arrow::flight::MakeFlightInfo(*schema1, descr1, {endpoint1, endpoint2}, 1000, + 100000, false, ""); +} + +void verifyArraysContainIntsOnly(std::shared_ptr intArray) { + for (int64_t i = 0; i < intArray->length(); ++i) { + // null values are accepted + if (!intArray->IsNull(i)) { + auto scalar_data = intArray->GetScalar(i).ValueOrDie(); + std::string scalar_str = ConvertToJson(*scalar_data); + ASSERT_TRUE(std::all_of(scalar_str.begin(), scalar_str.end(), ::isdigit)); + } + } +} + +TEST_F(FlightStreamChunkBufferTest, TestMultipleEndpointsInt) { + FlightClientOptions client_options = FlightClientOptions::Defaults(); + FlightCallOptions options; + FlightInfo info = MultipleEndpointsFlightInfo(server_location1, server_location2); + std::shared_ptr info_ptr = std::make_shared(info); + + FlightStreamChunkBuffer chunk_buffer(*sql_client, client_options, options, info_ptr); + + FlightStreamChunk current_chunk; + + // Server returns 5 batch of results from each endpoints. + // Each batch contains 8 columns + int num_chunks = 0; + while (chunk_buffer.GetNext(¤t_chunk)) { + num_chunks++; + + int num_cols = current_chunk.data->num_columns(); + EXPECT_EQ(num_cols, 8); + + for (int i = 0; i < num_cols; i++) { + auto array = current_chunk.data->column(i); + // Each array has random length + EXPECT_GT(array->length(), 0); + + verifyArraysContainIntsOnly(array); + } + } + + // Verify 5 batches of data is returned by each of the two endpoints. + // In total 10 batches should be returned. + EXPECT_EQ(num_chunks, 10); +} +} // namespace flight_sql +} // namespace driver diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc index cf10c658dbf..1e4cdbeb65d 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.cc @@ -204,15 +204,20 @@ inline void SetDefaultIfMissing( namespace driver { namespace flight_sql { using arrow::flight::FlightCallOptions; +using arrow::flight::FlightClientOptions; using arrow::flight::sql::FlightSqlClient; using arrow::flight::sql::SqlInfoOptions; using driver::odbcabstraction::Connection; using driver::odbcabstraction::DriverException; -GetInfoCache::GetInfoCache(FlightCallOptions& call_options, +GetInfoCache::GetInfoCache(FlightClientOptions& client_options, + FlightCallOptions& call_options, std::unique_ptr& client, const std::string& driver_version) - : call_options_(call_options), sql_client_(client), has_server_info_(false) { + : client_options_(client_options), + call_options_(call_options), + sql_client_(client), + has_server_info_(false) { info_[SQL_DRIVER_NAME] = "Arrow Flight ODBC Driver"; info_[SQL_DRIVER_VER] = ConvertToDBMSVer(driver_version); @@ -294,7 +299,8 @@ bool GetInfoCache::LoadInfoFromServer() { arrow::Result> result = sql_client_->GetSqlInfo(call_options_, {}); ThrowIfNotOK(result.status()); - FlightStreamChunkBuffer chunk_iter(*sql_client_, call_options_, result.ValueOrDie()); + FlightStreamChunkBuffer chunk_iter(*sql_client_, client_options_, call_options_, + result.ValueOrDie()); FlightStreamChunk chunk; bool supports_correlation_name = false; diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h index 819b095e6a6..547fb1cdf28 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/get_info_cache.h @@ -30,13 +30,15 @@ namespace flight_sql { class GetInfoCache { private: std::unordered_map info_; + arrow::flight::FlightClientOptions& client_options_; arrow::flight::FlightCallOptions& call_options_; std::unique_ptr& sql_client_; std::mutex mutex_; std::atomic has_server_info_; public: - GetInfoCache(arrow::flight::FlightCallOptions& call_options, + GetInfoCache(arrow::flight::FlightClientOptions& client_options, + arrow::flight::FlightCallOptions& call_options, std::unique_ptr& client, const std::string& driver_version); void SetProperty(uint16_t property, driver::odbcabstraction::Connection::Info value); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 15d60ccf1ac..02947756479 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -52,7 +52,7 @@ namespace { // characters such as semi-colons and equals signs. NOTE: This can be optimized to be // built statically. const boost::xpressive::sregex CONNECTION_STR_REGEX( - boost::xpressive::sregex::compile("([^=;]+)=({.+}|[^=;]+|[^;])")); + boost::xpressive::sregex::compile("([^=;]+)=({.+}|[^;]+|[^;])")); // Load properties from the given DSN. The properties loaded do _not_ overwrite existing // entries in the properties. From ed8f98f5e28bc35851549d5ed366f2f5df910128 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:42:48 -0700 Subject: [PATCH 34/74] SQLColumns Implementation * Initial impl for SQLColumns * Add test for SQLColumns * Add columns test with all supported column types - mock server doesn't support schema * Address comments from James - Test different data type return value for SQLColumns - Add todo comment for SQLDescribeCol tests --------- Co-authored-by: rscales --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 13 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 35 + cpp/src/arrow/flight/sql/odbc/odbc_api.h | 4 + .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/columns_test.cc | 984 ++++++++++++++++++ .../flight/sql/odbc/tests/odbc_test_suite.cc | 91 +- .../flight/sql/odbc/tests/odbc_test_suite.h | 35 +- 7 files changed, 1146 insertions(+), 17 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 54d22ade54c..4b9960f4489 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -198,16 +198,9 @@ SQLRETURN SQL_API SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* columnName, SQLSMALLINT columnNameLength) { - LOG_DEBUG( - "SQLColumnsW called with stmt: {}, catalogName: {}, catalogNameLength: " - "{}, " - "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " - "columnName: {}, " - "columnNameLength: {}", - stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), - schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(columnName), - columnNameLength); - return SQL_ERROR; + return arrow::SQLColumns(stmt, catalogName, catalogNameLength, schemaName, + schemaNameLength, tableName, tableNameLength, columnName, + columnNameLength); } SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 89f08ffe8d8..5553db10276 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1112,4 +1112,39 @@ SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { return SQL_SUCCESS; }); } + +SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, + SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, + SQLWCHAR* tableName, SQLSMALLINT tableNameLength, + SQLWCHAR* columnName, SQLSMALLINT columnNameLength) { + // GH-47159: Return NUM_PREC_RADIX based on whether COLUMN_SIZE contains number of + // digits or bits + LOG_DEBUG( + "SQLColumnsW called with stmt: {}, catalogName: {}, catalogNameLength: " + "{}, " + "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " + "columnName: {}, " + "columnNameLength: {}", + stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(columnName), + columnNameLength); + + using ODBC::ODBCStatement; + using ODBC::SqlWcharToString; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + std::string catalog = SqlWcharToString(catalogName, catalogNameLength); + std::string schema = SqlWcharToString(schemaName, schemaNameLength); + std::string table = SqlWcharToString(tableName, tableNameLength); + std::string column = SqlWcharToString(columnName, columnNameLength); + + statement->GetColumns(catalogName ? &catalog : nullptr, + schemaName ? &schema : nullptr, tableName ? &table : nullptr, + columnName ? &column : nullptr); + + return SQL_SUCCESS; + }); +} } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index e31a06c1666..b1b5c5a3033 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -79,4 +79,8 @@ SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType SQLRETURN SQLMoreResults(SQLHSTMT stmt); SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr); SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr); +SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, + SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, + SQLWCHAR* tableName, SQLSMALLINT tableNameLength, + SQLWCHAR* columnName, SQLSMALLINT columnNameLength); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 952b1d36cc8..3bb1ac697e9 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -31,6 +31,7 @@ set(ARROW_FLIGHT_SQL_MOCK_SERVER_SRCS add_arrow_test(flight_sql_odbc_test SOURCES + columns_test.cc connection_attr_test.cc connection_info_test.cc statement_attr_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc new file mode 100644 index 00000000000..a05b8cd0f3a --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -0,0 +1,984 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +// TODO: add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and +// ODBC 3. + +namespace arrow::flight::sql::odbc { +void checkSQLColumns( + SQLHSTMT stmt, const std::wstring& expectedTable, const std::wstring& expectedColumn, + const SQLINTEGER& expectedDataType, const std::wstring& expectedTypeName, + const SQLINTEGER& expectedColumnSize, const SQLINTEGER& expectedBufferLength, + const SQLSMALLINT& expectedDecimalDigits, const SQLSMALLINT& expectedNumPrecRadix, + const SQLSMALLINT& expectedNullable, const SQLSMALLINT& expectedSqlDataType, + const SQLSMALLINT& expectedDateTimeSub, const SQLINTEGER& expectedOctetCharLength, + const SQLINTEGER& expectedOrdinalPosition, const std::wstring& expectedIsNullable) { + CheckStringColumnW(stmt, 3, expectedTable); // table name + CheckStringColumnW(stmt, 4, expectedColumn); // column name + + CheckIntColumn(stmt, 5, expectedDataType); // data type + + CheckStringColumnW(stmt, 6, expectedTypeName); // type name + + CheckIntColumn(stmt, 7, expectedColumnSize); // column size + CheckIntColumn(stmt, 8, expectedBufferLength); // buffer length + + CheckSmallIntColumn(stmt, 9, expectedDecimalDigits); // decimal digits + CheckSmallIntColumn(stmt, 10, expectedNumPrecRadix); // num prec radix + CheckSmallIntColumn(stmt, 11, + expectedNullable); // nullable + + CheckNullColumnW(stmt, 12); // remarks + CheckNullColumnW(stmt, 13); // column def + + CheckSmallIntColumn(stmt, 14, expectedSqlDataType); // sql data type + CheckSmallIntColumn(stmt, 15, expectedDateTimeSub); // sql date type sub + CheckIntColumn(stmt, 16, expectedOctetCharLength); // char octet length + CheckIntColumn(stmt, 17, + expectedOrdinalPosition); // oridinal position + + CheckStringColumnW(stmt, 18, expectedIsNullable); // is nullable +} + +void checkMockSQLColumns( + SQLHSTMT stmt, const std::wstring& expectedCatalog, const std::wstring& expectedTable, + const std::wstring& expectedColumn, const SQLINTEGER& expectedDataType, + const std::wstring& expectedTypeName, const SQLINTEGER& expectedColumnSize, + const SQLINTEGER& expectedBufferLength, const SQLSMALLINT& expectedDecimalDigits, + const SQLSMALLINT& expectedNumPrecRadix, const SQLSMALLINT& expectedNullable, + const SQLSMALLINT& expectedSqlDataType, const SQLSMALLINT& expectedDateTimeSub, + const SQLINTEGER& expectedOctetCharLength, const SQLINTEGER& expectedOrdinalPosition, + const std::wstring& expectedIsNullable) { + CheckStringColumnW(stmt, 1, expectedCatalog); // catalog + CheckNullColumnW(stmt, 2); // schema + + checkSQLColumns(stmt, expectedTable, expectedColumn, expectedDataType, expectedTypeName, + expectedColumnSize, expectedBufferLength, expectedDecimalDigits, + expectedNumPrecRadix, expectedNullable, expectedSqlDataType, + expectedDateTimeSub, expectedOctetCharLength, expectedOrdinalPosition, + expectedIsNullable); +} + +void checkRemoteSQLColumns( + SQLHSTMT stmt, const std::wstring& expectedSchema, const std::wstring& expectedTable, + const std::wstring& expectedColumn, const SQLINTEGER& expectedDataType, + const std::wstring& expectedTypeName, const SQLINTEGER& expectedColumnSize, + const SQLINTEGER& expectedBufferLength, const SQLSMALLINT& expectedDecimalDigits, + const SQLSMALLINT& expectedNumPrecRadix, const SQLSMALLINT& expectedNullable, + const SQLSMALLINT& expectedSqlDataType, const SQLSMALLINT& expectedDateTimeSub, + const SQLINTEGER& expectedOctetCharLength, const SQLINTEGER& expectedOrdinalPosition, + const std::wstring& expectedIsNullable) { + CheckNullColumnW(stmt, 1); // catalog + CheckStringColumnW(stmt, 2, expectedSchema); // schema + checkSQLColumns(stmt, expectedTable, expectedColumn, expectedDataType, expectedTypeName, + expectedColumnSize, expectedBufferLength, expectedDecimalDigits, + expectedNumPrecRadix, expectedNullable, expectedSqlDataType, + expectedDateTimeSub, expectedOctetCharLength, expectedOrdinalPosition, + expectedIsNullable); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsAllColumns) { + // Check table pattern and column pattern returns all columns + this->connect(); + + // Attempt to get all columns + SQLWCHAR tablePattern[] = L"%"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // mock limitation: SQLite mock server returns 10 for bigint size when spec indicates + // should be 19 + // DECIMAL_DIGITS should be 0 for bigint type since it is exact + // mock limitation: SQLite mock server returns 10 for bigint decimal digits when spec + // indicates should be 0 + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"foreignTable"), // expectedTable + std::wstring(L"id"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 2nd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"foreignTable"), // expectedTable + std::wstring(L"foreignName"), // expectedColumn + SQL_WVARCHAR, // expectedDataType + std::wstring(L"WVARCHAR"), // expectedTypeName + 0, // expectedColumnSize (mock server limitation: returns 0 for + // varchar(100), the ODBC spec expects 100) + 0, // expectedBufferLength + 15, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedDateTimeSub + 0, // expectedOctetCharLength + 2, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 3rd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"foreignTable"), // expectedTable + std::wstring(L"value"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 3, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 4th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"intTable"), // expectedTable + std::wstring(L"id"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 5th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"intTable"), // expectedTable + std::wstring(L"keyName"), // expectedColumn + SQL_WVARCHAR, // expectedDataType + std::wstring(L"WVARCHAR"), // expectedTypeName + 0, // expectedColumnSize (mock server limitation: returns 0 for + // varchar(100), the ODBC spec expects 100) + 0, // expectedBufferLength + 15, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedDateTimeSub + 0, // expectedOctetCharLength + 2, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 6th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"intTable"), // expectedTable + std::wstring(L"value"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 3, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 7th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"intTable"), // expectedTable + std::wstring(L"foreignId"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 4, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsAllTypes) { + // Limitation: Mock server returns incorrect values for column size for some columns. + // For character and binary type columns, the driver calculates buffer length and char + // octet length from column size. + + // Checks filtering table with table name pattern + this->connect(); + this->CreateTableAllDataType(); + + // Attempt to get all columns from AllTypesTable + SQLWCHAR tablePattern[] = L"AllTypesTable"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Fetch SQLColumn data for 1st column in AllTypesTable + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"AllTypesTable"), // expectedTable + std::wstring(L"bigint_col"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock server limitation: returns 10, + // the ODBC spec expects 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock server limitation: returns 15, + // the ODBC spec expects 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check SQLColumn data for 2nd column in AllTypesTable + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"AllTypesTable"), // expectedTable + std::wstring(L"char_col"), // expectedColumn + SQL_WVARCHAR, // expectedDataType + std::wstring(L"WVARCHAR"), // expectedTypeName + 0, // expectedColumnSize (mock server limitation: returns 0 for + // varchar(100), the ODBC spec expects 100) + 0, // expectedBufferLength + 15, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedDateTimeSub + 0, // expectedOctetCharLength + 2, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check SQLColumn data for 3rd column in AllTypesTable + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"AllTypesTable"), // expectedTable + std::wstring(L"varbinary_col"), // expectedColumn + SQL_BINARY, // expectedDataType + std::wstring(L"BINARY"), // expectedTypeName + 0, // expectedColumnSize (mock server limitation: returns 0 for + // BLOB column, spec expects binary data limit) + 0, // expectedBufferLength + 15, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BINARY, // expectedSqlDataType + NULL, // expectedDateTimeSub + 0, // expectedOctetCharLength + 3, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check SQLColumn data for 4th column in AllTypesTable + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"AllTypesTable"), // expectedTable + std::wstring(L"double_col"), // expectedColumn + SQL_DOUBLE, // expectedDataType + std::wstring(L"DOUBLE"), // expectedTypeName + 15, // expectedColumnSize + 8, // expectedBufferLength + 15, // expectedDecimalDigits + 2, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 4, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There should be no more column data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsUnicode) { + // Limitation: Mock server returns incorrect values for column size for some columns. + // For character and binary type columns, the driver calculates buffer length and char + // octet length from column size. + this->connect(); + this->CreateUnicodeTable(); + + // Attempt to get all columns + SQLWCHAR tablePattern[] = L"数据"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check SQLColumn data for 1st column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"数据"), // expectedTable + std::wstring(L"资料"), // expectedColumn + SQL_WVARCHAR, // expectedDataType + std::wstring(L"WVARCHAR"), // expectedTypeName + 0, // expectedColumnSize (mock server limitation: returns 0 for + // varchar(100), spec expects 100) + 0, // expectedBufferLength + 15, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedDateTimeSub + 0, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There should be no more column data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColumnsAllTypes) { + // GH-47159: Return NUM_PREC_RADIX based on whether COLUMN_SIZE contains number of + // digits or bits + this->connect(); + + SQLWCHAR tablePattern[] = L"ODBCTest"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check 1st Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"sinteger_max"), // expectedColumn + SQL_INTEGER, // expectedDataType + std::wstring(L"INTEGER"), // expectedTypeName + 32, // expectedColumnSize (remote server returns number of bits) + 4, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_INTEGER, // expectedSqlDataType + NULL, // expectedDateTimeSub + 4, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 2nd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"sbigint_max"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 64, // expectedColumnSize (remote server returns number of bits) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 2, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 3rd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"decimal_positive"), // expectedColumn + SQL_DECIMAL, // expectedDataType + std::wstring(L"DECIMAL"), // expectedTypeName + 38, // expectedColumnSize + 19, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DECIMAL, // expectedSqlDataType + NULL, // expectedDateTimeSub + 2, // expectedOctetCharLength + 3, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 4th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"float_max"), // expectedColumn + SQL_FLOAT, // expectedDataType + std::wstring(L"FLOAT"), // expectedTypeName + 24, // expectedColumnSize (precision bits from IEEE 754) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 2, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_FLOAT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 4, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 5th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"double_max"), // expectedColumn + SQL_DOUBLE, // expectedDataType + std::wstring(L"DOUBLE"), // expectedTypeName + 53, // expectedColumnSize (precision bits from IEEE 754) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 2, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 5, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 6th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"bit_true"), // expectedColumn + SQL_BIT, // expectedDataType + std::wstring(L"BOOLEAN"), // expectedTypeName + 0, // expectedColumnSize (limitation: remote server remote server + // returns 0, should be 1) + 1, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 1, // expectedOctetCharLength + 6, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // ODBC ver 3 returns SQL_TYPE_DATE, SQL_TYPE_TIME, and SQL_TYPE_TIMESTAMP in the + // DATA_TYPE field + + // Check 7th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"date_max"), // expectedColumn + SQL_TYPE_DATE, // expectedDataType + std::wstring(L"DATE"), // expectedTypeName + 0, // expectedColumnSize (limitation: remote server returns 0, should be 10) + 10, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_DATE, // expectedDateTimeSub + 6, // expectedOctetCharLength + 7, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 8th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"time_max"), // expectedColumn + SQL_TYPE_TIME, // expectedDataType + std::wstring(L"TIME"), // expectedTypeName + 3, // expectedColumnSize (limitation: should be 9+fractional digits) + 12, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIME, // expectedDateTimeSub + 6, // expectedOctetCharLength + 8, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 9th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"timestamp_max"), // expectedColumn + SQL_TYPE_TIMESTAMP, // expectedDataType + std::wstring(L"TIMESTAMP"), // expectedTypeName + 3, // expectedColumnSize (limitation: should be 20+fractional digits) + 23, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIMESTAMP, // expectedDateTimeSub + 16, // expectedOctetCharLength + 9, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There is no more column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColumnsAllTypesODBCVer2) { + // GH-47159: Return NUM_PREC_RADIX based on whether COLUMN_SIZE contains number of + // digits or bits + this->connect(SQL_OV_ODBC2); + + SQLWCHAR tablePattern[] = L"ODBCTest"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check 1st Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"sinteger_max"), // expectedColumn + SQL_INTEGER, // expectedDataType + std::wstring(L"INTEGER"), // expectedTypeName + 32, // expectedColumnSize (remote server returns number of bits) + 4, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_INTEGER, // expectedSqlDataType + NULL, // expectedDateTimeSub + 4, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 2nd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"sbigint_max"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 64, // expectedColumnSize (remote server returns number of bits) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 2, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 3rd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"decimal_positive"), // expectedColumn + SQL_DECIMAL, // expectedDataType + std::wstring(L"DECIMAL"), // expectedTypeName + 38, // expectedColumnSize + 19, // expectedBufferLength + 0, // expectedDecimalDigits + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DECIMAL, // expectedSqlDataType + NULL, // expectedDateTimeSub + 2, // expectedOctetCharLength + 3, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 4th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"float_max"), // expectedColumn + SQL_FLOAT, // expectedDataType + std::wstring(L"FLOAT"), // expectedTypeName + 24, // expectedColumnSize (precision bits from IEEE 754) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 2, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_FLOAT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 4, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 5th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"double_max"), // expectedColumn + SQL_DOUBLE, // expectedDataType + std::wstring(L"DOUBLE"), // expectedTypeName + 53, // expectedColumnSize (precision bits from IEEE 754) + 8, // expectedBufferLength + 0, // expectedDecimalDigits + 2, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 5, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 6th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns(this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"bit_true"), // expectedColumn + SQL_BIT, // expectedDataType + std::wstring(L"BOOLEAN"), // expectedTypeName + 0, // expectedColumnSize (limitation: remote server remote server + // returns 0, should be 1) + 1, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 1, // expectedOctetCharLength + 6, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // ODBC ver 2 returns SQL_DATE, SQL_TIME, and SQL_TIMESTAMP in the DATA_TYPE field + + // Check 7th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"date_max"), // expectedColumn + SQL_DATE, // expectedDataType + std::wstring(L"DATE"), // expectedTypeName + 0, // expectedColumnSize (limitation: remote server returns 0, should be 10) + 10, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_DATE, // expectedDateTimeSub + 6, // expectedOctetCharLength + 7, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 8th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"time_max"), // expectedColumn + SQL_TIME, // expectedDataType + std::wstring(L"TIME"), // expectedTypeName + 3, // expectedColumnSize (limitation: should be 9+fractional digits) + 12, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIME, // expectedDateTimeSub + 6, // expectedOctetCharLength + 8, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 9th Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkRemoteSQLColumns( + this->stmt, + std::wstring(L"$scratch"), // expectedSchema + std::wstring(L"ODBCTest"), // expectedTable + std::wstring(L"timestamp_max"), // expectedColumn + SQL_TIMESTAMP, // expectedDataType + std::wstring(L"TIMESTAMP"), // expectedTypeName + 3, // expectedColumnSize (limitation: should be 20+fractional digits) + 23, // expectedBufferLength + 0, // expectedDecimalDigits + 0, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIMESTAMP, // expectedDateTimeSub + 16, // expectedOctetCharLength + 9, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There is no more column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsColumnPattern) { + // Checks filtering table with column name pattern. + // Only check table and column name + this->connect(); + + SQLWCHAR tablePattern[] = L"%"; + SQLWCHAR columnPattern[] = L"id"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check 1st Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"foreignTable"), // expectedTable + std::wstring(L"id"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // Check 2nd Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"intTable"), // expectedTable + std::wstring(L"id"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There is no more column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsTableColumnPattern) { + // Checks filtering table with table and column name pattern. + // Only check table and column name + this->connect(); + + SQLWCHAR tablePattern[] = L"foreignTable"; + SQLWCHAR columnPattern[] = L"id"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check 1st Column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkMockSQLColumns(this->stmt, + std::wstring(L"main"), // expectedCatalog + std::wstring(L"foreignTable"), // expectedTable + std::wstring(L"id"), // expectedColumn + SQL_BIGINT, // expectedDataType + std::wstring(L"BIGINT"), // expectedTypeName + 10, // expectedColumnSize (mock returns 10 instead of 19) + 8, // expectedBufferLength + 15, // expectedDecimalDigits (mock returns 15 instead of 0) + 10, // expectedNumPrecRadix + SQL_NULLABLE, // expectedNullable + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedDateTimeSub + 8, // expectedOctetCharLength + 1, // expectedOrdinalPosition + std::wstring(L"YES")); // expectedIsNullable + + // There is no more column + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsInvalidTablePattern) { + this->connect(); + + SQLWCHAR tablePattern[] = L"non-existent-table"; + SQLWCHAR columnPattern[] = L"%"; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, tablePattern, + SQL_NTS, columnPattern, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // There is no column from filter + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index a8da14c6ecf..e39cce54e41 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -27,13 +27,14 @@ namespace arrow::flight::sql::odbc { -void FlightSQLODBCRemoteTestBase::allocEnvConnHandles() { +void FlightSQLODBCRemoteTestBase::allocEnvConnHandles(SQLINTEGER odbc_ver) { // Allocate an environment handle SQLRETURN ret = SQLAllocEnv(&env); EXPECT_EQ(ret, SQL_SUCCESS); - ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, + reinterpret_cast(static_cast(odbc_ver)), 0); EXPECT_EQ(ret, SQL_SUCCESS); @@ -43,8 +44,8 @@ void FlightSQLODBCRemoteTestBase::allocEnvConnHandles() { EXPECT_EQ(ret, SQL_SUCCESS); } -void FlightSQLODBCRemoteTestBase::connect() { - allocEnvConnHandles(); +void FlightSQLODBCRemoteTestBase::connect(SQLINTEGER odbc_ver) { + allocEnvConnHandles(odbc_ver); std::string connect_str = getConnectionString(); connectWithString(connect_str); } @@ -273,6 +274,42 @@ std::wstring FlightSQLODBCMockTestBase::getQueryAllDataTypes() { return wsql; } +void FlightSQLODBCMockTestBase::CreateTableAllDataType() { + // Limitation on mock SQLite server: + // Only int64, float64, binary, and utf8 Arrow Types are supported by + // SQLiteFlightSqlServer::Impl::DoGetTables + ASSERT_OK(server->ExecuteSql(R"( + CREATE TABLE AllTypesTable( + bigint_col INTEGER PRIMARY KEY AUTOINCREMENT, + char_col varchar(100), + varbinary_col BLOB, + double_col REAL); + + INSERT INTO AllTypesTable ( + char_col, + varbinary_col, + double_col) VALUES ( + '1st Row', + X'31737420726F77', + 3.14159 + ); + )")); +} + +void FlightSQLODBCMockTestBase::CreateUnicodeTable() { + std::string unicodeSql = arrow::util::WideStringToUTF8( + LR"( + CREATE TABLE 数据( + 资料 varchar(100)); + + INSERT INTO 数据 (资料) VALUES ('第一行'); + INSERT INTO 数据 (资料) VALUES ('二行'); + INSERT INTO 数据 (资料) VALUES ('3rd Row'); + )") + .ValueOr(""); + ASSERT_OK(server->ExecuteSql(unicodeSql)); +} + void FlightSQLODBCMockTestBase::SetUp() { ASSERT_OK_AND_ASSIGN(auto location, Location::ForGrpcTcp("0.0.0.0", 0)); arrow::flight::FlightServerOptions options(location); @@ -371,4 +408,50 @@ bool writeDSN(Connection::ConnPropertyMap properties) { return RegisterDsn(config, wDriver.c_str()); } +void CheckStringColumnW(SQLHSTMT stmt, int colId, const std::wstring& expected) { + SQLWCHAR buf[1024]; + SQLLEN bufLen = sizeof(buf) * ODBC::GetSqlWCharSize(); + + SQLRETURN ret = SQLGetData(stmt, colId, SQL_C_WCHAR, buf, bufLen, &bufLen); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(bufLen, 0); + + // returned bufLen is in bytes so convert to length in characters + size_t charCount = static_cast(bufLen) / ODBC::GetSqlWCharSize(); + std::wstring returned(buf, buf + charCount); + + EXPECT_EQ(returned, expected); +} + +void CheckNullColumnW(SQLHSTMT stmt, int colId) { + SQLWCHAR buf[1024]; + SQLLEN bufLen = sizeof(buf); + + SQLRETURN ret = SQLGetData(stmt, colId, SQL_C_WCHAR, buf, bufLen, &bufLen); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(bufLen, SQL_NULL_DATA); +} + +void CheckIntColumn(SQLHSTMT stmt, int colId, const SQLINTEGER& expected) { + SQLINTEGER buf; + SQLLEN bufLen = sizeof(buf); + + SQLRETURN ret = SQLGetData(stmt, colId, SQL_C_LONG, &buf, sizeof(buf), &bufLen); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(buf, expected); +} + +void CheckSmallIntColumn(SQLHSTMT stmt, int colId, const SQLSMALLINT& expected) { + SQLSMALLINT buf; + SQLLEN bufLen = sizeof(buf); + + SQLRETURN ret = SQLGetData(stmt, colId, SQL_C_SSHORT, &buf, sizeof(buf), &bufLen); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(buf, expected); +} + } // namespace arrow::flight::sql::odbc 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 21e2d899ef1..a93a633d754 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 @@ -48,10 +48,11 @@ using driver::odbcabstraction::Connection; class FlightSQLODBCRemoteTestBase : public ::testing::Test { public: /// \brief Allocate environment and connection handles - void allocEnvConnHandles(); + void allocEnvConnHandles(SQLINTEGER odbc_ver = SQL_OV_ODBC3); /// \brief Connect to Arrow Flight SQL server using connection string defined in - /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN", allocate statement handle - void connect(); + /// environment variable "ARROW_FLIGHT_SQL_ODBC_CONN", allocate statement handle. + /// Connects using ODBC Ver 3 by default + void connect(SQLINTEGER odbc_ver = SQL_OV_ODBC3); /// \brief Connect to Arrow Flight SQL server using connection string void connectWithString(std::string connection_str); /// \brief Disconnect from server @@ -124,6 +125,11 @@ class FlightSQLODBCMockTestBase : public FlightSQLODBCRemoteTestBase { /// \brief Return a SQL query that selects all data types std::wstring getQueryAllDataTypes() override; + /// \brief run a SQL query to create a table with all data types + void CreateTableAllDataType(); + /// \brief run a SQL query to create a table with unicode name + void CreateUnicodeTable(); + int port; protected: @@ -185,4 +191,27 @@ bool writeDSN(std::string connection_str); /// \param[in] properties map. /// \return true on success bool writeDSN(Connection::ConnPropertyMap properties); + +/// \brief Check wide string column. +/// \param[in] stmt Statement. +/// \param[in] colId Column ID to check. +/// \param[in] expected Expected value. +void CheckStringColumnW(SQLHSTMT stmt, int colId, const std::wstring& expected); + +/// \brief Check wide string column value is null. +/// \param[in] stmt Statement. +/// \param[in] colId Column ID to check. +void CheckNullColumnW(SQLHSTMT stmt, int colId); + +/// \brief Check int column. +/// \param[in] stmt Statement. +/// \param[in] colId Column ID to check. +/// \param[in] expected Expected value. +void CheckIntColumn(SQLHSTMT stmt, int colId, const SQLINTEGER& expected); + +/// \brief Check smallint column. +/// \param[in] stmt Statement. +/// \param[in] colId Column ID to check. +/// \param[in] expected Expected value. +void CheckSmallIntColumn(SQLHSTMT stmt, int colId, const SQLSMALLINT& expected); } // namespace arrow::flight::sql::odbc From 7e240e263367c7daa4dbf9c1f48d6f71bd2ea3f4 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:39:05 -0700 Subject: [PATCH 35/74] Fix bug of DSN not read properly (#67) * Fix reading DSN bug Use a simpler, more robust way to load the DSN --- .../odbc_impl/odbc_connection.cc | 43 ++--------- .../flight/sql/odbc/tests/connection_test.cc | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc index 02947756479..337951ede3a 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_connection.cc @@ -20,6 +20,7 @@ #include "arrow/result.h" #include "arrow/util/utf8.h" +#include "arrow/flight/sql/odbc/flight_sql/include/flight_sql/config/configuration.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/exceptions.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/attribute_utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h" @@ -58,44 +59,10 @@ const boost::xpressive::sregex CONNECTION_STR_REGEX( // entries in the properties. void loadPropertiesFromDSN(const std::string& dsn, Connection::ConnPropertyMap& properties) { - const size_t BUFFER_SIZE = 1024 * 10; - std::vector outputBuffer; - outputBuffer.resize(BUFFER_SIZE, '\0'); - SQLSetConfigMode(ODBC_BOTH_DSN); - - std::wstring wDsn = arrow::util::UTF8ToWideString(dsn).ValueOr(L""); - - SQLGetPrivateProfileString(wDsn.c_str(), NULL, L"", &outputBuffer[0], BUFFER_SIZE, - L"odbc.ini"); - - // The output buffer holds the list of keys in a series of NUL-terminated strings. - // The series is terminated with an empty string (eg a NUL-terminator terminating the - // last key followed by a NUL terminator after). - std::vector keys; - size_t pos = 0; - while (pos < BUFFER_SIZE) { - std::wstring wKey(&outputBuffer[pos]); - if (wKey.empty()) { - break; - } - size_t len = wKey.size(); - - // Skip over Driver or DSN keys. - if (!boost::iequals(wKey, L"DSN") && !boost::iequals(wKey, L"Driver")) { - keys.emplace_back(std::move(wKey)); - } - pos += len + 1; - } - - for (auto& wKey : keys) { - outputBuffer.clear(); - outputBuffer.resize(BUFFER_SIZE, '\0'); - SQLGetPrivateProfileString(wDsn.c_str(), wKey.data(), L"", &outputBuffer[0], - BUFFER_SIZE, L"odbc.ini"); - - std::wstring wValue = std::wstring(&outputBuffer[0]); - std::string value = arrow::util::WideStringToUTF8(wValue).ValueOr(""); - std::string key = arrow::util::WideStringToUTF8(std::wstring(wKey)).ValueOr(""); + driver::flight_sql::config::Configuration config; + config.LoadDsn(dsn); + Connection::ConnPropertyMap dsnProperties = config.GetProperties(); + for (auto& [key, value] : dsnProperties) { auto propIter = properties.find(key); if (propIter == properties.end()) { properties.emplace(std::make_pair(std::move(key), std::move(value))); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 471a2fffd80..81c4abe70cf 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -361,6 +361,79 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnect) { EXPECT_EQ(ret, SQL_SUCCESS); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLDriverConnectDsn) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Connect string + std::string connect_str = this->getConnectionString(); + + // Write connection string content into a DSN, + // must succeed before continuing + ASSERT_TRUE(writeDSN(connect_str)); + + std::string dsn(TEST_DSN); + ASSERT_OK_AND_ASSIGN(std::wstring wdsn, arrow::util::UTF8ToWideString(dsn)); + + // Update connection string to use DSN to connect + connect_str = std::string("DSN=") + std::string(TEST_DSN) + + std::string(";driver={Apache Arrow Flight SQL ODBC Driver};"); + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE] = L""; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Remove DSN + EXPECT_TRUE(UnregisterDsn(wdsn)); + + // Disconnect from ODBC + ret = SQLDisconnect(conn); + + if (ret != SQL_SUCCESS) { + std::cerr << GetOdbcErrorMessage(SQL_HANDLE_DBC, conn) << std::endl; + } + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + TEST_F(FlightSQLODBCRemoteTestBase, TestSQLDriverConnectInvalidUid) { // ODBC Environment SQLHENV env; From 4bfcee428f4587f8180fdd53d445726e3fdbc70a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:56:54 -0700 Subject: [PATCH 36/74] SQLColAttribute implementation * SQLColAttribute initial impl * Switch to use `GetAttributeSQLWCHAR` to be unicode-compatible * Add SQLColAttribute tests and fix bugs in `flightsql-odbc` impl * Fixed bug with incorrect column count. It was returning column count + 1. * Fixed bug with incorrect numPrecRadix. It was returning SQL_NO_TOTAL (maps to -4) for non-numeric columns, but the correct value is 0. * Fixed bug with unsigned column to return true for unsigned columns (non-numeric columns or unsigned numeric columns) and false for signed columns (numeric columns) * Fixed bug with incorrect non-concise data type return value for date time type types. The correct return is SQL_DATETIME for all date time types * Address comments from James * use const for `ConvertToWString` * add nullptr check in odbc_api.cc layer * Add support for `SQL_COLUMN_LENGTH`, `SQL_COLUMN_PRECISION`, and `SQL_COLUMN_SCALE`. The driver will return the same values for ODBC2 and ODBC3. * Add tests for ODBC v2 SQLColAttributes values * I converted tests that can be run on remote servers where possible * Address comment from Rob --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 14 +- .../flight_sql_result_set_metadata.cc | 17 +- .../flight_sql_result_set_metadata.h | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 95 ++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 4 + .../odbc_impl/odbc_descriptor.cc | 59 +- .../flight/sql/odbc/tests/columns_test.cc | 1176 +++++++++++++++++ .../flight/sql/odbc/tests/odbc_test_suite.cc | 13 + .../flight/sql/odbc/tests/odbc_test_suite.h | 6 + .../flight/sql/odbc/tests/statement_test.cc | 2 - 10 files changed, 1346 insertions(+), 41 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 4b9960f4489..9aacc844e0f 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -179,18 +179,14 @@ SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT stmt) { return SQL_ERROR; } -SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT columnNumber, +SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, - SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, + SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, SQLLEN* numericAttributePtr) { - LOG_DEBUG( - "SQLColAttributeW called with stmt: {}, columnNumber: {}, " - "fieldIdentifier: {}, characterAttributePtr: {}, bufferLength: {}, " - "stringLengthPtr: {}, numericAttributePtr: {}", - stmt, columnNumber, fieldIdentifier, characterAttributePtr, bufferLength, - fmt::ptr(stringLengthPtr), fmt::ptr(numericAttributePtr)); - return SQL_ERROR; + return arrow::SQLColAttribute(stmt, recordNumber, fieldIdentifier, + characterAttributePtr, bufferLength, outputLength, + numericAttributePtr); } SQLRETURN SQL_API SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc index 035390981c8..710f7608ecf 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc @@ -260,18 +260,29 @@ bool FlightSqlResultSetMetadata::IsUnsigned(int column_position) { const std::shared_ptr& field = schema_->field(column_position - 1); switch (field->type()->id()) { + case arrow::Type::INT8: + case arrow::Type::INT16: + case arrow::Type::INT32: + case arrow::Type::INT64: + case arrow::Type::DOUBLE: + case arrow::Type::FLOAT: + case arrow::Type::HALF_FLOAT: + case arrow::Type::DECIMAL32: + case arrow::Type::DECIMAL64: + case arrow::Type::DECIMAL128: + case arrow::Type::DECIMAL256: + return false; case arrow::Type::UINT8: case arrow::Type::UINT16: case arrow::Type::UINT32: case arrow::Type::UINT64: - return true; default: - return false; + return true; } } bool FlightSqlResultSetMetadata::IsFixedPrecScale(int column_position) { - // TODO: Flight SQL column metadata does not have this, should we add to the spec? + // Precision for Arrow data types are modifiable by the user return false; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.h b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.h index f8e78eb2d6d..29901652c52 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.h +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.h @@ -89,6 +89,7 @@ class FlightSqlResultSetMetadata : public odbcabstraction::ResultSetMetadata { odbcabstraction::Searchability IsSearchable(int column_position) override; + /// \brief Returns true if the column is unsigned (not numeric) bool IsUnsigned(int column_position) override; bool IsFixedPrecScale(int column_position) override; diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 5553db10276..69c6059d1d0 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1147,4 +1147,99 @@ SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNa return SQL_SUCCESS; }); } + +SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, + SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, + SQLLEN* numericAttributePtr) { + LOG_DEBUG( + "SQLColAttributeW called with stmt: {}, recordNumber: {}, " + "fieldIdentifier: {}, characterAttributePtr: {}, bufferLength: {}, " + "outputLength: {}, numericAttributePtr: {}", + stmt, recordNumber, fieldIdentifier, characterAttributePtr, bufferLength, + fmt::ptr(outputLength), fmt::ptr(numericAttributePtr)); + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + ODBCDescriptor* ird = statement->GetIRD(); + SQLINTEGER outputLengthInt; + switch (fieldIdentifier) { + // Numeric attributes + // internal is SQLLEN, no conversion is needed + case SQL_DESC_DISPLAY_SIZE: + case SQL_DESC_OCTET_LENGTH: { + ird->GetField(recordNumber, fieldIdentifier, numericAttributePtr, bufferLength, + &outputLengthInt); + break; + } + // internal is SQLULEN, conversion is needed. + case SQL_COLUMN_LENGTH: // ODBC 2.0 + case SQL_DESC_LENGTH: { + SQLULEN temp; + ird->GetField(recordNumber, fieldIdentifier, &temp, bufferLength, + &outputLengthInt); + if (numericAttributePtr) { + *numericAttributePtr = static_cast(temp); + } + break; + } + // internal is SQLINTEGER, conversion is needed. + case SQL_DESC_AUTO_UNIQUE_VALUE: + case SQL_DESC_CASE_SENSITIVE: + case SQL_DESC_NUM_PREC_RADIX: { + SQLINTEGER temp; + ird->GetField(recordNumber, fieldIdentifier, &temp, bufferLength, + &outputLengthInt); + if (numericAttributePtr) { + *numericAttributePtr = static_cast(temp); + } + break; + } + // internal is SQLSMALLINT, conversion is needed. + case SQL_DESC_CONCISE_TYPE: + case SQL_DESC_COUNT: + case SQL_DESC_FIXED_PREC_SCALE: + case SQL_DESC_TYPE: + case SQL_DESC_NULLABLE: + case SQL_COLUMN_PRECISION: // ODBC 2.0 + case SQL_DESC_PRECISION: + case SQL_COLUMN_SCALE: // ODBC 2.0 + case SQL_DESC_SCALE: + case SQL_DESC_SEARCHABLE: + case SQL_DESC_UNNAMED: + case SQL_DESC_UNSIGNED: + case SQL_DESC_UPDATABLE: { + SQLSMALLINT temp; + ird->GetField(recordNumber, fieldIdentifier, &temp, bufferLength, + &outputLengthInt); + if (numericAttributePtr) { + *numericAttributePtr = static_cast(temp); + } + break; + } + // Character attributes + case SQL_DESC_BASE_COLUMN_NAME: + case SQL_DESC_BASE_TABLE_NAME: + case SQL_DESC_CATALOG_NAME: + case SQL_DESC_LABEL: + case SQL_DESC_LITERAL_PREFIX: + case SQL_DESC_LITERAL_SUFFIX: + case SQL_DESC_LOCAL_TYPE_NAME: + case SQL_DESC_NAME: + case SQL_DESC_SCHEMA_NAME: + case SQL_DESC_TABLE_NAME: + case SQL_DESC_TYPE_NAME: + ird->GetField(recordNumber, fieldIdentifier, characterAttributePtr, bufferLength, + &outputLengthInt); + break; + default: + throw DriverException("Invalid descriptor field", "HY091"); + } + if (outputLength) { + *outputLength = static_cast(outputLengthInt); + } + return SQL_SUCCESS; + }); +} } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index b1b5c5a3033..dec6603201d 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -83,4 +83,8 @@ SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNa SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* columnName, SQLSMALLINT columnNameLength); +SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, + SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, + SQLLEN* numericAttributePtr); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_descriptor.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_descriptor.cc index b578bea3609..97b31bb550e 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_descriptor.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_descriptor.cc @@ -275,7 +275,9 @@ void ODBCDescriptor::GetHeaderField(SQLSMALLINT fieldIdentifier, SQLPOINTER valu GetAttribute(m_rowsProccessedPtr, value, bufferLength, outputLength); break; case SQL_DESC_COUNT: { - GetAttribute(m_highestOneBasedBoundRecord, value, bufferLength, outputLength); + // m_highestOneBasedBoundRecord equals number of records + 1 + GetAttribute(static_cast(m_highestOneBasedBoundRecord - 1), value, + bufferLength, outputLength); break; } default: @@ -311,52 +313,53 @@ void ODBCDescriptor::GetField(SQLSMALLINT recordNumber, SQLSMALLINT fieldIdentif // TODO: Restrict fields based on AppDescriptor IPD, and IRD. + bool lengthInBytes = true; SQLSMALLINT zeroBasedRecord = recordNumber - 1; const DescriptorRecord& record = m_records[zeroBasedRecord]; switch (fieldIdentifier) { case SQL_DESC_BASE_COLUMN_NAME: - GetAttributeUTF8(record.m_baseColumnName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_baseColumnName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_BASE_TABLE_NAME: - GetAttributeUTF8(record.m_baseTableName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_baseTableName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_CATALOG_NAME: - GetAttributeUTF8(record.m_catalogName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_catalogName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_LABEL: - GetAttributeUTF8(record.m_label, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_label, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_LITERAL_PREFIX: - GetAttributeUTF8(record.m_literalPrefix, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_literalPrefix, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_LITERAL_SUFFIX: - GetAttributeUTF8(record.m_literalSuffix, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_literalSuffix, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_LOCAL_TYPE_NAME: - GetAttributeUTF8(record.m_localTypeName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_localTypeName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_NAME: - GetAttributeUTF8(record.m_name, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_name, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_SCHEMA_NAME: - GetAttributeUTF8(record.m_schemaName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_schemaName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_TABLE_NAME: - GetAttributeUTF8(record.m_tableName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_tableName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_TYPE_NAME: - GetAttributeUTF8(record.m_typeName, value, bufferLength, outputLength, - GetDiagnostics()); + GetAttributeSQLWCHAR(record.m_typeName, lengthInBytes, value, bufferLength, + outputLength, GetDiagnostics()); break; case SQL_DESC_DATA_PTR: @@ -366,7 +369,7 @@ void ODBCDescriptor::GetField(SQLSMALLINT recordNumber, SQLSMALLINT fieldIdentif case SQL_DESC_OCTET_LENGTH_PTR: GetAttribute(record.m_indicatorPtr, value, bufferLength, outputLength); break; - + case SQL_COLUMN_LENGTH: // ODBC 2.0 case SQL_DESC_LENGTH: GetAttribute(record.m_length, value, bufferLength, outputLength); break; @@ -405,12 +408,14 @@ void ODBCDescriptor::GetField(SQLSMALLINT recordNumber, SQLSMALLINT fieldIdentif case SQL_DESC_PARAMETER_TYPE: GetAttribute(record.m_paramType, value, bufferLength, outputLength); break; + case SQL_COLUMN_PRECISION: // ODBC 2.0 case SQL_DESC_PRECISION: GetAttribute(record.m_precision, value, bufferLength, outputLength); break; case SQL_DESC_ROWVER: GetAttribute(record.m_rowVer, value, bufferLength, outputLength); break; + case SQL_COLUMN_SCALE: // ODBC 2.0 case SQL_DESC_SCALE: GetAttribute(record.m_scale, value, bufferLength, outputLength); break; @@ -500,7 +505,8 @@ void ODBCDescriptor::PopulateFromResultSetMetadata(ResultSetMetadata* rsmd) { m_records[i].m_caseSensitive = rsmd->IsCaseSensitive(oneBasedIndex) ? SQL_TRUE : SQL_FALSE; m_records[i].m_datetimeIntervalPrecision; // TODO - update when rsmd adds this - m_records[i].m_numPrecRadix = rsmd->GetNumPrecRadix(oneBasedIndex); + SQLINTEGER numPrecRadix = rsmd->GetNumPrecRadix(oneBasedIndex); + m_records[i].m_numPrecRadix = numPrecRadix > 0 ? numPrecRadix : 0; m_records[i].m_datetimeIntervalCode; // TODO m_records[i].m_fixedPrecScale = rsmd->IsFixedPrecScale(oneBasedIndex) ? SQL_TRUE : SQL_FALSE; @@ -510,8 +516,7 @@ void ODBCDescriptor::PopulateFromResultSetMetadata(ResultSetMetadata* rsmd) { m_records[i].m_rowVer = SQL_FALSE; m_records[i].m_scale = rsmd->GetScale(oneBasedIndex); m_records[i].m_searchable = rsmd->IsSearchable(oneBasedIndex); - m_records[i].m_type = - GetSqlTypeForODBCVersion(rsmd->GetDataType(oneBasedIndex), m_is2xConnection); + m_records[i].m_type = rsmd->GetDataType(oneBasedIndex); m_records[i].m_unnamed = m_records[i].m_name.empty() ? SQL_TRUE : SQL_FALSE; m_records[i].m_unsigned = rsmd->IsUnsigned(oneBasedIndex) ? SQL_TRUE : SQL_FALSE; m_records[i].m_updatable = rsmd->GetUpdatable(oneBasedIndex); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index a05b8cd0f3a..8874572ec63 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -30,6 +30,7 @@ // ODBC 3. namespace arrow::flight::sql::odbc { +// Helper functions void checkSQLColumns( SQLHSTMT stmt, const std::wstring& expectedTable, const std::wstring& expectedColumn, const SQLINTEGER& expectedDataType, const std::wstring& expectedTypeName, @@ -102,6 +103,269 @@ void checkRemoteSQLColumns( expectedIsNullable); } +void checkSQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT idx, + const std::wstring& expectedColmnName, SQLLEN expectedDataType, + SQLLEN expectedConciseType, SQLLEN expectedDisplaySize, + SQLLEN expectedPrecScale, SQLLEN expectedLength, + const std::wstring& expectedLiteralPrefix, + const std::wstring& expectedLiteralSuffix, + SQLLEN expectedColumnSize, SQLLEN expectedColumnScale, + SQLLEN expectedColumnNullability, SQLLEN expectedNumPrecRadix, + SQLLEN expectedOctetLength, SQLLEN expectedSearchable, + SQLLEN expectedUnsignedColumn) { + std::vector name(ODBC_BUFFER_SIZE); + SQLSMALLINT nameLen = 0; + std::vector baseColumnName(ODBC_BUFFER_SIZE); + SQLSMALLINT columnNameLen = 0; + std::vector label(ODBC_BUFFER_SIZE); + SQLSMALLINT labelLen = 0; + std::vector prefix(ODBC_BUFFER_SIZE); + SQLSMALLINT prefixLen = 0; + std::vector suffix(ODBC_BUFFER_SIZE); + SQLSMALLINT suffixLen = 0; + SQLLEN dataType = 0; + SQLLEN conciseType = 0; + SQLLEN displaySize = 0; + SQLLEN precScale = 0; + SQLLEN length = 0; + SQLLEN size = 0; + SQLLEN scale = 0; + SQLLEN nullability = 0; + SQLLEN numPrecRadix = 0; + SQLLEN octetLength = 0; + SQLLEN searchable = 0; + SQLLEN unsignedCol = 0; + + SQLRETURN ret = SQLColAttribute(stmt, idx, SQL_DESC_NAME, &name[0], + (SQLSMALLINT)name.size(), &nameLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_BASE_COLUMN_NAME, &baseColumnName[0], + (SQLSMALLINT)baseColumnName.size(), &columnNameLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_LABEL, &label[0], (SQLSMALLINT)label.size(), + &labelLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_TYPE, 0, 0, 0, &dataType); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_CONCISE_TYPE, 0, 0, 0, &conciseType); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_DISPLAY_SIZE, 0, 0, 0, &displaySize); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_FIXED_PREC_SCALE, 0, 0, 0, &precScale); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_LENGTH, 0, 0, 0, &length); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_LITERAL_PREFIX, &prefix[0], + (SQLSMALLINT)prefix.size(), &prefixLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_LITERAL_SUFFIX, &suffix[0], + (SQLSMALLINT)suffix.size(), &suffixLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_PRECISION, 0, 0, 0, &size); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_SCALE, 0, 0, 0, &scale); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_NULLABLE, 0, 0, 0, &nullability); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_NUM_PREC_RADIX, 0, 0, 0, &numPrecRadix); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_OCTET_LENGTH, 0, 0, 0, &octetLength); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_SEARCHABLE, 0, 0, 0, &searchable); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_DESC_UNSIGNED, 0, 0, 0, &unsignedCol); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::wstring nameStr = ConvertToWString(name, nameLen); + std::wstring baseColumnNameStr = ConvertToWString(baseColumnName, columnNameLen); + std::wstring labelStr = ConvertToWString(label, labelLen); + std::wstring prefixStr = ConvertToWString(prefix, prefixLen); + + // Assume column name, base column name, and label are equivalent in the result set + EXPECT_EQ(nameStr, expectedColmnName); + EXPECT_EQ(baseColumnNameStr, expectedColmnName); + EXPECT_EQ(labelStr, expectedColmnName); + EXPECT_EQ(dataType, expectedDataType); + EXPECT_EQ(conciseType, expectedConciseType); + EXPECT_EQ(displaySize, expectedDisplaySize); + EXPECT_EQ(precScale, expectedPrecScale); + EXPECT_EQ(length, expectedLength); + EXPECT_EQ(prefixStr, expectedLiteralPrefix); + EXPECT_EQ(size, expectedColumnSize); + EXPECT_EQ(scale, expectedColumnScale); + EXPECT_EQ(nullability, expectedColumnNullability); + EXPECT_EQ(numPrecRadix, expectedNumPrecRadix); + EXPECT_EQ(octetLength, expectedOctetLength); + EXPECT_EQ(searchable, expectedSearchable); + EXPECT_EQ(unsignedCol, expectedUnsignedColumn); +} + +void checkSQLColAttributes(SQLHSTMT stmt, SQLUSMALLINT idx, + const std::wstring& expectedColmnName, SQLLEN expectedDataType, + SQLLEN expectedDisplaySize, SQLLEN expectedPrecScale, + SQLLEN expectedLength, SQLLEN expectedColumnSize, + SQLLEN expectedColumnScale, SQLLEN expectedColumnNullability, + SQLLEN expectedSearchable, SQLLEN expectedUnsignedColumn) { + std::vector name(ODBC_BUFFER_SIZE); + SQLSMALLINT nameLen = 0; + std::vector label(ODBC_BUFFER_SIZE); + SQLSMALLINT labelLen = 0; + SQLLEN dataType = 0; + SQLLEN displaySize = 0; + SQLLEN precScale = 0; + SQLLEN length = 0; + SQLLEN size = 0; + SQLLEN scale = 0; + SQLLEN nullability = 0; + SQLLEN searchable = 0; + SQLLEN unsignedCol = 0; + + SQLRETURN ret = SQLColAttributes(stmt, idx, SQL_COLUMN_NAME, &name[0], + (SQLSMALLINT)name.size(), &nameLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_LABEL, &label[0], + (SQLSMALLINT)label.size(), &labelLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_TYPE, 0, 0, 0, &dataType); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_DISPLAY_SIZE, 0, 0, 0, &displaySize); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(stmt, idx, SQL_COLUMN_MONEY, 0, 0, 0, &precScale); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_LENGTH, 0, 0, 0, &length); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_PRECISION, 0, 0, 0, &size); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_SCALE, 0, 0, 0, &scale); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_NULLABLE, 0, 0, 0, &nullability); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_SEARCHABLE, 0, 0, 0, &searchable); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttributes(stmt, idx, SQL_COLUMN_UNSIGNED, 0, 0, 0, &unsignedCol); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::wstring nameStr = ConvertToWString(name, nameLen); + std::wstring labelStr = ConvertToWString(label, labelLen); + + EXPECT_EQ(nameStr, expectedColmnName); + EXPECT_EQ(labelStr, expectedColmnName); + EXPECT_EQ(dataType, expectedDataType); + EXPECT_EQ(displaySize, expectedDisplaySize); + EXPECT_EQ(length, expectedLength); + EXPECT_EQ(size, expectedColumnSize); + EXPECT_EQ(scale, expectedColumnScale); + EXPECT_EQ(nullability, expectedColumnNullability); + EXPECT_EQ(searchable, expectedSearchable); + EXPECT_EQ(unsignedCol, expectedUnsignedColumn); +} + +void checkSQLColAttributeString(SQLHSTMT stmt, const std::wstring& wsql, SQLUSMALLINT idx, + SQLUSMALLINT fieldIdentifier, + const std::wstring& expectedAttrString) { + // Execute query and check SQLColAttribute string attribute + std::vector sql0(wsql.begin(), wsql.end()); + SQLRETURN ret = SQLExecDirect(stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::vector strVal(ODBC_BUFFER_SIZE); + SQLSMALLINT strLen = 0; + + ret = SQLColAttribute(stmt, idx, fieldIdentifier, &strVal[0], + (SQLSMALLINT)strVal.size(), &strLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::wstring attrStr = ConvertToWString(strVal, strLen); + EXPECT_EQ(attrStr, expectedAttrString); +} + +void checkSQLColAttributeNumeric(SQLHSTMT stmt, const std::wstring& wsql, + SQLUSMALLINT idx, SQLUSMALLINT fieldIdentifier, + SQLLEN expectedAttrNumeric) { + // Execute query and check SQLColAttribute numeric attribute + std::vector sql0(wsql.begin(), wsql.end()); + SQLRETURN ret = SQLExecDirect(stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLLEN numVal = 0; + ret = SQLColAttribute(stmt, idx, fieldIdentifier, 0, 0, 0, &numVal); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(numVal, expectedAttrNumeric); +} + +void checkSQLColAttributesString(SQLHSTMT stmt, const std::wstring& wsql, + SQLUSMALLINT idx, SQLUSMALLINT fieldIdentifier, + const std::wstring& expectedAttrString) { + // Execute query and check ODBC 2.0 API SQLColAttributes string attribute + std::vector sql0(wsql.begin(), wsql.end()); + SQLRETURN ret = SQLExecDirect(stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::vector strVal(ODBC_BUFFER_SIZE); + SQLSMALLINT strLen = 0; + + ret = SQLColAttributes(stmt, idx, fieldIdentifier, &strVal[0], + (SQLSMALLINT)strVal.size(), &strLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + std::wstring attrStr = ConvertToWString(strVal, strLen); + EXPECT_EQ(attrStr, expectedAttrString); +} + +void checkSQLColAttributesNumeric(SQLHSTMT stmt, const std::wstring& wsql, + SQLUSMALLINT idx, SQLUSMALLINT fieldIdentifier, + SQLLEN expectedAttrNumeric) { + // Execute query and check ODBC 2.0 API SQLColAttributes numeric attribute + std::vector sql0(wsql.begin(), wsql.end()); + SQLRETURN ret = SQLExecDirect(stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLLEN numVal = 0; + ret = SQLColAttributes(stmt, idx, fieldIdentifier, 0, 0, 0, &numVal); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(numVal, expectedAttrNumeric); +} + TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsAllColumns) { // Check table pattern and column pattern returns all columns this->connect(); @@ -981,4 +1245,916 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsInvalidTablePattern) { this->disconnect(); } +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeAllTypes) { + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLColAttribute(this->stmt, 1, + std::wstring(L"bigint_col"), // expectedColmnName + SQL_BIGINT, // expectedDataType + SQL_BIGINT, // expectedConciseType + 20, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 2, + std::wstring(L"char_col"), // expectedColmnName + SQL_WVARCHAR, // expectedDataType + SQL_WVARCHAR, // expectedConciseType + 0, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 0, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 0, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 0, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 3, + std::wstring(L"varbinary_col"), // expectedColmnName + SQL_BINARY, // expectedDataType + SQL_BINARY, // expectedConciseType + 0, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 0, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 0, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 0, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 4, + std::wstring(L"double_col"), // expectedColmnName + SQL_DOUBLE, // expectedDataType + SQL_DOUBLE, // expectedConciseType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 2, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesAllTypesODBCVer2) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + checkSQLColAttributes(this->stmt, 1, + std::wstring(L"bigint_col"), // expectedColmnName + SQL_BIGINT, // expectedDataType + 20, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 2, + std::wstring(L"char_col"), // expectedColmnName + SQL_WVARCHAR, // expectedDataType + 0, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 0, // expectedLength + 0, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 3, + std::wstring(L"varbinary_col"), // expectedColmnName + SQL_BINARY, // expectedDataType + 0, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 0, // expectedLength + 0, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 4, + std::wstring(L"double_col"), // expectedColmnName + SQL_DOUBLE, // expectedDataType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLColAttribute(this->stmt, 1, + std::wstring(L"sinteger_max"), // expectedColmnName + SQL_INTEGER, // expectedDataType + SQL_INTEGER, // expectedConciseType + 11, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 4, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 4, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 4, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 2, + std::wstring(L"sbigint_max"), // expectedColmnName + SQL_BIGINT, // expectedDataType + SQL_BIGINT, // expectedConciseType + 20, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 3, + std::wstring(L"decimal_positive"), // expectedColmnName + SQL_DECIMAL, // expectedDataType + SQL_DECIMAL, // expectedConciseType + 40, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 19, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 19, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 40, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 4, + std::wstring(L"float_max"), // expectedColmnName + SQL_FLOAT, // expectedDataType + SQL_FLOAT, // expectedConciseType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 2, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 5, + std::wstring(L"double_max"), // expectedColmnName + SQL_DOUBLE, // expectedDataType + SQL_DOUBLE, // expectedConciseType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 2, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 6, + std::wstring(L"bit_true"), // expectedColmnName + SQL_BIT, // expectedDataType + SQL_BIT, // expectedConciseType + 1, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 1, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 1, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 1, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 7, + std::wstring(L"date_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_TYPE_DATE, // expectedConciseType + 10, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 10, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 10, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 6, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 8, + std::wstring(L"time_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_TYPE_TIME, // expectedConciseType + 12, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 12, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 12, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 6, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 9, + std::wstring(L"timestamp_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_TYPE_TIMESTAMP, // expectedConciseType + 23, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 23, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 23, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 16, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLColAttribute(this->stmt, 1, + std::wstring(L"sinteger_max"), // expectedColmnName + SQL_INTEGER, // expectedDataType + SQL_INTEGER, // expectedConciseType + 11, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 4, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 4, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 4, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 2, + std::wstring(L"sbigint_max"), // expectedColmnName + SQL_BIGINT, // expectedDataType + SQL_BIGINT, // expectedConciseType + 20, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 3, + std::wstring(L"decimal_positive"), // expectedColmnName + SQL_DECIMAL, // expectedDataType + SQL_DECIMAL, // expectedConciseType + 40, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 19, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 19, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 10, // expectedNumPrecRadix + 40, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 4, + std::wstring(L"float_max"), // expectedColmnName + SQL_FLOAT, // expectedDataType + SQL_FLOAT, // expectedConciseType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 2, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 5, + std::wstring(L"double_max"), // expectedColmnName + SQL_DOUBLE, // expectedDataType + SQL_DOUBLE, // expectedConciseType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 2, // expectedNumPrecRadix + 8, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 6, + std::wstring(L"bit_true"), // expectedColmnName + SQL_BIT, // expectedDataType + SQL_BIT, // expectedConciseType + 1, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 1, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 1, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 1, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 7, + std::wstring(L"date_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_DATE, // expectedConciseType + 10, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 10, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 10, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 6, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 8, + std::wstring(L"time_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_TIME, // expectedConciseType + 12, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 12, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 12, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 6, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttribute(this->stmt, 9, + std::wstring(L"timestamp_max"), // expectedColmnName + SQL_DATETIME, // expectedDataType + SQL_TIMESTAMP, // expectedConciseType + 23, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 23, // expectedLength + std::wstring(L""), // expectedLiteralPrefix + std::wstring(L""), // expectedLiteralSuffix + 23, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + 0, // expectedNumPrecRadix + 16, // expectedOctetLength + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { + // Tests ODBC 2.0 API SQLColAttributes + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLColAttributes(this->stmt, 1, + std::wstring(L"sinteger_max"), // expectedColmnName + SQL_INTEGER, // expectedDataType + 11, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 4, // expectedLength + 4, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 2, + std::wstring(L"sbigint_max"), // expectedColmnName + SQL_BIGINT, // expectedDataType + 20, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 3, + std::wstring(L"decimal_positive"), // expectedColmnName + SQL_DECIMAL, // expectedDataType + 40, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 19, // expectedLength + 19, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 4, + std::wstring(L"float_max"), // expectedColmnName + SQL_FLOAT, // expectedDataType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 5, + std::wstring(L"double_max"), // expectedColmnName + SQL_DOUBLE, // expectedDataType + 24, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 8, // expectedLength + 8, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_FALSE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 6, + std::wstring(L"bit_true"), // expectedColmnName + SQL_BIT, // expectedDataType + 1, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 1, // expectedLength + 1, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 7, + std::wstring(L"date_max"), // expectedColmnName + SQL_DATE, // expectedDataType + 10, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 10, // expectedLength + 10, // expectedColumnSize + 0, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 8, + std::wstring(L"time_max"), // expectedColmnName + SQL_TIME, // expectedDataType + 12, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 12, // expectedLength + 12, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + checkSQLColAttributes(this->stmt, 9, + std::wstring(L"timestamp_max"), // expectedColmnName + SQL_TIMESTAMP, // expectedDataType + 23, // expectedDisplaySize + SQL_FALSE, // expectedPrecScale + 23, // expectedLength + 23, // expectedColumnSize + 3, // expectedColumnScale + SQL_NULLABLE, // expectedColumnNullability + SQL_PRED_NONE, // expectedSearchable + SQL_TRUE); // expectedUnsignedColumn + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributeCaseSensitive) { + // Arrow limitation: returns SQL_FALSE for case sensitive column + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Int column + checkSQLColAttributeNumeric(this->stmt, wsql, 1, SQL_DESC_CASE_SENSITIVE, SQL_FALSE); + SQLFreeStmt(this->stmt, SQL_CLOSE); + // Varchar column + checkSQLColAttributeNumeric(this->stmt, wsql, 28, SQL_DESC_CASE_SENSITIVE, SQL_FALSE); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributesCaseSensitive) { + // Arrow limitation: returns SQL_FALSE for case sensitive column + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Int column + checkSQLColAttributesNumeric(this->stmt, wsql, 1, SQL_COLUMN_CASE_SENSITIVE, SQL_FALSE); + SQLFreeStmt(this->stmt, SQL_CLOSE); + // Varchar column + checkSQLColAttributesNumeric(this->stmt, wsql, 28, SQL_COLUMN_CASE_SENSITIVE, + SQL_FALSE); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeUniqueValue) { + // Mock server limitation: returns false for auto-increment column + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeNumeric(this->stmt, wsql, 1, SQL_DESC_AUTO_UNIQUE_VALUE, SQL_FALSE); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesAutoIncrement) { + // Tests ODBC 2.0 API SQLColAttributes + // Mock server limitation: returns false for auto-increment column + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeNumeric(this->stmt, wsql, 1, SQL_COLUMN_AUTO_INCREMENT, SQL_FALSE); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeBaseTableName) { + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_BASE_TABLE_NAME, + std::wstring(L"AllTypesTable")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesTableName) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributesString(this->stmt, wsql, 1, SQL_COLUMN_TABLE_NAME, + std::wstring(L"AllTypesTable")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeCatalogName) { + // Mock server limitattion: mock doesn't return catalog for result metadata, + // and the defautl catalog should be 'main' + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_CATALOG_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeCatalogName) { + // Remote server does not have catalogs + this->connect(); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_CATALOG_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesQualifierName) { + // Mock server limitattion: mock doesn't return catalog for result metadata, + // and the defautl catalog should be 'main' + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_COLUMN_QUALIFIER_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesQualifierName) { + // Remote server does not have catalogs + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_COLUMN_QUALIFIER_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributeCount) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Pass 0 as column number, driver should ignore it + checkSQLColAttributeNumeric(this->stmt, wsql, 0, SQL_DESC_COUNT, 32); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeLocalTypeName) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Mock server doesn't have local type name + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_LOCAL_TYPE_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeLocalTypeName) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_LOCAL_TYPE_NAME, + std::wstring(L"INTEGER")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeSchemaName) { + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + // Mock server doesn't have schemas + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_SCHEMA_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeSchemaName) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + // Remote server limitation: doesn't return schema name, expected schema name is + // $scratch + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_SCHEMA_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesOwnerName) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + // Mock server doesn't have schemas + checkSQLColAttributesString(this->stmt, wsql, 1, SQL_COLUMN_OWNER_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesOwnerName) { + // Test assumes there is a table $scratch.ODBCTest in remote server + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + // Remote server limitation: doesn't return schema name, expected schema name is + // $scratch + checkSQLColAttributesString(this->stmt, wsql, 1, SQL_COLUMN_OWNER_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeTableName) { + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_TABLE_NAME, + std::wstring(L"AllTypesTable")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeTypeName) { + this->connect(); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + // Mock server doesn't return data source-dependent data type name + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_TYPE_NAME, std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeTypeName) { + this->connect(); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + checkSQLColAttributeString(this->stmt, wsql, 1, SQL_DESC_TYPE_NAME, + std::wstring(L"INTEGER")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributesTypeName) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + this->CreateTableAllDataType(); + + std::wstring wsql = L"SELECT * from AllTypesTable;"; + // Mock server doesn't return data source-dependent data type name + checkSQLColAttributesString(this->stmt, wsql, 1, SQL_COLUMN_TYPE_NAME, + std::wstring(L"")); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesTypeName) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT * from $scratch.ODBCTest;"; + checkSQLColAttributesString(this->stmt, wsql, 1, SQL_COLUMN_TYPE_NAME, + std::wstring(L"INTEGER")); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributeUnnamed) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + checkSQLColAttributeNumeric(this->stmt, wsql, 1, SQL_DESC_UNNAMED, SQL_NAMED); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributeUpdatable) { + this->connect(); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Mock server and remote server do not return updatable information + checkSQLColAttributeNumeric(this->stmt, wsql, 1, SQL_DESC_UPDATABLE, + SQL_ATTR_READWRITE_UNKNOWN); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributesUpdatable) { + // Tests ODBC 2.0 API SQLColAttributes + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = this->getQueryAllDataTypes(); + // Mock server and remote server do not return updatable information + checkSQLColAttributesNumeric(this->stmt, wsql, 1, SQL_COLUMN_UPDATABLE, + SQL_ATTR_READWRITE_UNKNOWN); + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index e39cce54e41..e0062f06da1 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -408,6 +408,19 @@ bool writeDSN(Connection::ConnPropertyMap properties) { return RegisterDsn(config, wDriver.c_str()); } +std::wstring ConvertToWString(const std::vector& strVal, SQLSMALLINT strLen) { + std::wstring attrStr; + if (strLen == 0) { + attrStr = std::wstring(&strVal[0]); + } else { + EXPECT_GT(strLen, 0); + EXPECT_LE(strLen, static_cast(ODBC_BUFFER_SIZE)); + attrStr = + std::wstring(strVal.begin(), strVal.begin() + strLen / ODBC::GetSqlWCharSize()); + } + return attrStr; +} + void CheckStringColumnW(SQLHSTMT stmt, int colId, const std::wstring& expected) { SQLWCHAR buf[1024]; SQLLEN bufLen = sizeof(buf) * ODBC::GetSqlWCharSize(); 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 a93a633d754..101c2fe0566 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 @@ -192,6 +192,12 @@ bool writeDSN(std::string connection_str); /// \return true on success bool writeDSN(Connection::ConnPropertyMap properties); +/// \brief Check wide char vector and convert into wstring +/// \param[in] strVal Vector of SQLWCHAR. +/// \param[in] strLen length of string, in bytes. +/// \return wstring +std::wstring ConvertToWString(const std::vector& strVal, SQLSMALLINT strLen); + /// \brief Check wide string column. /// \param[in] stmt Statement. /// \param[in] colId Column ID to check. 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 a490457c3a3..000e0b05a71 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -30,7 +30,6 @@ #include "gtest/gtest.h" namespace arrow::flight::sql::odbc { - TYPED_TEST(FlightSQLODBCTestBase, TestSQLExecDirectSimpleQuery) { this->connect(); @@ -2235,5 +2234,4 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExtendedFetchQueryNullIndicator) { this->disconnect(); } - } // namespace arrow::flight::sql::odbc From 40375f8ba4dfc1166153c298acc8b29fce9e3b5a Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 24 Jul 2025 18:11:39 -0700 Subject: [PATCH 37/74] Implement SQLTables --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 27 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 31 + cpp/src/arrow/flight/sql/odbc/odbc_api.h | 4 + .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/odbc_test_suite.cc | 19 + .../flight/sql/odbc/tests/odbc_test_suite.h | 8 + .../flight/sql/odbc/tests/tables_test.cc | 585 ++++++++++++++++++ 7 files changed, 658 insertions(+), 17 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 9aacc844e0f..6ddd2481e9b 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -189,6 +189,16 @@ SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, numericAttributePtr); } +SQLRETURN SQL_API SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, + SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, + SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, + SQLSMALLINT tableNameLength, SQLWCHAR* tableType, + SQLSMALLINT tableTypeLength) { + return arrow::SQLTables(stmt, catalogName, catalogNameLength, schemaName, + schemaNameLength, tableName, tableNameLength, tableType, + tableTypeLength); +} + SQLRETURN SQL_API SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, @@ -278,20 +288,3 @@ SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER SQLINTEGER stringLength) { return arrow::SQLSetStmtAttr(stmt, attribute, valuePtr, stringLength); } - -SQLRETURN SQL_API SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, - SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, - SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, - SQLSMALLINT tableNameLength, SQLWCHAR* tableType, - SQLSMALLINT tableTypeLength) { - LOG_DEBUG( - "SQLTablesW called with stmt: {}, catalogName: {}, catalogNameLength: " - "{}, " - "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " - "tableType: {}, " - "tableTypeLength: {}", - stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), - schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(tableType), - tableTypeLength); - return SQL_ERROR; -} diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 69c6059d1d0..f5cac728c09 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1113,6 +1113,37 @@ SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { }); } +SQLRETURN SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, + SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, + SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* tableType, + SQLSMALLINT tableTypeLength) { + LOG_DEBUG( + "SQLTables called with stmt: {}, catalogName: {}, catalogNameLength: " + "{}, " + "schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}, " + "tableType: {}, " + "tableTypeLength: {}", + stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), + schemaNameLength, fmt::ptr(tableName), tableNameLength, fmt::ptr(tableType), + tableTypeLength); + using ODBC::ODBCStatement; + using ODBC::SqlWcharToString; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + std::string catalog = SqlWcharToString(catalogName, catalogNameLength); + std::string schema = SqlWcharToString(schemaName, schemaNameLength); + std::string table = SqlWcharToString(tableName, tableNameLength); + std::string type = SqlWcharToString(tableType, tableTypeLength); + + statement->GetTables(catalogName ? &catalog : nullptr, schemaName ? &schema : nullptr, + tableName ? &table : nullptr, tableType ? &type : nullptr); + + return SQL_SUCCESS; + }); +} + SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index dec6603201d..05f8d1a74f8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -79,6 +79,10 @@ SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType SQLRETURN SQLMoreResults(SQLHSTMT stmt); SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr); SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr); +SQLRETURN SQLTables(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, + SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, + SQLWCHAR* tableName, SQLSMALLINT tableNameLength, SQLWCHAR* tableType, + SQLSMALLINT tableTypeLength); SQLRETURN SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, SQLSMALLINT catalogNameLength, SQLWCHAR* schemaName, SQLSMALLINT schemaNameLength, SQLWCHAR* tableName, SQLSMALLINT tableNameLength, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 3bb1ac697e9..d5662d088ca 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -36,6 +36,7 @@ add_arrow_test(flight_sql_odbc_test connection_info_test.cc statement_attr_test.cc statement_test.cc + tables_test.cc # Connection test needs to be put last to resolve segfault issue connection_test.cc odbc_test_suite.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc index e0062f06da1..bb92bee0713 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/odbc_test_suite.cc @@ -274,6 +274,19 @@ std::wstring FlightSQLODBCMockTestBase::getQueryAllDataTypes() { return wsql; } +void FlightSQLODBCMockTestBase::CreateTestTables() { + ASSERT_OK(server->ExecuteSql(R"( + CREATE TABLE TestTable ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyName varchar(100), + value int); + + INSERT INTO TestTable (keyName, value) VALUES ('One', 1); + INSERT INTO TestTable (keyName, value) VALUES ('Two', 0); + INSERT INTO TestTable (keyName, value) VALUES ('Three', -1); + )")); +} + void FlightSQLODBCMockTestBase::CreateTableAllDataType() { // Limitation on mock SQLite server: // Only int64, float64, binary, and utf8 Arrow Types are supported by @@ -467,4 +480,10 @@ void CheckSmallIntColumn(SQLHSTMT stmt, int colId, const SQLSMALLINT& expected) EXPECT_EQ(buf, expected); } +void ValidateFetch(SQLHSTMT stmt, SQLRETURN expectedReturn) { + SQLRETURN ret = SQLFetch(stmt); + + EXPECT_EQ(ret, expectedReturn); +} + } // namespace arrow::flight::sql::odbc 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 101c2fe0566..ffaa33c3a10 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 @@ -125,6 +125,9 @@ class FlightSQLODBCMockTestBase : public FlightSQLODBCRemoteTestBase { /// \brief Return a SQL query that selects all data types std::wstring getQueryAllDataTypes() override; + /// \brief Run a SQL query to create default table for table test cases + void CreateTestTables(); + /// \brief run a SQL query to create a table with all data types void CreateTableAllDataType(); /// \brief run a SQL query to create a table with unicode name @@ -220,4 +223,9 @@ void CheckIntColumn(SQLHSTMT stmt, int colId, const SQLINTEGER& expected); /// \param[in] colId Column ID to check. /// \param[in] expected Expected value. void CheckSmallIntColumn(SQLHSTMT stmt, int colId, const SQLSMALLINT& expected); + +/// \brief Check sql return against expected. +/// \param[in] stmt Statement. +/// \param[in] expected Expected return. +void ValidateFetch(SQLHSTMT stmt, SQLRETURN expected); } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc new file mode 100644 index 00000000000..e9b7e818777 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -0,0 +1,585 @@ +// 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/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/statement.h" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +// TODO: Add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and +// ODBC 3. + +// Helper Functions + +std::wstring GetStringColumnW(SQLHSTMT stmt, int colId) { + SQLWCHAR buf[1024]; + SQLLEN lenIndicator = 0; + + SQLRETURN ret = SQLGetData(stmt, colId, SQL_C_WCHAR, buf, sizeof(buf), &lenIndicator); + + EXPECT_EQ(ret, SQL_SUCCESS); + + if (lenIndicator == SQL_NULL_DATA) { + return L""; + } + + // indicator is in bytes, so convert to character count + size_t charCount = static_cast(lenIndicator) / ODBC::GetSqlWCharSize(); + return std::wstring(buf, buf + charCount); +} + +// Test Cases + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestInputData) { + this->connect(); + + SQLWCHAR catalogName[] = L""; + SQLWCHAR schemaName[] = L""; + SQLWCHAR tableName[] = L""; + SQLWCHAR tableType[] = L""; + std::wstring expectedCatalogName = std::wstring(L"main"); + + // All values populated + SQLRETURN ret = SQLTables(this->stmt, catalogName, sizeof(catalogName), schemaName, + sizeof(schemaName), tableName, sizeof(tableName), tableType, + sizeof(tableType)); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + // Sizes are nulls + ret = SQLTables(this->stmt, catalogName, 0, schemaName, 0, tableName, 0, tableType, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + // Values are nulls + ret = SQLTables(this->stmt, 0, sizeof(catalogName), 0, sizeof(schemaName), 0, + sizeof(tableName), 0, sizeof(tableType)); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + // Close statement cursor to avoid leaving in an invalid state + SQLFreeStmt(stmt, SQL_CLOSE); + + // All values and sizes are nulls + ret = SQLTables(this->stmt, 0, 0, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForAllCatalogs) { + this->connect(); + + SQLWCHAR empty[] = L""; + SQLWCHAR SQL_ALL_CATALOGS_W[] = L"%"; + std::wstring expectedCatalogName = std::wstring(L"main"); + + // Get Catalog metadata + SQLRETURN ret = SQLTables(this->stmt, SQL_ALL_CATALOGS_W, SQL_NTS, empty, SQL_NTS, + empty, SQL_NTS, empty, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + CheckNullColumnW(this->stmt, 2); + CheckNullColumnW(this->stmt, 3); + CheckNullColumnW(this->stmt, 4); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForNamedCatalog) { + this->connect(); + this->CreateTestTables(); + + SQLWCHAR catalogName[] = L"main"; + SQLWCHAR* tableNames[] = {(SQLWCHAR*)L"TestTable", (SQLWCHAR*)L"foreignTable", + (SQLWCHAR*)L"intTable", (SQLWCHAR*)L"sqlite_sequence"}; + std::wstring expectedCatalogName = std::wstring(catalogName); + std::wstring expectedTableType = std::wstring(L"table"); + + // Get named Catalog metadata - Mock server returns the system table sqlite_sequence as + // type "table" + SQLRETURN ret = SQLTables(this->stmt, catalogName, SQL_NTS, nullptr, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(tableNames) / sizeof(*tableNames); ++i) { + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + // Mock server does not support table schema + CheckNullColumnW(this->stmt, 2); + CheckStringColumnW(this->stmt, 3, tableNames[i]); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + } + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetSchemaHasNoData) { + this->connect(); + + SQLWCHAR SQL_ALL_SCHEMAS_W[] = L"%"; + + // Validate that no schema data is available for Mock server + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, SQL_ALL_SCHEMAS_W, SQL_NTS, + nullptr, SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesTestGetMetadataForAllSchemas) { + this->connect(); + + SQLWCHAR empty[] = L""; + SQLWCHAR SQL_ALL_SCHEMAS_W[] = L"%"; + std::set actualSchemas; + std::set expectedSchemas = {L"$scratch", L"INFORMATION_SCHEMA", L"sys", + L"sys.cache"}; + + // Return is unordered and contains user specific schemas, so collect schema names for + // comparison with a known list + SQLRETURN ret = SQLTables(this->stmt, empty, SQL_NTS, SQL_ALL_SCHEMAS_W, SQL_NTS, empty, + SQL_NTS, empty, SQL_NTS); + + ASSERT_EQ(ret, SQL_SUCCESS); + + while (true) { + ret = SQLFetch(this->stmt); + if (ret == SQL_NO_DATA) break; + ASSERT_EQ(ret, SQL_SUCCESS); + + CheckNullColumnW(this->stmt, 1); + std::wstring schema = GetStringColumnW(this->stmt, 2); + CheckNullColumnW(this->stmt, 3); + CheckNullColumnW(this->stmt, 4); + CheckNullColumnW(this->stmt, 5); + + // Skip user-specific schemas like "@UserName" + if (!schema.empty() && schema[0] != L'@') { + actualSchemas.insert(schema); + } + } + + EXPECT_EQ(actualSchemas, expectedSchemas); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesTestFilterByAllSchema) { + // Requires creation of user table named ODBCTest using schema $scratch in remote server + this->connect(); + + SQLWCHAR SQL_ALL_SCHEMAS_W[] = L"%"; + SQLWCHAR* schemaNames[] = {(SQLWCHAR*)L"INFORMATION_SCHEMA", + (SQLWCHAR*)L"INFORMATION_SCHEMA", + (SQLWCHAR*)L"INFORMATION_SCHEMA", + (SQLWCHAR*)L"INFORMATION_SCHEMA", + (SQLWCHAR*)L"INFORMATION_SCHEMA", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys", + (SQLWCHAR*)L"sys.cache", + (SQLWCHAR*)L"sys.cache", + (SQLWCHAR*)L"sys.cache", + (SQLWCHAR*)L"sys.cache", + (SQLWCHAR*)L"$scratch"}; + std::wstring expectedSystemTableType = std::wstring(L"SYSTEM_TABLE"); + std::wstring expectedUserTableType = std::wstring(L"TABLE"); + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, SQL_ALL_SCHEMAS_W, SQL_NTS, + nullptr, SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(schemaNames) / sizeof(*schemaNames); ++i) { + ValidateFetch(this->stmt, SQL_SUCCESS); + + const std::wstring& expectedTableType = + (std::wstring(schemaNames[i]).rfind(L"sys", 0) == 0 || + std::wstring(schemaNames[i]) == L"INFORMATION_SCHEMA") + ? expectedSystemTableType + : expectedUserTableType; + + CheckNullColumnW(this->stmt, 1); + CheckStringColumnW(this->stmt, 2, schemaNames[i]); + // Ignore table name + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + } + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetMetadataForNamedSchema) { + // Requires creation of user table named ODBCTest using schema $scratch in remote server + this->connect(); + + SQLWCHAR schemaName[] = L"$scratch"; + std::wstring expectedSchemaName = std::wstring(schemaName); + std::wstring expectedTableName = std::wstring(L"ODBCTest"); + std::wstring expectedTableType = std::wstring(L"TABLE"); + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, schemaName, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckNullColumnW(this->stmt, 1); + CheckStringColumnW(this->stmt, 2, expectedSchemaName); + // Ignore table name + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForAllTables) { + this->connect(); + this->CreateTestTables(); + + SQLWCHAR SQL_ALL_TABLES_W[] = L"%"; + SQLWCHAR* tableNames[] = {(SQLWCHAR*)L"TestTable", (SQLWCHAR*)L"foreignTable", + (SQLWCHAR*)L"intTable", (SQLWCHAR*)L"sqlite_sequence"}; + std::wstring expectedCatalogName = std::wstring(L"main"); + std::wstring expectedTableType = std::wstring(L"table"); + + // Get all Table metadata - Mock server returns the system table sqlite_sequence as type + // "table" + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, + SQL_ALL_TABLES_W, SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(tableNames) / sizeof(*tableNames); ++i) { + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + // Mock server does not support table schema + CheckNullColumnW(this->stmt, 2); + CheckStringColumnW(this->stmt, 3, tableNames[i]); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + } + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForTableName) { + this->connect(); + this->CreateTestTables(); + + SQLWCHAR* tableNames[] = {(SQLWCHAR*)L"TestTable", (SQLWCHAR*)L"foreignTable", + (SQLWCHAR*)L"intTable", (SQLWCHAR*)L"sqlite_sequence"}; + std::wstring expectedCatalogName = std::wstring(L"main"); + std::wstring expectedTableType = std::wstring(L"table"); + + for (size_t i = 0; i < sizeof(tableNames) / sizeof(*tableNames); ++i) { + // Get specific Table metadata + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, + tableNames[i], SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + // Mock server does not support table schema + CheckNullColumnW(this->stmt, 2); + CheckStringColumnW(this->stmt, 3, tableNames[i]); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForUnicodeTableByTableName) { + this->connect(); + this->CreateUnicodeTable(); + + SQLWCHAR unicodeTableName[] = L"数据"; + std::wstring expectedCatalogName = std::wstring(L"main"); + std::wstring expectedTableName = std::wstring(unicodeTableName); + std::wstring expectedTableType = std::wstring(L"table"); + + // Get specific Table metadata + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, + unicodeTableName, SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + // Mock server does not support table schema + CheckNullColumnW(this->stmt, 2); + CheckStringColumnW(this->stmt, 3, expectedTableName); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestGetMetadataForInvalidTableNameNoData) { + this->connect(); + this->CreateTestTables(); + + SQLWCHAR invalidTableName[] = L"NonExistantTableName"; + + // Try to get metadata for a non-existant table name + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, + invalidTableName, SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesGetMetadataForTableType) { + // Mock server only supports table type "table" in lowercase + this->connect(); + this->CreateTestTables(); + + SQLWCHAR tableTypeTableLowercase[] = L"table"; + SQLWCHAR tableTypeTableUppercase[] = L"TABLE"; + SQLWCHAR tableTypeView[] = L"VIEW"; + SQLWCHAR tableTypeTableView[] = L"TABLE,VIEW"; + SQLWCHAR* tableNames[] = {(SQLWCHAR*)L"TestTable", (SQLWCHAR*)L"foreignTable", + (SQLWCHAR*)L"intTable", (SQLWCHAR*)L"sqlite_sequence"}; + std::wstring expectedCatalogName = std::wstring(L"main"); + std::wstring expectedTableName = std::wstring(L"TestTable"); + std::wstring expectedTableType = std::wstring(tableTypeTableLowercase); + SQLRETURN ret = SQL_SUCCESS; + + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + tableTypeTableUppercase, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + tableTypeView, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + tableTypeTableView, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + // Returns user table as well as system tables, even though only type table requested + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + tableTypeTableLowercase, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(tableNames) / sizeof(*tableNames); ++i) { + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckStringColumnW(this->stmt, 1, expectedCatalogName); + // Mock server does not support table schema + CheckNullColumnW(this->stmt, 2); + CheckStringColumnW(this->stmt, 3, tableNames[i]); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + } + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetMetadataForTableTypeTable) { + // Requires creation of user table named ODBCTest using schema $scratch in remote server + this->connect(); + + SQLWCHAR* typeList[] = {(SQLWCHAR*)L"TABLE", (SQLWCHAR*)L"TABLE,VIEW"}; + std::wstring expectedSchemaName = std::wstring(L"$scratch"); + std::wstring expectedTableName = std::wstring(L"ODBCTest"); + std::wstring expectedTableType = std::wstring(L"TABLE"); + SQLRETURN ret = SQL_SUCCESS; + + for (size_t i = 0; i < sizeof(typeList) / sizeof(*typeList); ++i) { + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + typeList[i], SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckNullColumnW(this->stmt, 1); + CheckStringColumnW(this->stmt, 2, expectedSchemaName); + CheckStringColumnW(this->stmt, 3, expectedTableName); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetMetadataForTableTypeViewHasNoData) { + this->connect(); + + SQLWCHAR empty[] = L""; + SQLWCHAR typeView[] = L"VIEW"; + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, empty, + SQL_NTS, typeView, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, SQL_NTS, + typeView, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLTablesGetSupportedTableTypes) { + this->connect(); + + SQLWCHAR empty[] = L""; + SQLWCHAR SQL_ALL_TABLE_TYPES_W[] = L"%"; + std::wstring expectedTableType = std::wstring(L"table"); + + // Mock server returns lower case for supported type of "table" + SQLRETURN ret = SQLTables(this->stmt, empty, SQL_NTS, empty, SQL_NTS, empty, SQL_NTS, + SQL_ALL_TABLE_TYPES_W, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckNullColumnW(this->stmt, 1); + CheckNullColumnW(this->stmt, 2); + CheckNullColumnW(this->stmt, 3); + CheckStringColumnW(this->stmt, 4, expectedTableType); + CheckNullColumnW(this->stmt, 5); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetSupportedTableTypes) { + this->connect(); + + SQLWCHAR empty[] = L""; + SQLWCHAR SQL_ALL_TABLE_TYPES_W[] = L"%"; + SQLWCHAR* typeLists[] = {(SQLWCHAR*)L"TABLE", (SQLWCHAR*)L"SYSTEM_TABLE", + (SQLWCHAR*)L"VIEW"}; + + SQLRETURN ret = SQLTables(this->stmt, empty, SQL_NTS, empty, SQL_NTS, empty, SQL_NTS, + SQL_ALL_TABLE_TYPES_W, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(typeLists) / sizeof(*typeLists); ++i) { + ValidateFetch(this->stmt, SQL_SUCCESS); + + CheckNullColumnW(this->stmt, 1); + CheckNullColumnW(this->stmt, 2); + CheckNullColumnW(this->stmt, 3); + CheckStringColumnW(this->stmt, 4, typeLists[i]); + CheckNullColumnW(this->stmt, 5); + } + + ValidateFetch(this->stmt, SQL_NO_DATA); + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc From d25da4971719ad139105039a7c3028a0f6f60504 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:50:38 -0700 Subject: [PATCH 38/74] Use C++ 20 standard for ODBC * Add support for building with C++20 * Upgrade spdlogs version as an attempt to fix build issues * Fix issue of missing unique_ptr definition --- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 7 +- .../flight_sql/ui/dsn_configuration_window.cc | 2 +- .../include/odbcabstraction/logger.h | 1 + .../sql/odbc/tests/connection_info_test.cc | 70 +++++++++---------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 5cc4a8f3abe..29495eb0fe3 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -15,6 +15,11 @@ # specific language governing permissions and limitations # under the License. +# Use C++ 20 for ODBC and its subdirectory +# GH-44792: Arrow will switch to C++ 20 +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + add_custom_target(arrow_flight_sql_odbc) # Ensure fmt is loaded as header only @@ -36,7 +41,7 @@ add_definitions(-DUNICODE=1) include(FetchContent) fetchcontent_declare(spdlog - URL https://github.com/gabime/spdlog/archive/76fb40d95455f249bd70824ecfcae7a8f0930fa3.zip + URL https://github.com/gabime/spdlog/archive/refs/tags/v1.15.3.zip CONFIGURE_COMMAND "" BUILD_COMMAND diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index 78d56fb1f07..c6a13271435 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -291,7 +291,7 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in rightCheckPosX, rowPos - 2, 20, 2 * ROW_HEIGHT, L"", ChildId::DISABLE_CERT_VERIFICATION_CHECKBOX, disableCertVerification); - rowPos += INTERVAL + static_cast(1.5 * ROW_HEIGHT); + rowPos += INTERVAL + static_cast(1.5 * static_cast(ROW_HEIGHT)); encryptionSettingsGroupBox = CreateGroupBox(posX, posY, sizeX, rowPos - posY, L"Encryption settings", diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h index 4ea3261cbed..6249df98834 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc index 82774a55d8c..39bf7e1440b 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_info_test.cc @@ -182,7 +182,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoBatchSupport) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDataSourceName) { this->connect(); - validate(this->conn, SQL_DATA_SOURCE_NAME, L""); + validate(this->conn, SQL_DATA_SOURCE_NAME, (SQLWCHAR*)L""); this->disconnect(); } @@ -255,7 +255,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverHstmt) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverName) { this->connect(); - validate(this->conn, SQL_DRIVER_NAME, L"Arrow Flight ODBC Driver"); + validate(this->conn, SQL_DRIVER_NAME, (SQLWCHAR*)L"Arrow Flight ODBC Driver"); this->disconnect(); } @@ -263,7 +263,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverName) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverOdbcVer) { this->connect(); - validate(this->conn, SQL_DRIVER_ODBC_VER, L"03.80"); + validate(this->conn, SQL_DRIVER_ODBC_VER, (SQLWCHAR*)L"03.80"); this->disconnect(); } @@ -271,7 +271,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverOdbcVer) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDriverVer) { this->connect(); - validate(this->conn, SQL_DRIVER_VER, L"00.09.0000.0"); + validate(this->conn, SQL_DRIVER_VER, (SQLWCHAR*)L"00.09.0000.0"); this->disconnect(); } @@ -403,7 +403,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoOdbcVer) { // This is implemented only in the Driver Manager. this->connect(); - validate(this->conn, SQL_ODBC_VER, L"03.80.0000"); + validate(this->conn, SQL_ODBC_VER, (SQLWCHAR*)L"03.80.0000"); this->disconnect(); } @@ -429,7 +429,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoParamArraySelects) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoRowUpdates) { this->connect(); - validate(this->conn, SQL_ROW_UPDATES, L"N"); + validate(this->conn, SQL_ROW_UPDATES, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -437,7 +437,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoRowUpdates) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSearchPatternEscape) { this->connect(); - validate(this->conn, SQL_SEARCH_PATTERN_ESCAPE, L"\\"); + validate(this->conn, SQL_SEARCH_PATTERN_ESCAPE, (SQLWCHAR*)L"\\"); this->disconnect(); } @@ -471,7 +471,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoStaticCursorAttributes2) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDatabaseName) { this->connect(); - validate(this->conn, SQL_DATABASE_NAME, L""); + validate(this->conn, SQL_DATABASE_NAME, (SQLWCHAR*)L""); this->disconnect(); } @@ -497,7 +497,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDbmsVer) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAccessibleProcedures) { this->connect(); - validate(this->conn, SQL_ACCESSIBLE_PROCEDURES, L"N"); + validate(this->conn, SQL_ACCESSIBLE_PROCEDURES, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -505,7 +505,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAccessibleProcedures) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAccessibleTables) { this->connect(); - validate(this->conn, SQL_ACCESSIBLE_TABLES, L"Y"); + validate(this->conn, SQL_ACCESSIBLE_TABLES, (SQLWCHAR*)L"Y"); this->disconnect(); } @@ -521,7 +521,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoBookmarkPersistence) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogTerm) { this->connect(); - validate(this->conn, SQL_CATALOG_TERM, L""); + validate(this->conn, SQL_CATALOG_TERM, (SQLWCHAR*)L""); this->disconnect(); } @@ -529,7 +529,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogTerm) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCollationSeq) { this->connect(); - validate(this->conn, SQL_COLLATION_SEQ, L""); + validate(this->conn, SQL_COLLATION_SEQ, (SQLWCHAR*)L""); this->disconnect(); } @@ -571,7 +571,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCursorSensitivity) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDataSourceReadOnly) { this->connect(); - validate(this->conn, SQL_DATA_SOURCE_READ_ONLY, L"N"); + validate(this->conn, SQL_DATA_SOURCE_READ_ONLY, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -587,7 +587,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDefaultTxnIsolation) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDescribeParameter) { this->connect(); - validate(this->conn, SQL_DESCRIBE_PARAMETER, L"N"); + validate(this->conn, SQL_DESCRIBE_PARAMETER, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -595,7 +595,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDescribeParameter) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultResultSets) { this->connect(); - validate(this->conn, SQL_MULT_RESULT_SETS, L"N"); + validate(this->conn, SQL_MULT_RESULT_SETS, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -603,7 +603,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultResultSets) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultipleActiveTxn) { this->connect(); - validate(this->conn, SQL_MULTIPLE_ACTIVE_TXN, L"N"); + validate(this->conn, SQL_MULTIPLE_ACTIVE_TXN, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -611,7 +611,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMultipleActiveTxn) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoNeedLongDataLen) { this->connect(); - validate(this->conn, SQL_NEED_LONG_DATA_LEN, L"N"); + validate(this->conn, SQL_NEED_LONG_DATA_LEN, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -627,7 +627,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoNullCollation) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoProcedureTerm) { this->connect(); - validate(this->conn, SQL_PROCEDURE_TERM, L""); + validate(this->conn, SQL_PROCEDURE_TERM, (SQLWCHAR*)L""); this->disconnect(); } @@ -635,7 +635,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoProcedureTerm) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSchemaTerm) { this->connect(); - validate(this->conn, SQL_SCHEMA_TERM, L"schema"); + validate(this->conn, SQL_SCHEMA_TERM, (SQLWCHAR*)L"schema"); this->disconnect(); } @@ -651,7 +651,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoScrollOptions) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTableTerm) { this->connect(); - validate(this->conn, SQL_TABLE_TERM, L"table"); + validate(this->conn, SQL_TABLE_TERM, (SQLWCHAR*)L"table"); this->disconnect(); } @@ -675,7 +675,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoTxnIsolationOption) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoUserName) { this->connect(); - validate(this->conn, SQL_USER_NAME, L""); + validate(this->conn, SQL_USER_NAME, (SQLWCHAR*)L""); this->disconnect(); } @@ -726,7 +726,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoAnsiSqlDatetimeLiterals) { this->connect(); // Type does not exist in sql.h - // validate(this->conn, SQL_ANSI_SQL_DATETIME_LITERALS, L""); + // validate(this->conn, SQL_ANSI_SQL_DATETIME_LITERALS, (SQLWCHAR*)L""); this->disconnect(); } @@ -742,7 +742,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogLocation) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogName) { this->connect(); - validate(this->conn, SQL_CATALOG_NAME, L"N"); + validate(this->conn, SQL_CATALOG_NAME, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -750,7 +750,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogName) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoCatalogNameSeparator) { this->connect(); - validate(this->conn, SQL_CATALOG_NAME_SEPARATOR, L""); + validate(this->conn, SQL_CATALOG_NAME_SEPARATOR, (SQLWCHAR*)L""); this->disconnect(); } @@ -766,7 +766,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoCatalogUsage) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoColumnAlias) { this->connect(); - validate(this->conn, SQL_COLUMN_ALIAS, L"Y"); + validate(this->conn, SQL_COLUMN_ALIAS, (SQLWCHAR*)L"Y"); this->disconnect(); } @@ -910,7 +910,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoDropView) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoExpressionsInOrderby) { this->connect(); - validate(this->conn, SQL_EXPRESSIONS_IN_ORDERBY, L"N"); + validate(this->conn, SQL_EXPRESSIONS_IN_ORDERBY, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -935,7 +935,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIdentifierCase) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIdentifierQuoteChar) { this->connect(); - validate(this->conn, SQL_IDENTIFIER_QUOTE_CHAR, L"\""); + validate(this->conn, SQL_IDENTIFIER_QUOTE_CHAR, (SQLWCHAR*)L"\""); this->disconnect(); } @@ -961,7 +961,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoInsertStatement) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoIntegrity) { this->connect(); - validate(this->conn, SQL_INTEGRITY, L"N"); + validate(this->conn, SQL_INTEGRITY, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -977,7 +977,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoKeywords) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoLikeEscapeClause) { this->connect(); - validate(this->conn, SQL_LIKE_ESCAPE_CLAUSE, L"Y"); + validate(this->conn, SQL_LIKE_ESCAPE_CLAUSE, (SQLWCHAR*)L"Y"); this->disconnect(); } @@ -1002,7 +1002,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOjCapabilities) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOrderByColumnsInSelect) { this->connect(); - validate(this->conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, L"Y"); + validate(this->conn, SQL_ORDER_BY_COLUMNS_IN_SELECT, (SQLWCHAR*)L"Y"); this->disconnect(); } @@ -1010,7 +1010,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOrderByColumnsInSelect) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOuterJoins) { this->connect(); - validate(this->conn, SQL_OUTER_JOINS, L"N"); + validate(this->conn, SQL_OUTER_JOINS, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -1018,7 +1018,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoOuterJoins) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoProcedures) { this->connect(); - validate(this->conn, SQL_PROCEDURES, L"N"); + validate(this->conn, SQL_PROCEDURES, (SQLWCHAR*)L"N"); this->disconnect(); } @@ -1043,7 +1043,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoSchemaUsage) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoSpecialCharacters) { this->connect(); - validate(this->conn, SQL_SPECIAL_CHARACTERS, L""); + validate(this->conn, SQL_SPECIAL_CHARACTERS, (SQLWCHAR*)L""); this->disconnect(); } @@ -1184,7 +1184,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxProcedureNameLen) { TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxRowSize) { this->connect(); - validate(this->conn, SQL_MAX_ROW_SIZE, L""); + validate(this->conn, SQL_MAX_ROW_SIZE, (SQLWCHAR*)L""); this->disconnect(); } @@ -1192,7 +1192,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetInfoMaxRowSize) { TEST_F(FlightSQLODBCMockTestBase, TestSQLGetInfoMaxRowSizeIncludesLong) { this->connect(); - validate(this->conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, L"N"); + validate(this->conn, SQL_MAX_ROW_SIZE_INCLUDES_LONG, (SQLWCHAR*)L"N"); this->disconnect(); } From 5c3d754b4df4d0116655552888f1f5f13a1086cf Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:53:46 -0700 Subject: [PATCH 39/74] Add tests for SQLMoreResults --- .../flight/sql/odbc/tests/statement_test.cc | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 000e0b05a71..79d58b64bc8 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -2234,4 +2234,35 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLExtendedFetchQueryNullIndicator) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLMoreResultsNoData) { + // Verify SQLMoreResults is stubbed to return SQL_NO_DATA + 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 = SQLMoreResults(this->stmt); + + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLMoreResultsInvalidFunctionSequence) { + this->connect(); + + SQLRETURN ret = SQLMoreResults(this->stmt); + + // Verify function sequence error state is reported when SQLMoreResults is called + // without executing any queries + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY010); + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc From 9d816af338f137adbd25d3ba3b37e0595b33235c Mon Sep 17 00:00:00 2001 From: rscales Date: Mon, 28 Jul 2025 13:25:52 -0700 Subject: [PATCH 40/74] Implement SQLNativeSql --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 9 +- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 29 ++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 3 + .../flight/sql/odbc/tests/odbc_test_suite.h | 2 + .../flight/sql/odbc/tests/statement_test.cc | 135 ++++++++++++++++++ 5 files changed, 171 insertions(+), 7 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 6ddd2481e9b..38edc86ddff 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -254,13 +254,8 @@ SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementTe SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength) { - LOG_DEBUG( - "SQLNativeSqlW called with connectionHandle: {}, inStatementText: {}, " - "inStatementTextLength: " - "{}, outStatementText: {}, bufferLength: {}, outStatementTextLength: {}", - connectionHandle, fmt::ptr(inStatementText), inStatementTextLength, - fmt::ptr(outStatementText), bufferLength, fmt::ptr(outStatementTextLength)); - return SQL_ERROR; + return arrow::SQLNativeSql(connectionHandle, inStatementText, inStatementTextLength, + outStatementText, bufferLength, outStatementTextLength); } SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index f5cac728c09..e87e713caa8 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1273,4 +1273,33 @@ SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, return SQL_SUCCESS; }); } + +SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, + SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, + SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength) { + LOG_DEBUG( + "SQLNativeSqlW called with connectionHandle: {}, inStatementText: {}, " + "inStatementTextLength: {}, outStatementText: {}, bufferLength: {}, " + "outStatementTextLength: {}", + connectionHandle, fmt::ptr(inStatementText), inStatementTextLength, + fmt::ptr(outStatementText), bufferLength, fmt::ptr(outStatementTextLength)); + + using driver::odbcabstraction::Diagnostics; + using ODBC::GetAttributeSQLWCHAR; + using ODBC::ODBCConnection; + using ODBC::SqlWcharToString; + + return ODBCConnection::ExecuteWithDiagnostics(connectionHandle, SQL_ERROR, [=]() { + const bool isLengthInBytes = false; + + ODBCConnection* connection = reinterpret_cast(connectionHandle); + Diagnostics& diagnostics = connection->GetDiagnostics(); + + std::string inStatementStr = SqlWcharToString(inStatementText, inStatementTextLength); + + return GetAttributeSQLWCHAR(inStatementStr, isLengthInBytes, outStatementText, + bufferLength, outStatementTextLength, diagnostics); + }); +} + } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 05f8d1a74f8..d9181ff329b 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -91,4 +91,7 @@ SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, SQLLEN* numericAttributePtr); +SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, + SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, + SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength); } // namespace arrow 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 ffaa33c3a10..0ce4bc4144d 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 @@ -172,8 +172,10 @@ static constexpr std::string_view error_state_22002 = "22002"; static constexpr std::string_view error_state_24000 = "24000"; static constexpr std::string_view error_state_28000 = "28000"; static constexpr std::string_view error_state_HY000 = "HY000"; +static constexpr std::string_view error_state_HY009 = "HY009"; 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_HY090 = "HY090"; 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"; 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 79d58b64bc8..45e4a29f9fa 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -2265,4 +2265,139 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLMoreResultsInvalidFunctionSequence) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsInputString) { + this->connect(); + + SQLWCHAR buf[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(buf) / ODBC::GetSqlWCharSize(); + SQLWCHAR inputStr[] = L"SELECT * FROM mytable WHERE id == 1"; + SQLINTEGER inputCharLen = static_cast(wcslen(inputStr)); + SQLINTEGER outputCharLen = 0; + std::wstring expectedString = std::wstring(inputStr); + + SQLRETURN ret = + SQLNativeSql(this->conn, inputStr, inputCharLen, buf, bufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(outputCharLen, inputCharLen); + + // returned length is in characters + std::wstring returnedString(buf, buf + outputCharLen); + + EXPECT_EQ(returnedString, expectedString); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsNTSInputString) { + this->connect(); + + SQLWCHAR buf[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(buf) / ODBC::GetSqlWCharSize(); + SQLWCHAR inputStr[] = L"SELECT * FROM mytable WHERE id == 1"; + SQLINTEGER inputCharLen = static_cast(wcslen(inputStr)); + SQLINTEGER outputCharLen = 0; + std::wstring expectedString = std::wstring(inputStr); + + SQLRETURN ret = + SQLNativeSql(this->conn, inputStr, SQL_NTS, buf, bufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(outputCharLen, inputCharLen); + + // returned length is in characters + std::wstring returnedString(buf, buf + outputCharLen); + + EXPECT_EQ(returnedString, expectedString); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsInputStringLength) { + this->connect(); + + SQLWCHAR inputStr[] = L"SELECT * FROM mytable WHERE id == 1"; + SQLINTEGER inputCharLen = static_cast(wcslen(inputStr)); + SQLINTEGER outputCharLen = 0; + std::wstring expectedString = std::wstring(inputStr); + + SQLRETURN ret = + SQLNativeSql(this->conn, inputStr, inputCharLen, nullptr, 0, &outputCharLen); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(outputCharLen, inputCharLen); + + ret = SQLNativeSql(this->conn, inputStr, SQL_NTS, nullptr, 0, &outputCharLen); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(outputCharLen, inputCharLen); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsTruncatedString) { + this->connect(); + + const SQLINTEGER smallBufSizeInChar = 11; + SQLWCHAR smallBuf[smallBufSizeInChar]; + constexpr SQLINTEGER smallBufCharLen = sizeof(smallBuf) / ODBC::GetSqlWCharSize(); + SQLWCHAR inputStr[] = L"SELECT * FROM mytable WHERE id == 1"; + SQLINTEGER inputCharLen = static_cast(wcslen(inputStr)); + SQLINTEGER outputCharLen = 0; + + // Create expected return string based on buf size + SQLWCHAR expectedStringBuf[smallBufSizeInChar]; + wcsncpy(expectedStringBuf, inputStr, 10); + expectedStringBuf[10] = L'\0'; + std::wstring expectedString(expectedStringBuf, expectedStringBuf + smallBufSizeInChar); + + SQLRETURN ret = SQLNativeSql(this->conn, inputStr, inputCharLen, smallBuf, + smallBufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_01004); + + // Returned text length represents full string char length regardless of truncation + EXPECT_EQ(outputCharLen, inputCharLen); + + std::wstring returnedString(smallBuf, smallBuf + smallBufCharLen); + + EXPECT_EQ(returnedString, expectedString); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsErrorOnBadInputs) { + this->connect(); + + SQLWCHAR buf[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(buf) / ODBC::GetSqlWCharSize(); + SQLWCHAR inputStr[] = L"SELECT * FROM mytable WHERE id == 1"; + SQLINTEGER inputCharLen = static_cast(wcslen(inputStr)); + SQLINTEGER outputCharLen = 0; + + SQLRETURN ret = + SQLNativeSql(this->conn, nullptr, inputCharLen, buf, bufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY009); + + ret = SQLNativeSql(this->conn, nullptr, SQL_NTS, buf, bufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY009); + + ret = SQLNativeSql(this->conn, inputStr, -100, buf, bufCharLen, &outputCharLen); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_DBC, this->conn, error_state_HY090); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc From 19f348d8e86513dd925dddb031416dd1362504a2 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:56:28 -0700 Subject: [PATCH 41/74] Add diagnostic records * remove `SQLErrors`, as the driver manager is supposed to map `SQLErrors` to `SQLGetDiagRec` --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 50 ++++++++++++------- cpp/src/arrow/flight/sql/odbc/odbc.def | 1 - 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 38edc86ddff..6087eb1635f 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -15,6 +15,9 @@ // specific language governing permissions and limitations // under the License. +// platform.h includes windows.h, so it needs to be included first +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" + #ifdef _WIN32 # include #endif @@ -27,6 +30,11 @@ #include "arrow/flight/sql/odbc/odbc_api.h" #include "arrow/flight/sql/odbc/visibility.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_connection.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_descriptor.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_environment.h" +#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" + #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/logger.h" SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) { @@ -171,12 +179,20 @@ SQLRETURN SQL_API SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLI SQLRETURN SQL_API SQLCancel(SQLHSTMT stmt) { LOG_DEBUG("SQLCancel called with stmt: {}", stmt); - return SQL_ERROR; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + throw driver::odbcabstraction::DriverException("SQLCancel is not implemented", + "IM001"); + return SQL_ERROR; + }); } SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT stmt) { LOG_DEBUG("SQLCloseCursor called with stmt: {}", stmt); - return SQL_ERROR; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + throw driver::odbcabstraction::DriverException("SQLCloseCursor is not implemented", + "IM001"); + return SQL_ERROR; + }); } SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, @@ -209,18 +225,6 @@ SQLRETURN SQL_API SQLColumns(SQLHSTMT stmt, SQLWCHAR* catalogName, columnNameLength); } -SQLRETURN SQL_API SQLError(SQLHENV handleType, SQLHDBC handle, SQLHSTMT hstmt, - SQLWCHAR FAR* szSqlState, SQLINTEGER FAR* pfNativeError, - SQLWCHAR FAR* szErrorMsg, SQLSMALLINT cbErrorMsgMax, - SQLSMALLINT FAR* pcbErrorMsg) { - LOG_DEBUG( - "SQLErrorW called with handleType: {}, handle: {}, hstmt: {}, szSqlState: {}, " - "pfNativeError: {}, szErrorMsg: {}, cbErrorMsgMax: {}, pcbErrorMsg: {}", - handleType, handle, hstmt, fmt::ptr(szSqlState), fmt::ptr(pfNativeError), - fmt::ptr(szErrorMsg), cbErrorMsgMax, fmt::ptr(pcbErrorMsg)); - return SQL_ERROR; -} - SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT stmt, SQLWCHAR* pKCatalogName, SQLSMALLINT pKCatalogNameLength, SQLWCHAR* pKSchemaName, SQLSMALLINT pKSchemaNameLength, SQLWCHAR* pKTableName, @@ -240,12 +244,20 @@ SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT stmt, SQLWCHAR* pKCatalogName, pKSchemaNameLength, fmt::ptr(pKTableName), pKTableNameLength, fmt::ptr(fKCatalogName), fKCatalogNameLength, fmt::ptr(fKSchemaName), fKSchemaNameLength, fmt::ptr(fKTableName), fKTableNameLength); - return SQL_ERROR; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + throw driver::odbcabstraction::DriverException("SQLForeignKeysW is not implemented", + "IM001"); + return SQL_ERROR; + }); } SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType) { LOG_DEBUG("SQLGetTypeInfoW called with stmt: {} dataType: {}", stmt, dataType); - return SQL_ERROR; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + throw driver::odbcabstraction::DriverException("SQLGetTypeInfoW is not implemented", + "IM001"); + return SQL_ERROR; + }); } SQLRETURN SQL_API SQLMoreResults(SQLHSTMT stmt) { return arrow::SQLMoreResults(stmt); } @@ -276,7 +288,11 @@ SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT stmt, SQLWCHAR* catalogName, "{}, schemaName: {}, schemaNameLength: {}, tableName: {}, tableNameLength: {}", stmt, fmt::ptr(catalogName), catalogNameLength, fmt::ptr(schemaName), schemaNameLength, fmt::ptr(tableName), tableNameLength); - return SQL_ERROR; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + throw driver::odbcabstraction::DriverException("SQLPrimaryKeysW is not implemented", + "IM001"); + return SQL_ERROR; + }); } SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER valuePtr, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index 1581e51b558..6a7402ffa90 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -30,7 +30,6 @@ EXPORTS SQLConnectW SQLDisconnect SQLDriverConnectW - SQLErrorW SQLExecDirectW SQLExecute SQLExtendedFetch From e9d62f6f3a43f9d38ff011353ce482df3860d3c1 Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 29 Jul 2025 13:29:55 -0700 Subject: [PATCH 42/74] Implement tests for SQLNumResultCols --- .../odbc_impl/odbc_statement.cc | 2 +- .../flight/sql/odbc/tests/statement_test.cc | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index 3940bdccc77..70ee80ea08f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -776,7 +776,7 @@ void ODBCStatement::getColumnCount(SQLSMALLINT* columnCountPtr) { // error return; } - size_t columnCount = m_currentArd->GetRecords().size(); + size_t columnCount = m_ird->GetRecords().size(); *columnCountPtr = static_cast(columnCount); } 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 45e4a29f9fa..fee8ab5f700 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -2400,4 +2400,74 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLNativeSqlReturnsErrorOnBadInputs) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLNumResultColsReturnsColumnsOnSelect) { + this->connect(); + + SQLSMALLINT columnCount = 0; + SQLSMALLINT expectedValue = 3; + SQLWCHAR sqlQuery[] = L"SELECT 1 AS col1, 'One' AS col2, 3 AS col3"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + CheckIntColumn(this->stmt, 1, 1); + CheckStringColumnW(this->stmt, 2, L"One"); + CheckIntColumn(this->stmt, 3, 3); + + ret = SQLNumResultCols(this->stmt, &columnCount); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(columnCount, expectedValue); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLNumResultColsReturnsSuccessOnNullptr) { + this->connect(); + + SQLWCHAR sqlQuery[] = L"SELECT 1 AS col1, 'One' AS col2, 3 AS col3"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + CheckIntColumn(this->stmt, 1, 1); + CheckStringColumnW(this->stmt, 2, L"One"); + CheckIntColumn(this->stmt, 3, 3); + + ret = SQLNumResultCols(this->stmt, nullptr); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLNumResultColsFunctionSequenceErrorOnNoQuery) { + this->connect(); + + SQLSMALLINT columnCount = 0; + SQLSMALLINT expectedValue = 0; + + SQLRETURN ret = SQLNumResultCols(this->stmt, &columnCount); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY010); + + EXPECT_EQ(columnCount, expectedValue); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc From 9484125b3bb25cd1548037c553c8e755354be930 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:00:24 -0700 Subject: [PATCH 43/74] SQLCloseCursor Implementation * fix error state to return the correct state 24000. * add tests for SQLCloseCursor and SQLFreeStmt(SQL_CLOSE) --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 9 +-- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 24 +++++--- cpp/src/arrow/flight/sql/odbc/odbc_api.h | 1 + .../odbc_impl/odbc_statement.cc | 2 +- .../flight/sql/odbc/tests/statement_test.cc | 60 +++++++++++++++++++ 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 6087eb1635f..52ab94dd74f 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -186,14 +186,7 @@ SQLRETURN SQL_API SQLCancel(SQLHSTMT stmt) { }); } -SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT stmt) { - LOG_DEBUG("SQLCloseCursor called with stmt: {}", stmt); - return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { - throw driver::odbcabstraction::DriverException("SQLCloseCursor is not implemented", - "IM001"); - return SQL_ERROR; - }); -} +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT stmt) { return arrow::SQLCloseCursor(stmt); } SQLRETURN SQL_API SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLUSMALLINT fieldIdentifier, diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index e87e713caa8..23f7a2b011c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -179,12 +179,8 @@ SQLRETURN SQLFreeStmt(SQLHSTMT handle, SQLUSMALLINT option) { case SQL_CLOSE: { using ODBC::ODBCStatement; - ODBCStatement* statement = reinterpret_cast(handle); - - return ODBCStatement::ExecuteWithDiagnostics(statement, SQL_ERROR, [=]() { - if (!statement) { - return SQL_INVALID_HANDLE; - } + return ODBCStatement::ExecuteWithDiagnostics(handle, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(handle); // Close cursor with suppressErrors set to true statement->closeCursor(true); @@ -1060,6 +1056,19 @@ SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType }); } +SQLRETURN SQLCloseCursor(SQLHSTMT stmt) { + LOG_DEBUG("SQLCloseCursor called with stmt: {}", stmt); + using ODBC::ODBCStatement; + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + // Close cursor with suppressErrors set to false + statement->closeCursor(false); + + return SQL_SUCCESS; + }); +} + SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr) { // GH-46979: support SQL_C_GUID data type @@ -1080,7 +1089,6 @@ SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType SQLRETURN SQLMoreResults(SQLHSTMT stmt) { LOG_DEBUG("SQLMoreResults called with stmt: {}", stmt); - // TODO: write tests for SQLMoreResults using ODBC::ODBCStatement; // Multiple result sets not supported. Return SQL_NO_DATA by default. return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { @@ -1092,7 +1100,6 @@ SQLRETURN SQLMoreResults(SQLHSTMT stmt) { SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { LOG_DEBUG("SQLNumResultCols called with stmt: {}, columnCountPtr: {}", stmt, fmt::ptr(columnCountPtr)); - // TODO: write tests for SQLNumResultCols using ODBC::ODBCStatement; return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); @@ -1104,7 +1111,6 @@ SQLRETURN SQLNumResultCols(SQLHSTMT stmt, SQLSMALLINT* columnCountPtr) { SQLRETURN SQLRowCount(SQLHSTMT stmt, SQLLEN* rowCountPtr) { LOG_DEBUG("SQLRowCount called with stmt: {}, columnCountPtr: {}", stmt, fmt::ptr(rowCountPtr)); - // TODO: write tests for SQLRowCount using ODBC::ODBCStatement; return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index d9181ff329b..e4139f96333 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -74,6 +74,7 @@ SQLRETURN SQLExtendedFetch(SQLHSTMT stmt, SQLUSMALLINT fetchOrientation, SQLRETURN SQLFetchScroll(SQLHSTMT stmt, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset); SQLRETURN SQLBindCol(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); +SQLRETURN SQLCloseCursor(SQLHSTMT stmt); SQLRETURN SQLGetData(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLSMALLINT cType, SQLPOINTER dataPtr, SQLLEN bufferLength, SQLLEN* indicatorPtr); SQLRETURN SQLMoreResults(SQLHSTMT stmt); diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index 70ee80ea08f..c990f766df5 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -703,7 +703,7 @@ void ODBCStatement::RevertAppDescriptor(bool isApd) { void ODBCStatement::closeCursor(bool suppressErrors) { if (!suppressErrors && !m_currenResult) { - throw DriverException("Invalid cursor state", "28000"); + throw DriverException("Invalid cursor state", "24000"); } if (m_currenResult) { 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 fee8ab5f700..89fce1a23d2 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -2470,4 +2470,64 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLNumResultColsFunctionSequenceErrorOnNoQuery this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLFreeStmtSQLClose) { + 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 = SQLFreeStmt(this->stmt, SQL_CLOSE); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLCloseCursor) { + 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 = SQLCloseCursor(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLFreeStmtSQLCloseWithoutCursor) { + // SQLFreeStmt(SQL_CLOSE) does not throw error with invalid cursor + this->connect(); + + SQLRETURN ret = SQLFreeStmt(this->stmt, SQL_CLOSE); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLCloseCursorWithoutCursor) { + this->connect(); + + SQLRETURN ret = SQLCloseCursor(this->stmt); + + EXPECT_EQ(ret, SQL_ERROR); + + // Verify invalid cursor error state is returned + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_24000); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc From b4d15bf9203ab43d68f232e5b11ceabbed0ba027 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:00:41 -0700 Subject: [PATCH 44/74] Add SQLColumns and SQLColAttribute tests with nullptr * Modify SQLTables test to run on both mock and remote * reorder error msg alphabetically --- .../flight/sql/odbc/tests/columns_test.cc | 164 ++++++++++++++++++ .../flight/sql/odbc/tests/odbc_test_suite.h | 6 +- .../flight/sql/odbc/tests/tables_test.cc | 5 +- 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 8874572ec63..325e59554ce 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -366,6 +366,51 @@ void checkSQLColAttributesNumeric(SQLHSTMT stmt, const std::wstring& wsql, EXPECT_EQ(numVal, expectedAttrNumeric); } +TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsTestInputData) { + this->connect(); + + SQLWCHAR catalogName[] = L""; + SQLWCHAR schemaName[] = L""; + SQLWCHAR tableName[] = L""; + SQLWCHAR columnName[] = L""; + + // All values populated + SQLRETURN ret = SQLColumns(this->stmt, catalogName, sizeof(catalogName), schemaName, + sizeof(schemaName), tableName, sizeof(tableName), columnName, + sizeof(columnName)); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + // Sizes are nulls + ret = + SQLColumns(this->stmt, catalogName, 0, schemaName, 0, tableName, 0, columnName, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_NO_DATA); + + // Values are nulls + ret = SQLColumns(this->stmt, 0, sizeof(catalogName), 0, sizeof(schemaName), 0, + sizeof(tableName), 0, sizeof(columnName)); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + // Close statement cursor to avoid leaving in an invalid state + SQLFreeStmt(this->stmt, SQL_CLOSE); + + // All values and sizes are nulls + ret = SQLColumns(this->stmt, 0, 0, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ValidateFetch(this->stmt, SQL_SUCCESS); + + this->disconnect(); +} + TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsAllColumns) { // Check table pattern and column pattern returns all columns this->connect(); @@ -1245,6 +1290,125 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLColumnsInvalidTablePattern) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLColAttributeTestInputData) { + this->connect(); + + std::wstring wsql = L"SELECT 1 as col1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLUSMALLINT idx = 1; + std::vector characterAttr(ODBC_BUFFER_SIZE); + SQLSMALLINT characterAttrLen = 0; + SQLLEN numericAttr = 0; + + // All character values populated + ret = SQLColAttribute(this->stmt, idx, SQL_DESC_NAME, &characterAttr[0], + (SQLSMALLINT)characterAttr.size(), &characterAttrLen, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + // All numeric values populated + ret = SQLColAttribute(this->stmt, idx, SQL_DESC_COUNT, 0, 0, 0, &numericAttr); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Pass null values, driver should not throw error + ret = SQLColAttribute(this->stmt, idx, SQL_COLUMN_TABLE_NAME, 0, 0, 0, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLColAttribute(this->stmt, idx, SQL_DESC_COUNT, 0, 0, 0, 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLColAttributeGetCharacterLen) { + this->connect(); + + std::wstring wsql = L"SELECT 1 as col1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLSMALLINT characterAttrLen = 0; + + // Check length of character attribute + ret = SQLColAttribute(this->stmt, 1, SQL_DESC_BASE_COLUMN_NAME, 0, 0, &characterAttrLen, + 0); + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(characterAttrLen, 4 * ODBC::GetSqlWCharSize()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLColAttributeInvalidFieldId) { + this->connect(); + + std::wstring wsql = L"SELECT 1 as col1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLUSMALLINT invalidFieldId = -100; + SQLUSMALLINT idx = 1; + std::vector characterAttr(ODBC_BUFFER_SIZE); + SQLSMALLINT characterAttrLen = 0; + SQLLEN numericAttr = 0; + + ret = SQLColAttribute(this->stmt, idx, invalidFieldId, &characterAttr[0], + (SQLSMALLINT)characterAttr.size(), &characterAttrLen, 0); + EXPECT_EQ(ret, SQL_ERROR); + // Verify invalid descriptor field identifier error state is returned + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY091); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLColAttributeInvalidColId) { + this->connect(); + + std::wstring wsql = L"SELECT 1 as col1;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + SQLUSMALLINT invalidColId = 2; + std::vector characterAttr(ODBC_BUFFER_SIZE); + SQLSMALLINT characterAttrLen = 0; + SQLLEN numericAttr = 0; + + ret = SQLColAttribute(this->stmt, invalidColId, SQL_DESC_BASE_COLUMN_NAME, + &characterAttr[0], (SQLSMALLINT)characterAttr.size(), + &characterAttrLen, 0); + EXPECT_EQ(ret, SQL_ERROR); + // Verify invalid descriptor index error state is returned + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + this->disconnect(); +} + TEST_F(FlightSQLODBCMockTestBase, TestSQLColAttributeAllTypes) { this->connect(); this->CreateTableAllDataType(); 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 0ce4bc4144d..ac8568900d5 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 @@ std::string GetOdbcErrorMessage(SQLSMALLINT handle_type, SQLHANDLE handle); static constexpr std::string_view error_state_01004 = "01004"; static constexpr std::string_view error_state_01S07 = "01S07"; static constexpr std::string_view error_state_01S02 = "01S02"; +static constexpr std::string_view error_state_07009 = "07009"; static constexpr std::string_view error_state_08003 = "08003"; static constexpr std::string_view error_state_22002 = "22002"; static constexpr std::string_view error_state_24000 = "24000"; @@ -174,14 +175,15 @@ static constexpr std::string_view error_state_28000 = "28000"; static constexpr std::string_view error_state_HY000 = "HY000"; static constexpr std::string_view error_state_HY009 = "HY009"; static constexpr std::string_view error_state_HY010 = "HY010"; +static constexpr std::string_view error_state_HY017 = "HY017"; static constexpr std::string_view error_state_HY024 = "HY024"; static constexpr std::string_view error_state_HY090 = "HY090"; +static constexpr std::string_view error_state_HY091 = "HY091"; 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"; +static constexpr std::string_view error_state_HYC00 = "HYC00"; /// Verify ODBC Error State void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index e9b7e818777..d2e7aab7d45 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -55,14 +55,13 @@ std::wstring GetStringColumnW(SQLHSTMT stmt, int colId) { // Test Cases -TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestInputData) { +TYPED_TEST(FlightSQLODBCTestBase, SQLTablesTestInputData) { this->connect(); SQLWCHAR catalogName[] = L""; SQLWCHAR schemaName[] = L""; SQLWCHAR tableName[] = L""; SQLWCHAR tableType[] = L""; - std::wstring expectedCatalogName = std::wstring(L"main"); // All values populated SQLRETURN ret = SQLTables(this->stmt, catalogName, sizeof(catalogName), schemaName, @@ -88,7 +87,7 @@ TEST_F(FlightSQLODBCMockTestBase, SQLTablesTestInputData) { ValidateFetch(this->stmt, SQL_SUCCESS); // Close statement cursor to avoid leaving in an invalid state - SQLFreeStmt(stmt, SQL_CLOSE); + SQLFreeStmt(this->stmt, SQL_CLOSE); // All values and sizes are nulls ret = SQLTables(this->stmt, 0, 0, 0, 0, 0, 0, 0, 0); From 184dfea745a82a57f67716e874fe153b25f76f3c Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 31 Jul 2025 10:17:03 -0700 Subject: [PATCH 45/74] Implement tests for SQLRowCount --- .../flight/sql/odbc/tests/statement_test.cc | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 89fce1a23d2..0d255101db3 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/statement_test.cc @@ -2470,6 +2470,76 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLNumResultColsFunctionSequenceErrorOnNoQuery this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLRowCountReturnsNegativeOneOnSelect) { + this->connect(); + + SQLLEN rowCount = 0; + SQLLEN expectedValue = -1; + SQLWCHAR sqlQuery[] = L"SELECT 1 AS col1, 'One' AS col2, 3 AS col3"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + CheckIntColumn(this->stmt, 1, 1); + CheckStringColumnW(this->stmt, 2, L"One"); + CheckIntColumn(this->stmt, 3, 3); + + ret = SQLRowCount(this->stmt, &rowCount); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(rowCount, expectedValue); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLRowCountReturnsSuccessOnNullptr) { + this->connect(); + + SQLWCHAR sqlQuery[] = L"SELECT 1 AS col1, 'One' AS col2, 3 AS col3"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + CheckIntColumn(this->stmt, 1, 1); + CheckStringColumnW(this->stmt, 2, L"One"); + CheckIntColumn(this->stmt, 3, 3); + + ret = SQLRowCount(this->stmt, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLRowCountFunctionSequenceErrorOnNoQuery) { + this->connect(); + + SQLLEN rowCount = 0; + SQLLEN expectedValue = 0; + + SQLRETURN ret = SQLRowCount(this->stmt, &rowCount); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY010); + + EXPECT_EQ(rowCount, expectedValue); + + this->disconnect(); +} + TYPED_TEST(FlightSQLODBCTestBase, TestSQLFreeStmtSQLClose) { this->connect(); From 5a1856a2a91df217269accb65c884528e49c65b1 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 31 Jul 2025 10:52:56 -0700 Subject: [PATCH 46/74] Fix merge conflicts --- .../accessors/date_array_accessor_test.cc | 5 ++- .../timestamp_array_accessor_test.cc | 45 +++---------------- 2 files changed, 8 insertions(+), 42 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc index e6c0044587e..fdd48d2706a 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/date_array_accessor_test.cc @@ -32,6 +32,7 @@ using arrow::NumericArray; using odbcabstraction::DATE_STRUCT; using odbcabstraction::OdbcVersion; +using odbcabstraction::tagDATE_STRUCT; using arrow::ArrayFromVector; using odbcabstraction::GetTimeForSecondsSinceEpoch; @@ -49,7 +50,7 @@ TEST(DateArrayAccessor, Test_Date32Array_CDataType_DATE) { DateArrayFlightSqlAccessor accessor( dynamic_cast*>(array.get())); - std::vector buffer(values.size()); + std::vector buffer(values.size()); std::vector strlen_buffer(values.size()); ColumnBinding binding(odbcabstraction::CDataType_DATE, 0, 0, buffer.data(), 0, @@ -87,7 +88,7 @@ TEST(DateArrayAccessor, Test_Date64Array_CDataType_DATE) { DateArrayFlightSqlAccessor accessor( dynamic_cast*>(array.get())); - std::vector buffer(values.size()); + std::vector buffer(values.size()); std::vector strlen_buffer(values.size()); ColumnBinding binding(odbcabstraction::CDataType_DATE, 0, 0, buffer.data(), 0, diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc index 248711bd861..930cc6a5654 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/accessors/timestamp_array_accessor_test.cc @@ -98,15 +98,8 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MILLI) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_SECONDS) { - std::vector values = {86400, 172800, 259200, 1649793238, 345600, - 432000, 518400, -86399, 0}; - std::vector expected = { - /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), - fraction(u32) */ - {1970, 1, 2, 0, 0, 0, 0}, {1970, 1, 3, 0, 0, 0, 0}, {1970, 1, 4, 0, 0, 0, 0}, - {2022, 4, 12, 19, 53, 58, 0}, {1970, 1, 5, 0, 0, 0, 0}, {1970, 1, 6, 0, 0, 0, 0}, - {1970, 1, 7, 0, 0, 0, 0}, {1969, 12, 31, 0, 0, 1, 0}, {1970, 1, 1, 0, 0, 0, 0}, - }; + std::vector values = {86400, 172800, 259200, 1649793238, + 345600, 432000, 518400}; std::shared_ptr timestamp_array; @@ -147,15 +140,7 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_SECONDS) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MICRO) { - std::vector values = {0, 86400000000, 1649793238000000, -86399999999, - -86399000001}; - std::vector expected = { - /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), - fraction(u32) */ - {1970, 1, 1, 0, 0, 0, 0}, {1970, 1, 2, 0, 0, 0, 0}, - {2022, 4, 12, 19, 53, 58, 0}, {1969, 12, 31, 0, 0, 0, 1000}, - {1969, 12, 31, 0, 0, 0, 999999000}, - }; + std::vector values = {86400000000, 1649793238000000}; std::shared_ptr timestamp_array; @@ -199,28 +184,7 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_MICRO) { } TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_NANO) { - std::vector values = {86400000010000, - 1649793238000000000, - -86399999999999, - -86399000000001, - 86400000000001, - 86400999999999, - 0, - -9223372036000000001}; - std::vector expected = { - /* year(16), month(u16), day(u16), hour(u16), minute(u16), second(u16), - fraction(u32) */ - {1970, 1, 2, 0, 0, 0, 10000}, - {2022, 4, 12, 19, 53, 58, 0}, - {1969, 12, 31, 0, 0, 0, 1}, - {1969, 12, 31, 0, 0, 0, 999999999}, - {1970, 1, 2, 0, 0, 0, 1}, - {1970, 1, 2, 0, 0, 0, 999999999}, - {1970, 1, 1, 0, 0, 0, 0}, - /* Test within range where floor (seconds) value is below INT64_MIN in nanoseconds - */ - {1677, 9, 21, 0, 12, 43, 999999999}, - }; + std::vector values = {86400000010000, 1649793238000000000}; std::shared_ptr timestamp_array; @@ -245,6 +209,7 @@ TEST(TEST_TIMESTAMP, TIMESTAMP_WITH_NANO) { for (size_t i = 0; i < values.size(); ++i) { ASSERT_EQ(sizeof(TIMESTAMP_STRUCT), strlen_buffer[i]); + tm date{}; auto converted_time = values[i] / odbcabstraction::NANO_TO_SECONDS_DIVISOR; GetTimeForSecondsSinceEpoch(converted_time, date); From 8e978a22a0e08e4a9c36184d494ec84fac15413d Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 31 Jul 2025 11:43:12 -0700 Subject: [PATCH 47/74] Adjust searchable return value based on GH-46920 fix --- .../flight/sql/odbc/tests/columns_test.cc | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 325e59554ce..08a86d4a840 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -1587,7 +1587,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 4, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 2, @@ -1604,7 +1604,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 3, @@ -1621,7 +1621,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 40, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 4, @@ -1638,7 +1638,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 2, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 5, @@ -1655,7 +1655,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 2, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 6, @@ -1672,7 +1672,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 1, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 7, @@ -1689,7 +1689,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 6, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 8, @@ -1706,7 +1706,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 6, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 9, @@ -1723,7 +1723,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypes) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 16, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn this->disconnect(); @@ -1757,7 +1757,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 4, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 2, @@ -1774,7 +1774,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 3, @@ -1791,7 +1791,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 10, // expectedNumPrecRadix 40, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 4, @@ -1808,7 +1808,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 2, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 5, @@ -1825,7 +1825,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 2, // expectedNumPrecRadix 8, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 6, @@ -1842,7 +1842,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 1, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 7, @@ -1859,7 +1859,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 6, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 8, @@ -1876,7 +1876,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 6, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttribute(this->stmt, 9, @@ -1893,7 +1893,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributeAllTypesODBCVer2) { SQL_NULLABLE, // expectedColumnNullability 0, // expectedNumPrecRadix 16, // expectedOctetLength - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn this->disconnect(); @@ -1923,7 +1923,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 4, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 2, @@ -1935,7 +1935,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 8, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 3, @@ -1947,7 +1947,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 19, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 4, @@ -1959,7 +1959,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 8, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 5, @@ -1971,7 +1971,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 8, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_FALSE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 6, @@ -1983,7 +1983,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 1, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 7, @@ -1995,7 +1995,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 10, // expectedColumnSize 0, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 8, @@ -2007,7 +2007,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 12, // expectedColumnSize 3, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn checkSQLColAttributes(this->stmt, 9, @@ -2019,7 +2019,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, TestSQLColAttributesAllTypesODBCVer2) { 23, // expectedColumnSize 3, // expectedColumnScale SQL_NULLABLE, // expectedColumnNullability - SQL_PRED_NONE, // expectedSearchable + SQL_SEARCHABLE, // expectedSearchable SQL_TRUE); // expectedUnsignedColumn this->disconnect(); From dd5b5747c5becf8efcf5a1dc96268913f3881044 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:56:36 -0700 Subject: [PATCH 48/74] Fix `getCTypeForSQLType` return for interval types - Fix `getCTypeForSQLType` function to return the correct `C type` for interval SQL data types. Previously the function was checking for interval `C type` and returning interval `SQL type`, which was the opposite of the intended functionality. --- .../odbc_impl/odbc_statement.cc | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc index c990f766df5..bda30f4466c 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/odbc_impl/odbc_statement.cc @@ -175,32 +175,32 @@ SQLSMALLINT getCTypeForSQLType(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.m_conciseType), From 46b1825de317aae7a2b44629c77f3cee33a83079 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:18:06 -0700 Subject: [PATCH 49/74] SQLGetTypeInfo implementation * Add tests for SQLGetTypeInfo * fix bug of createParams returning empty string instead of null * fix bug of non-concise data type being returned for datetime/interval * fix bug of longvarchar not converted to wlongvarchar when driver has unicode enabled * Address comments from James and add checks * test SQL_ALL_TYPES in ODBC 2.x. * test SQL_TYPE_* in ODBC 2.x, the driver manager reports invalid data type error. * test SQL_* datetime in ODBC 3.x for backwards compatibility. * add check for valid/invalid data type. --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 7 +- .../flight_sql_statement_get_type_info.cc | 8 +- .../arrow/flight/sql/odbc/flight_sql/utils.cc | 7 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 59 + cpp/src/arrow/flight/sql/odbc/odbc_api.h | 1 + .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/odbc_test_suite.h | 2 + .../flight/sql/odbc/tests/tables_test.cc | 3 - .../flight/sql/odbc/tests/type_info_test.cc | 1886 +++++++++++++++++ 9 files changed, 1962 insertions(+), 12 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 52ab94dd74f..63faa57e45b 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -245,12 +245,7 @@ SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT stmt, SQLWCHAR* pKCatalogName, } SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType) { - LOG_DEBUG("SQLGetTypeInfoW called with stmt: {} dataType: {}", stmt, dataType); - return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { - throw driver::odbcabstraction::DriverException("SQLGetTypeInfoW is not implemented", - "IM001"); - return SQL_ERROR; - }); + return arrow::SQLGetTypeInfo(stmt, dataType); } SQLRETURN SQL_API SQLMoreResults(SQLHSTMT stmt) { return arrow::SQLMoreResults(stmt); } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_type_info.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_type_info.cc index 3faf607c5f2..3f59f4abcc1 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_type_info.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_type_info.cc @@ -105,7 +105,7 @@ Result> Transform_inner( data.literal_suffix = reader.GetLiteralSuffix(); const auto& create_params = reader.GetCreateParams(); - if (create_params) { + if (create_params && !create_params->empty()) { data.create_params = boost::algorithm::join(*create_params, ","); } else { data.create_params = nullopt; @@ -114,6 +114,8 @@ Result> Transform_inner( data.nullable = reader.GetNullable() ? odbcabstraction::NULLABILITY_NULLABLE : odbcabstraction::NULLABILITY_NO_NULLS; data.case_sensitive = reader.GetCaseSensitive(); + // GH-47237 return SEARCHABILITY_LIKE_ONLY and SEARCHABILITY_ALL_EXPECT_LIKE for + // appropriate data types data.searchable = reader.GetSearchable() ? odbcabstraction::SEARCHABILITY_ALL : odbcabstraction::SEARCHABILITY_NONE; data.unsigned_attribute = reader.GetUnsignedAttribute(); @@ -122,9 +124,9 @@ Result> Transform_inner( data.local_type_name = reader.GetLocalTypeName(); data.minimum_scale = reader.GetMinimumScale(); data.maximum_scale = reader.GetMaximumScale(); - data.sql_data_type = EnsureRightSqlCharType( + data.sql_data_type = GetNonConciseDataType(EnsureRightSqlCharType( static_cast(reader.GetSqlDataType()), - metadata_settings_.use_wide_char_); + metadata_settings_.use_wide_char_)); data.sql_datetime_sub = GetSqlDateTimeSubCode(static_cast(data.data_type)); data.num_prec_radix = reader.GetNumPrecRadix(); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.cc index 33a11aaabed..945d3c9f0da 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/utils.cc @@ -59,6 +59,10 @@ odbcabstraction::SqlDataType GetDefaultSqlVarcharType(bool useWideChar) { return useWideChar ? odbcabstraction::SqlDataType_WVARCHAR : odbcabstraction::SqlDataType_VARCHAR; } +odbcabstraction::SqlDataType GetDefaultSqlLongVarcharType(bool useWideChar) { + return useWideChar ? odbcabstraction::SqlDataType_WLONGVARCHAR + : odbcabstraction::SqlDataType_LONGVARCHAR; +} odbcabstraction::CDataType GetDefaultCCharType(bool useWideChar) { return useWideChar ? odbcabstraction::CDataType_WCHAR : odbcabstraction::CDataType_CHAR; } @@ -155,6 +159,9 @@ SqlDataType EnsureRightSqlCharType(SqlDataType data_type, bool useWideChar) { case odbcabstraction::SqlDataType_VARCHAR: case odbcabstraction::SqlDataType_WVARCHAR: return GetDefaultSqlVarcharType(useWideChar); + case odbcabstraction::SqlDataType_LONGVARCHAR: + case odbcabstraction::SqlDataType_WLONGVARCHAR: + return GetDefaultSqlLongVarcharType(useWideChar); default: return data_type; } diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 23f7a2b011c..0331306e31d 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1280,6 +1280,65 @@ SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, }); } +SQLRETURN SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType) { + // GH-47237 return SQL_PRED_CHAR and SQL_PRED_BASIC for + // appropriate data types in `SEARCHABLE` field + LOG_DEBUG("SQLGetTypeInfoW called with stmt: {} dataType: {}", stmt, dataType); + using ODBC::ODBCStatement; + return ODBC::ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + + switch (dataType) { + case SQL_ALL_TYPES: + case SQL_CHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + case SQL_BIT: + case SQL_BINARY: + case SQL_VARBINARY: + case SQL_LONGVARBINARY: + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_NUMERIC: + case SQL_DECIMAL: + case SQL_FLOAT: + case SQL_REAL: + case SQL_DOUBLE: + case SQL_GUID: + case SQL_DATE: + case SQL_TYPE_DATE: + case SQL_TIME: + case SQL_TYPE_TIME: + case SQL_TIMESTAMP: + case SQL_TYPE_TIMESTAMP: + case SQL_INTERVAL_DAY: + case SQL_INTERVAL_DAY_TO_HOUR: + case SQL_INTERVAL_DAY_TO_MINUTE: + case SQL_INTERVAL_DAY_TO_SECOND: + case SQL_INTERVAL_HOUR: + case SQL_INTERVAL_HOUR_TO_MINUTE: + case SQL_INTERVAL_HOUR_TO_SECOND: + case SQL_INTERVAL_MINUTE: + case SQL_INTERVAL_MINUTE_TO_SECOND: + case SQL_INTERVAL_SECOND: + case SQL_INTERVAL_YEAR: + case SQL_INTERVAL_YEAR_TO_MONTH: + case SQL_INTERVAL_MONTH: + statement->GetTypeInfo(dataType); + break; + default: + throw DriverException("Invalid SQL data type", "HY004"); + } + + return SQL_SUCCESS; + }); +} + SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength) { diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index e4139f96333..6aa2ec681af 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -92,6 +92,7 @@ SQLRETURN SQLColAttribute(SQLHSTMT stmt, SQLUSMALLINT recordNumber, SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttributePtr, SQLSMALLINT bufferLength, SQLSMALLINT* outputLength, SQLLEN* numericAttributePtr); +SQLRETURN SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType); SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index d5662d088ca..0262011ddaa 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -37,6 +37,7 @@ add_arrow_test(flight_sql_odbc_test statement_attr_test.cc statement_test.cc tables_test.cc + type_info_test.cc # Connection test needs to be put last to resolve segfault issue connection_test.cc odbc_test_suite.cc 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 ac8568900d5..960da101fd3 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 @@ -173,6 +173,7 @@ static constexpr std::string_view error_state_22002 = "22002"; static constexpr std::string_view error_state_24000 = "24000"; static constexpr std::string_view error_state_28000 = "28000"; static constexpr std::string_view error_state_HY000 = "HY000"; +static constexpr std::string_view error_state_HY004 = "HY004"; static constexpr std::string_view error_state_HY009 = "HY009"; static constexpr std::string_view error_state_HY010 = "HY010"; static constexpr std::string_view error_state_HY017 = "HY017"; @@ -184,6 +185,7 @@ 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_HY118 = "HY118"; static constexpr std::string_view error_state_HYC00 = "HYC00"; +static constexpr std::string_view error_state_S1004 = "S1004"; /// Verify ODBC Error State void VerifyOdbcErrorState(SQLSMALLINT handle_type, SQLHANDLE handle, diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index d2e7aab7d45..1dc0c90245e 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -16,9 +16,6 @@ // under the License. #include "arrow/flight/sql/odbc/tests/odbc_test_suite.h" -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h" -#include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/spi/statement.h" - #ifdef _WIN32 # include #endif diff --git a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc new file mode 100644 index 00000000000..e642575210b --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc @@ -0,0 +1,1886 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +// TODO: add tests with SQLDescribeCol to check metadata of SQLGetTypeInfo for ODBC 2 and +// ODBC 3. + +namespace arrow::flight::sql::odbc { + +using std::optional; + +void checkSQLGetTypeInfo( + SQLHSTMT stmt, const std::wstring& expectedTypeName, + const SQLSMALLINT& expectedDataType, const SQLINTEGER& expectedColumnSize, + const optional& expectedLiteralPrefix, + const optional& expectedLiteralSuffix, + const optional& expectedCreateParams, + const SQLSMALLINT& expectedNullable, const SQLSMALLINT& expectedCaseSensitive, + const SQLSMALLINT& expectedSearchable, const SQLSMALLINT& expectedUnsignedAttr, + const SQLSMALLINT& expectedFixedPrecScale, const SQLSMALLINT& expectedAutoUniqueValue, + const std::wstring& expectedLocalTypeName, const SQLSMALLINT& expectedMinScale, + const SQLSMALLINT& expectedMaxScale, const SQLSMALLINT& expectedSqlDataType, + const SQLSMALLINT& expectedSqlDatetimeSub, const SQLINTEGER& expectedNumPrecRadix, + const SQLINTEGER& expectedIntervalPrec) { + CheckStringColumnW(stmt, 1, expectedTypeName); // type name + CheckSmallIntColumn(stmt, 2, expectedDataType); // data type + CheckIntColumn(stmt, 3, expectedColumnSize); // column size + + if (expectedLiteralPrefix) { // literal prefix + CheckStringColumnW(stmt, 4, *expectedLiteralPrefix); + } else { + CheckNullColumnW(stmt, 4); + } + + if (expectedLiteralSuffix) { // literal suffix + CheckStringColumnW(stmt, 5, *expectedLiteralSuffix); + } else { + CheckNullColumnW(stmt, 5); + } + + if (expectedCreateParams) { // create params + CheckStringColumnW(stmt, 6, *expectedCreateParams); + } else { + CheckNullColumnW(stmt, 6); + } + + CheckSmallIntColumn(stmt, 7, expectedNullable); // nullable + CheckSmallIntColumn(stmt, 8, expectedCaseSensitive); // case sensitive + CheckSmallIntColumn(stmt, 9, expectedSearchable); // searchable + CheckSmallIntColumn(stmt, 10, expectedUnsignedAttr); // unsigned attr + CheckSmallIntColumn(stmt, 11, expectedFixedPrecScale); // fixed prec scale + CheckSmallIntColumn(stmt, 12, expectedAutoUniqueValue); // auto unique value + CheckStringColumnW(stmt, 13, expectedLocalTypeName); // local type name + CheckSmallIntColumn(stmt, 14, expectedMinScale); // min scale + CheckSmallIntColumn(stmt, 15, expectedMaxScale); // max scale + CheckSmallIntColumn(stmt, 16, expectedSqlDataType); // sql data type + CheckSmallIntColumn(stmt, 17, expectedSqlDatetimeSub); // sql datetime sub + CheckIntColumn(stmt, 18, expectedNumPrecRadix); // num prec radix + CheckIntColumn(stmt, 19, expectedIntervalPrec); // interval prec +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_ALL_TYPES); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check bit data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bit"), // expectedTypeName + SQL_BIT, // expectedDataType + 1, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bit"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check tinyint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"tinyint"), // expectedTypeName + SQL_TINYINT, // expectedDataType + 3, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"tinyint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_TINYINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check bigint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bigint"), // expectedTypeName + SQL_BIGINT, // expectedDataType + 19, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bigint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check longvarbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarbinary"), // expectedTypeName + SQL_LONGVARBINARY, // expectedDataType + 65536, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_LONGVARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check varbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varbinary"), // expectedTypeName + SQL_VARBINARY, // expectedDataType + 255, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_VARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check text data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WLONGVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"text"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"text"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check longvarchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarchar"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check char data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"char"), // expectedTypeName + SQL_WCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"char"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check integer data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"integer"), // expectedTypeName + SQL_INTEGER, // expectedDataType + 9, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"integer"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_INTEGER, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check smallint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"smallint"), // expectedTypeName + SQL_SMALLINT, // expectedDataType + 5, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"smallint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_SMALLINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check float data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"float"), // expectedTypeName + SQL_FLOAT, // expectedDataType + 7, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"float"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_FLOAT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check double data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"double"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"double"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check numeric data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Mock server treats numeric data type as a double type + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"numeric"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"numeric"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check varchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varchar"), // expectedTypeName + SQL_WVARCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check date data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"date"), // expectedTypeName + SQL_TYPE_DATE, // expectedDataType + 10, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"date"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_DATE, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check time data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"time"), // expectedTypeName + SQL_TYPE_TIME, // expectedDataType + 8, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"time"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIME, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check timestamp data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"timestamp"), // expectedTypeName + SQL_TYPE_TIMESTAMP, // expectedDataType + 32, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"timestamp"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIMESTAMP, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesVer2) { + this->connect(SQL_OV_ODBC2); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_ALL_TYPES); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check bit data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bit"), // expectedTypeName + SQL_BIT, // expectedDataType + 1, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bit"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check tinyint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"tinyint"), // expectedTypeName + SQL_TINYINT, // expectedDataType + 3, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"tinyint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_TINYINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check bigint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bigint"), // expectedTypeName + SQL_BIGINT, // expectedDataType + 19, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bigint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check longvarbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarbinary"), // expectedTypeName + SQL_LONGVARBINARY, // expectedDataType + 65536, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_LONGVARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check varbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varbinary"), // expectedTypeName + SQL_VARBINARY, // expectedDataType + 255, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_VARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check text data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WLONGVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"text"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"text"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check longvarchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarchar"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check char data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"char"), // expectedTypeName + SQL_WCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"char"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check integer data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"integer"), // expectedTypeName + SQL_INTEGER, // expectedDataType + 9, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"integer"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_INTEGER, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check smallint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"smallint"), // expectedTypeName + SQL_SMALLINT, // expectedDataType + 5, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"smallint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_SMALLINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check float data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"float"), // expectedTypeName + SQL_FLOAT, // expectedDataType + 7, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"float"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_FLOAT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check double data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"double"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"double"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check numeric data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Mock server treats numeric data type as a double type + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"numeric"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"numeric"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check varchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varchar"), // expectedTypeName + SQL_WVARCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check date data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"date"), // expectedTypeName + SQL_DATE, // expectedDataType + 10, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"date"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check time data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"time"), // expectedTypeName + SQL_TIME, // expectedDataType + 8, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"time"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check timestamp data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"timestamp"), // expectedTypeName + SQL_TIMESTAMP, // expectedDataType + 32, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"timestamp"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoBit) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_BIT); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check bit data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bit"), // expectedTypeName + SQL_BIT, // expectedDataType + 1, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bit"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTinyInt) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TINYINT); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check tinyint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"tinyint"), // expectedTypeName + SQL_TINYINT, // expectedDataType + 3, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"tinyint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_TINYINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoBigInt) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_BIGINT); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check bigint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"bigint"), // expectedTypeName + SQL_BIGINT, // expectedDataType + 19, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"bigint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_BIGINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoLongVarbinary) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_LONGVARBINARY); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check longvarbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarbinary"), // expectedTypeName + SQL_LONGVARBINARY, // expectedDataType + 65536, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_LONGVARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoVarbinary) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_VARBINARY); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check varbinary data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varbinary"), // expectedTypeName + SQL_VARBINARY, // expectedDataType + 255, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varbinary"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_VARBINARY, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoLongVarchar) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_WLONGVARCHAR); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check text data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WLONGVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"text"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"text"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check longvarchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"longvarchar"), // expectedTypeName + SQL_WLONGVARCHAR, // expectedDataType + 65536, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"longvarchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WLONGVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoChar) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_WCHAR); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check char data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"char"), // expectedTypeName + SQL_WCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + NULL, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"char"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoInteger) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_INTEGER); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check integer data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"integer"), // expectedTypeName + SQL_INTEGER, // expectedDataType + 9, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"integer"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_INTEGER, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSmallInt) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_SMALLINT); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check smallint data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"smallint"), // expectedTypeName + SQL_SMALLINT, // expectedDataType + 5, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"smallint"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_SMALLINT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoFloat) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_FLOAT); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check float data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"float"), // expectedTypeName + SQL_FLOAT, // expectedDataType + 7, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"float"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_FLOAT, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDouble) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_DOUBLE); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check double data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"double"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"double"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // Check numeric data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Mock server treats numeric data type as a double type + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"numeric"), // expectedTypeName + SQL_DOUBLE, // expectedDataType + 15, // expectedColumnSize + std::nullopt, // expectedLiteralPrefix + std::nullopt, // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"numeric"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DOUBLE, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoVarchar) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_WVARCHAR); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check varchar data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Driver returns SQL_WVARCHAR since unicode is enabled + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"varchar"), // expectedTypeName + SQL_WVARCHAR, // expectedDataType + 255, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::wstring(L"length"), // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"varchar"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_WVARCHAR, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeDate) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_DATE); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check date data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"date"), // expectedTypeName + SQL_TYPE_DATE, // expectedDataType + 10, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"date"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_DATE, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLDate) { + this->connect(); + + // Pass ODBC Ver 2 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_DATE); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check date data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"date"), // expectedTypeName + SQL_TYPE_DATE, // expectedDataType + 10, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"date"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_DATE, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDateVer2) { + this->connect(SQL_OV_ODBC2); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_DATE); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check date data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"date"), // expectedTypeName + SQL_DATE, // expectedDataType + 10, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"date"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeDateVer2) { + this->connect(SQL_OV_ODBC2); + + // Pass ODBC Ver 3 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_DATE); + + EXPECT_EQ(ret, SQL_ERROR); + + // Driver manager returns SQL data type out of range error state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_S1004); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTime) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_TIME); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check time data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"time"), // expectedTypeName + SQL_TYPE_TIME, // expectedDataType + 8, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"time"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIME, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTime) { + this->connect(); + + // Pass ODBC Ver 2 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIME); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check time data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"time"), // expectedTypeName + SQL_TYPE_TIME, // expectedDataType + 8, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"time"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIME, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTimeVer2) { + this->connect(SQL_OV_ODBC2); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIME); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check time data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"time"), // expectedTypeName + SQL_TIME, // expectedDataType + 8, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"time"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimeVer2) { + this->connect(SQL_OV_ODBC2); + + // Pass ODBC Ver 3 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_TIME); + + EXPECT_EQ(ret, SQL_ERROR); + + // Driver manager returns SQL data type out of range error state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_S1004); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimestamp) { + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_TIMESTAMP); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check timestamp data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"timestamp"), // expectedTypeName + SQL_TYPE_TIMESTAMP, // expectedDataType + 32, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"timestamp"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIMESTAMP, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestamp) { + this->connect(); + + // Pass ODBC Ver 2 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIMESTAMP); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check timestamp data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"timestamp"), // expectedTypeName + SQL_TYPE_TIMESTAMP, // expectedDataType + 32, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"timestamp"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + SQL_CODE_TIMESTAMP, // expectedSqlDatetimeSub + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestampVer2) { + this->connect(SQL_OV_ODBC2); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIMESTAMP); + EXPECT_EQ(ret, SQL_SUCCESS); + + // Check timestamp data type + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + checkSQLGetTypeInfo(this->stmt, + std::wstring(L"timestamp"), // expectedTypeName + SQL_TIMESTAMP, // expectedDataType + 32, // expectedColumnSize + std::wstring(L"'"), // expectedLiteralPrefix + std::wstring(L"'"), // expectedLiteralSuffix + std::nullopt, // expectedCreateParams + SQL_NULLABLE, // expectedNullable + SQL_FALSE, // expectedCaseSensitive + SQL_SEARCHABLE, // expectedSearchable + SQL_FALSE, // expectedUnsignedAttr + SQL_FALSE, // expectedFixedPrecScale + NULL, // expectedAutoUniqueValue + std::wstring(L"timestamp"), // expectedLocalTypeName + NULL, // expectedMinScale + NULL, // expectedMaxScale + SQL_DATETIME, // expectedSqlDataType + NULL, // expectedSqlDatetimeSub, driver returns NULL for Ver2 + NULL, // expectedNumPrecRadix + NULL); // expectedIntervalPrec + + // No more data + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimestampVer2) { + this->connect(SQL_OV_ODBC2); + + // Pass ODBC Ver 3 data type + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TYPE_TIMESTAMP); + + EXPECT_EQ(ret, SQL_ERROR); + + // Driver manager returns SQL data type out of range error state + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_S1004); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoInvalidDataType) { + this->connect(); + + SQLSMALLINT invalidDataType = -114; + SQLRETURN ret = SQLGetTypeInfo(this->stmt, invalidDataType); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_HY004); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetTypeInfoUnsupportedDataType) { + // Assumes mock and remote server don't support GUID data type + this->connect(); + + SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_GUID); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Result set is empty with valid data type that is unsupported by the server + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_NO_DATA); + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc From 7c60479fcd33fefdb3799fb7636f50ede56e07b1 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:16:08 -0700 Subject: [PATCH 50/74] Fix segfault issue from empty metadata * Use empty map in bug fix * Address comments from James --- cpp/src/arrow/flight/sql/column_metadata.cc | 11 +++++++++-- .../odbc/flight_sql/flight_sql_result_set_metadata.cc | 8 +------- .../flight_sql/flight_sql_statement_get_columns.cc | 4 ++-- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cpp/src/arrow/flight/sql/column_metadata.cc b/cpp/src/arrow/flight/sql/column_metadata.cc index 30f557084b2..8d2d2b4ddca 100644 --- a/cpp/src/arrow/flight/sql/column_metadata.cc +++ b/cpp/src/arrow/flight/sql/column_metadata.cc @@ -58,8 +58,15 @@ const char* ColumnMetadata::kIsSearchable = "ARROW:FLIGHT:SQL:IS_SEARCHABLE"; const char* ColumnMetadata::kRemarks = "ARROW:FLIGHT:SQL:REMARKS"; ColumnMetadata::ColumnMetadata( - std::shared_ptr metadata_map) - : metadata_map_(std::move(metadata_map)) {} + std::shared_ptr metadata_map) { + if (metadata_map) { + metadata_map_ = std::move(metadata_map); + } else { + std::shared_ptr empty_metadata_map( + new arrow::KeyValueMetadata); + metadata_map_ = std::move(empty_metadata_map); + } +} arrow::Result ColumnMetadata::GetCatalogName() const { return metadata_map_->Get(kCatalogName); diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc index 710f7608ecf..0fa6b03c4a7 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_result_set_metadata.cc @@ -42,15 +42,9 @@ constexpr int32_t DefaultDecimalPrecision = 38; constexpr int32_t DefaultLengthForVariableLengthColumns = 1024; namespace { -std::shared_ptr empty_metadata_map( - new arrow::KeyValueMetadata); - inline arrow::flight::sql::ColumnMetadata GetMetadata( const std::shared_ptr& field) { - const auto& metadata_map = field->metadata(); - - arrow::flight::sql::ColumnMetadata metadata(metadata_map ? metadata_map - : empty_metadata_map); + arrow::flight::sql::ColumnMetadata metadata(field->metadata()); return metadata; } diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc index d3250401193..f61c198cd23 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_columns.cc @@ -125,8 +125,8 @@ Result> Transform_inner( ? data_type_v3 : ConvertSqlDataTypeFromV3ToV2(data_type_v3); - // TODO: Use `metadata.GetTypeName()` when ARROW-16064 is merged. - const auto& type_name_result = field->metadata()->Get("ARROW:FLIGHT:SQL:TYPE_NAME"); + const auto& type_name_result = metadata.GetTypeName(); + data.type_name = type_name_result.ok() ? type_name_result.ValueOrDie() : GetTypeNameFromSqlDataType(data_type_v3); diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 0331306e31d..461e17fae5f 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -962,7 +962,6 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { using ODBC::ODBCDescriptor; using ODBC::ODBCStatement; - return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); @@ -970,6 +969,7 @@ SQLRETURN SQLFetch(SQLHSTMT stmt) { // rowset. ODBCDescriptor* ard = statement->GetARD(); size_t rows = static_cast(ard->GetArraySize()); + if (statement->Fetch(rows)) { return SQL_SUCCESS; } else { From c9a1718c1fa3786ba448cf5e5fb8fc316e34668a Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 17:04:25 -0700 Subject: [PATCH 51/74] Implement SQLDescribeCol --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 10 + cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 114 ++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 5 + .../flight/sql/odbc/tests/columns_test.cc | 557 +++++++++++++++++- .../flight/sql/odbc/tests/tables_test.cc | 108 +++- 6 files changed, 789 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 63faa57e45b..38b4a1fc8ed 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -287,3 +287,13 @@ SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER SQLINTEGER stringLength) { return arrow::SQLSetStmtAttr(stmt, attribute, valuePtr, stringLength); } + +SQLRETURN SQL_API SQLDescribeCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLWCHAR* columnName, SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr) { + return arrow::SQLDescribeCol(statementHandle, columnNumber, columnName, bufferLength, + nameLengthPtr, dataTypePtr, columnSizePtr, + decimalDigitsPtr, nullablePtr); +} diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index 6a7402ffa90..8ba5b3fff78 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -28,6 +28,7 @@ EXPORTS SQLColAttributeW SQLColumnsW SQLConnectW + SQLDescribeColW SQLDisconnect SQLDriverConnectW SQLExecDirectW diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 461e17fae5f..3ccde05ca5b 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1367,4 +1367,118 @@ SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, }); } +SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* columnName, + SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr) { + LOG_DEBUG( + "SQLDescribeColW called with stmt: {}, columnNumber: {}, " + "columnName: {}, bufferLength: {}, nameLengthPtr: {}, dataTypePtr: {}, " + "columnSizePtr: {}, decimalDigitsPtr: {}, nullablePtr: {}", + stmt, columnNumber, fmt::ptr(columnName), bufferLength, fmt::ptr(nameLengthPtr), + fmt::ptr(dataTypePtr), fmt::ptr(columnSizePtr), fmt::ptr(decimalDigitsPtr), + fmt::ptr(nullablePtr)); + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + ODBCDescriptor* ird = statement->GetIRD(); + SQLINTEGER outputLengthInt; + SQLSMALLINT sqlType; + + // Column SQL Type + ird->GetField(columnNumber, SQL_DESC_CONCISE_TYPE, &sqlType, sizeof(SQLSMALLINT), + nullptr); + if (dataTypePtr) { + *dataTypePtr = sqlType; + } + + // Column Name + if (columnName || nameLengthPtr) { + ird->GetField(columnNumber, SQL_DESC_NAME, columnName, bufferLength, + &outputLengthInt); + if (nameLengthPtr) { + *nameLengthPtr = static_cast(outputLengthInt); + } + } + + // Column Size + if (columnSizePtr) { + switch (sqlType) { + // All numeric types + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_REAL: + case SQL_FLOAT: + case SQL_DOUBLE: { + ird->GetField(columnNumber, SQL_DESC_PRECISION, columnSizePtr, sizeof(SQLULEN), + nullptr); + break; + } + + default: { + ird->GetField(columnNumber, SQL_DESC_LENGTH, columnSizePtr, sizeof(SQLULEN), + nullptr); + } + } + } + + // Column Decimal Digits + if (decimalDigitsPtr) { + switch (sqlType) { + // All exact numeric types + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: { + ird->GetField(columnNumber, SQL_DESC_SCALE, decimalDigitsPtr, sizeof(SQLULEN), + nullptr); + break; + } + + // All datetime types (ODBC2) + case SQL_DATE: + case SQL_TIME: + case SQL_TIMESTAMP: + // All datetime types (ODBC3) + case SQL_TYPE_DATE: + case SQL_TYPE_TIME: + case SQL_TYPE_TIMESTAMP: + // All interval types with a seconds component + case SQL_INTERVAL_SECOND: + case SQL_INTERVAL_MINUTE_TO_SECOND: + case SQL_INTERVAL_HOUR_TO_SECOND: + case SQL_INTERVAL_DAY_TO_SECOND: { + ird->GetField(columnNumber, SQL_DESC_PRECISION, decimalDigitsPtr, + sizeof(SQLULEN), nullptr); + break; + } + + default: { + // All character and binary types + // SQL_BIT + // All approximate numeric types + // All interval types with no seconds component + *decimalDigitsPtr = static_cast(0); + } + } + } + + // Column Nullable + if (nullablePtr) { + ird->GetField(columnNumber, SQL_DESC_NULLABLE, nullablePtr, sizeof(SQLSMALLINT), + nullptr); + } + + return SQL_SUCCESS; + }); +} + } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 6aa2ec681af..94a7dc0ec3e 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -96,4 +96,9 @@ SQLRETURN SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType); SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength); +SQLRETURN SQLDescribeCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLWCHAR* columnName, SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr); } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 08a86d4a840..04ca7ec9b96 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -26,9 +26,6 @@ #include "gtest/gtest.h" -// TODO: add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and -// ODBC 3. - namespace arrow::flight::sql::odbc { // Helper functions void checkSQLColumns( @@ -2321,4 +2318,558 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributesUpdatable) { this->disconnect(); } + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColValidateInput) { + this->connect(); + this->CreateTestTables(); + + SQLSMALLINT columnCount = 0; + SQLSMALLINT expectedValue = 3; + SQLWCHAR sqlQuery[] = L"SELECT * FROM TestTable LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLUSMALLINT bookmarkColumn = 0; + SQLUSMALLINT validColumn = 1; + SQLUSMALLINT outOfRangeColumn = 4; + SQLUSMALLINT negativeColumn = -1; + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT dataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Invalid descriptor index - Bookmarks are not supported + ret = SQLDescribeCol(this->stmt, bookmarkColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + // Invalid descriptor index - index out of range + ret = SQLDescribeCol(this->stmt, outOfRangeColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + // Invalid descriptor index - index out of range + ret = SQLDescribeCol(this->stmt, negativeColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadata) { + // Mock server has a limitation where only SQL_WVARCHAR column type values are returned + // from SELECT AS queries + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"stiny_int_min", (SQLWCHAR*)L"stiny_int_max", + (SQLWCHAR*)L"utiny_int_min", (SQLWCHAR*)L"utiny_int_max", + (SQLWCHAR*)L"ssmall_int_min", (SQLWCHAR*)L"ssmall_int_max", + (SQLWCHAR*)L"usmall_int_min", (SQLWCHAR*)L"usmall_int_max", + (SQLWCHAR*)L"sinteger_min", (SQLWCHAR*)L"sinteger_max", + (SQLWCHAR*)L"uinteger_min", (SQLWCHAR*)L"uinteger_max", + (SQLWCHAR*)L"sbigint_min", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"ubigint_min", (SQLWCHAR*)L"ubigint_max", + (SQLWCHAR*)L"decimal_negative", (SQLWCHAR*)L"decimal_positive", + (SQLWCHAR*)L"float_min", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_min", (SQLWCHAR*)L"double_max", + (SQLWCHAR*)L"bit_false", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", + (SQLWCHAR*)L"c_wvarchar", (SQLWCHAR*)L"c_varchar", + (SQLWCHAR*)L"date_min", (SQLWCHAR*)L"date_max", + (SQLWCHAR*)L"timestamp_min", (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = { + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR}; + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, 1024); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadata) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"stiny_int_min", (SQLWCHAR*)L"stiny_int_max", + (SQLWCHAR*)L"utiny_int_min", (SQLWCHAR*)L"utiny_int_max", + (SQLWCHAR*)L"ssmall_int_min", (SQLWCHAR*)L"ssmall_int_max", + (SQLWCHAR*)L"usmall_int_min", (SQLWCHAR*)L"usmall_int_max", + (SQLWCHAR*)L"sinteger_min", (SQLWCHAR*)L"sinteger_max", + (SQLWCHAR*)L"uinteger_min", (SQLWCHAR*)L"uinteger_max", + (SQLWCHAR*)L"sbigint_min", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"ubigint_min", (SQLWCHAR*)L"ubigint_max", + (SQLWCHAR*)L"decimal_negative", (SQLWCHAR*)L"decimal_positive", + (SQLWCHAR*)L"float_min", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_min", (SQLWCHAR*)L"double_max", + (SQLWCHAR*)L"bit_false", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", + (SQLWCHAR*)L"c_wvarchar", (SQLWCHAR*)L"c_varchar", + (SQLWCHAR*)L"date_min", (SQLWCHAR*)L"date_max", + (SQLWCHAR*)L"timestamp_min", (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = { + SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, + SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, + SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, + SQL_WVARCHAR, SQL_DECIMAL, SQL_DECIMAL, SQL_FLOAT, SQL_FLOAT, + SQL_DOUBLE, SQL_DOUBLE, SQL_BIT, SQL_BIT, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_TYPE_DATE, SQL_TYPE_DATE, + SQL_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, + 8, 8, 8, 8, 65536, 19, 19, 8, 8, 8, 8, + 1, 1, 65536, 65536, 65536, 65536, 10, 10, 23, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 23, 23}; + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadata) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from $scratch.ODBCTest LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"sinteger_max", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"decimal_positive", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", + (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQL_TYPE_DATE, SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataODBC2) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from $scratch.ODBCTest LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"sinteger_max", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"decimal_positive", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", + (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQL_DATE, SQL_TIME, SQL_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadata) { + this->connect(); + this->CreateTableAllDataType(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from AllTypesTable LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"bigint_col", (SQLWCHAR*)L"char_col", + (SQLWCHAR*)L"varbinary_col", (SQLWCHAR*)L"double_col"}; + SQLSMALLINT columnDataTypes[] = {SQL_BIGINT, SQL_WVARCHAR, SQL_BINARY, SQL_DOUBLE}; + SQLULEN columnSizes[] = {8, 0, 0, 8}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadata) { + this->connect(); + this->CreateUnicodeTable(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 1; + + SQLWCHAR sqlQuery[] = L"SELECT * from 数据 LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR expectedColumnName[] = L"资料"; + SQLSMALLINT expectedColumnDataType = SQL_WVARCHAR; + SQLULEN expectedColumnSize = 0; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, expectedColumnName); + EXPECT_EQ(columnDataType, expectedColumnDataType); + EXPECT_EQ(columnSize, expectedColumnSize); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeCol) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeColODBC2) { + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", (SQLWCHAR*)L"TABLE_OWNER", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index 1dc0c90245e..68405c51583 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -28,9 +28,6 @@ namespace arrow::flight::sql::odbc { -// TODO: Add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and -// ODBC 3. - // Helper Functions std::wstring GetStringColumnW(SQLHSTMT stmt, int colId) { @@ -578,4 +575,109 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetSupportedTableTypes) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeCol) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"TABLE_TYPE", + (SQLWCHAR*)L"REMARKS"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 1024}; + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeColODBC2) { + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", (SQLWCHAR*)L"TABLE_OWNER", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"TABLE_TYPE", + (SQLWCHAR*)L"REMARKS"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 1024}; + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc From dc3dc58b320174963d16c55d6652b3ea6b7871c4 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:26:08 -0700 Subject: [PATCH 52/74] Add SQLError Tests * add test to free null handles. Without handle value initialization, segfault error was seen Move `SQLGetDiagField` and `SQLGetDiagRec` tests to `errors_test.cc` * Address comments from James * Add ODBC Ver 2 tests * update test name to indicate if error handling is from driver manager. * add tests for warnings. * fix lint errors. * remove SQL_ATTR_APP_ROW_DESC that is not applicable to Environment Attribute. --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 6 +- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../flight/sql/odbc/tests/connection_test.cc | 262 +------- .../flight/sql/odbc/tests/errors_test.cc | 610 ++++++++++++++++++ .../flight/sql/odbc/tests/odbc_test_suite.h | 6 +- 5 files changed, 634 insertions(+), 251 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/errors_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 3ccde05ca5b..e19553160f0 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -617,8 +617,7 @@ SQLRETURN SQLGetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, return SQL_SUCCESS; } - case SQL_ATTR_CONNECTION_POOLING: - case SQL_ATTR_APP_ROW_DESC: { + case SQL_ATTR_CONNECTION_POOLING: { throw DriverException("Optional feature not supported.", "HYC00"); } @@ -669,8 +668,7 @@ SQLRETURN SQLSetEnvAttr(SQLHENV env, SQLINTEGER attr, SQLPOINTER valuePtr, } } - case SQL_ATTR_CONNECTION_POOLING: - case SQL_ATTR_APP_ROW_DESC: { + case SQL_ATTR_CONNECTION_POOLING: { throw DriverException("Optional feature not supported.", "HYC00"); } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 0262011ddaa..606b3cb7e4a 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt @@ -34,6 +34,7 @@ add_arrow_test(flight_sql_odbc_test columns_test.cc connection_attr_test.cc connection_info_test.cc + errors_test.cc statement_attr_test.cc statement_test.cc tables_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index 81c4abe70cf..c935646de5b 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -128,6 +128,24 @@ TEST(SQLFreeHandle, TestSQLFreeHandleConnect) { EXPECT_TRUE(return_free_handle == SQL_SUCCESS); } +TYPED_TEST(FlightSQLODBCTestBase, TestFreeNullHandles) { + // Verifies attempt to free invalid handle does not cause segfault + // Attempt to free null statement handle + SQLRETURN ret = SQLFreeHandle(SQL_HANDLE_STMT, this->stmt); + + EXPECT_EQ(ret, SQL_INVALID_HANDLE); + + // Attempt to free null connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, this->conn); + + EXPECT_EQ(ret, SQL_INVALID_HANDLE); + + // Attempt to free null environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, this->env); + + EXPECT_EQ(ret, SQL_INVALID_HANDLE); +} + TEST(SQLFreeConnect, TestSQLFreeConnect) { // ODBC Environment SQLHENV env; @@ -811,250 +829,6 @@ TEST(SQLDisconnect, TestSQLDisconnectWithoutConnection) { EXPECT_EQ(ret, SQL_SUCCESS); } -TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { - // ODBC Environment - SQLHENV env; - SQLHDBC conn; - - // Allocate an environment handle - SQLRETURN ret = SQLAllocEnv(&env); - - EXPECT_EQ(ret, SQL_SUCCESS); - - ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Allocate a connection using alloc handle - ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Invalid connect string - std::string connect_str = this->getInvalidConnectionString(); - - ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, - arrow::util::UTF8ToWideString(connect_str)); - std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); - - SQLWCHAR outstr[ODBC_BUFFER_SIZE]; - SQLSMALLINT outstrlen; - - // Connecting to ODBC server. - ret = SQLDriverConnect(conn, NULL, &connect_str0[0], - static_cast(connect_str0.size()), outstr, - ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - - EXPECT_TRUE(ret == SQL_ERROR); - - // Retrieve all supported header level and record level data - SQLSMALLINT HEADER_LEVEL = 0; - SQLSMALLINT RECORD_1 = 1; - - // SQL_DIAG_NUMBER - SQLINTEGER diag_number; - SQLSMALLINT diag_number_length; - - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, HEADER_LEVEL, SQL_DIAG_NUMBER, &diag_number, - sizeof(SQLINTEGER), &diag_number_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - EXPECT_EQ(diag_number, 1); - - // SQL_DIAG_SERVER_NAME - SQLWCHAR server_name[ODBC_BUFFER_SIZE]; - SQLSMALLINT server_name_length; - - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SERVER_NAME, server_name, - ODBC_BUFFER_SIZE, &server_name_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // SQL_DIAG_MESSAGE_TEXT - SQLWCHAR message_text[ODBC_BUFFER_SIZE]; - SQLSMALLINT message_text_length; - - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, - message_text, ODBC_BUFFER_SIZE, &message_text_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - EXPECT_GT(message_text_length, 100); - - // SQL_DIAG_NATIVE - SQLINTEGER diag_native; - SQLSMALLINT diag_native_length; - - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_NATIVE, &diag_native, - sizeof(diag_native), &diag_native_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - EXPECT_EQ(diag_native, 200); - - // SQL_DIAG_SQLSTATE - const SQLSMALLINT sql_state_size = 6; - SQLWCHAR sql_state[sql_state_size]; - SQLSMALLINT sql_state_length; - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SQLSTATE, sql_state, - sql_state_size * driver::odbcabstraction::GetSqlWCharSize(), - &sql_state_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // 28000 - EXPECT_EQ(sql_state[0], '2'); - EXPECT_EQ(sql_state[1], '8'); - EXPECT_EQ(sql_state[2], '0'); - EXPECT_EQ(sql_state[3], '0'); - EXPECT_EQ(sql_state[4], '0'); - - // Free connection handle - ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Free environment handle - ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - - EXPECT_EQ(ret, SQL_SUCCESS); -} - -TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { - // Test is disabled because driver manager on Windows does not pass through SQL_NTS - // This test case can be potentially used on macOS/Linux - GTEST_SKIP(); - // ODBC Environment - SQLHENV env; - SQLHDBC conn; - - // Allocate an environment handle - SQLRETURN ret = SQLAllocEnv(&env); - - EXPECT_EQ(ret, SQL_SUCCESS); - - ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Allocate a connection using alloc handle - ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Invalid connect string - std::string connect_str = this->getInvalidConnectionString(); - - ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, - arrow::util::UTF8ToWideString(connect_str)); - std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); - - SQLWCHAR outstr[ODBC_BUFFER_SIZE]; - SQLSMALLINT outstrlen; - - // Connecting to ODBC server. - ret = SQLDriverConnect(conn, NULL, &connect_str0[0], - static_cast(connect_str0.size()), outstr, - ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - - EXPECT_TRUE(ret == SQL_ERROR); - - // Retrieve all supported header level and record level data - SQLSMALLINT RECORD_1 = 1; - - // SQL_DIAG_MESSAGE_TEXT SQL_NTS - SQLWCHAR message_text[ODBC_BUFFER_SIZE]; - SQLSMALLINT message_text_length; - - message_text[ODBC_BUFFER_SIZE - 1] = '\0'; - - ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, - message_text, SQL_NTS, &message_text_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - EXPECT_GT(message_text_length, 100); - - // Free connection handle - ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Free environment handle - ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - - EXPECT_EQ(ret, SQL_SUCCESS); -} - -TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { - // ODBC Environment - SQLHENV env; - SQLHDBC conn; - - // Allocate an environment handle - SQLRETURN ret = SQLAllocEnv(&env); - - EXPECT_EQ(ret, SQL_SUCCESS); - - ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Allocate a connection using alloc handle - ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Invalid connect string - std::string connect_str = this->getInvalidConnectionString(); - - ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, - arrow::util::UTF8ToWideString(connect_str)); - std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); - - SQLWCHAR outstr[ODBC_BUFFER_SIZE]; - SQLSMALLINT outstrlen; - - // Connecting to ODBC server. - ret = SQLDriverConnect(conn, NULL, &connect_str0[0], - static_cast(connect_str0.size()), outstr, - ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - - EXPECT_TRUE(ret == SQL_ERROR); - - SQLWCHAR sql_state[6]; - SQLINTEGER native_error; - SQLWCHAR message[ODBC_BUFFER_SIZE]; - SQLSMALLINT message_length; - - ret = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, sql_state, &native_error, message, - ODBC_BUFFER_SIZE, &message_length); - - EXPECT_EQ(ret, SQL_SUCCESS); - - EXPECT_GT(message_length, 120); - - EXPECT_EQ(native_error, 200); - - // 28000 - EXPECT_EQ(sql_state[0], '2'); - EXPECT_EQ(sql_state[1], '8'); - EXPECT_EQ(sql_state[2], '0'); - EXPECT_EQ(sql_state[3], '0'); - EXPECT_EQ(sql_state[4], '0'); - - // Free connection handle - ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); - - EXPECT_EQ(ret, SQL_SUCCESS); - - // Free environment handle - ret = SQLFreeHandle(SQL_HANDLE_ENV, env); - - EXPECT_EQ(ret, SQL_SUCCESS); -} - TYPED_TEST(FlightSQLODBCTestBase, TestConnect) { // Verifies connect and disconnect works on its own this->connect(); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc new file mode 100644 index 00000000000..ee0a2846194 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc @@ -0,0 +1,610 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + // Retrieve all supported header level and record level data + SQLSMALLINT HEADER_LEVEL = 0; + SQLSMALLINT RECORD_1 = 1; + + // SQL_DIAG_NUMBER + SQLINTEGER diag_number; + SQLSMALLINT diag_number_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, HEADER_LEVEL, SQL_DIAG_NUMBER, &diag_number, + sizeof(SQLINTEGER), &diag_number_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(diag_number, 1); + + // SQL_DIAG_SERVER_NAME + SQLWCHAR server_name[ODBC_BUFFER_SIZE]; + SQLSMALLINT server_name_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SERVER_NAME, server_name, + ODBC_BUFFER_SIZE, &server_name_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // SQL_DIAG_MESSAGE_TEXT + SQLWCHAR message_text[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_text_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, + message_text, ODBC_BUFFER_SIZE, &message_text_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_text_length, 100); + + // SQL_DIAG_NATIVE + SQLINTEGER diag_native; + SQLSMALLINT diag_native_length; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_NATIVE, &diag_native, + sizeof(diag_native), &diag_native_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(diag_native, 200); + + // SQL_DIAG_SQLSTATE + const SQLSMALLINT sql_state_size = 6; + SQLWCHAR sql_state[sql_state_size]; + SQLSMALLINT sql_state_length; + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_SQLSTATE, sql_state, + sql_state_size * driver::odbcabstraction::GetSqlWCharSize(), + &sql_state_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"28000")); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { + // Test is disabled because driver manager on Windows does not pass through SQL_NTS + // This test case can be potentially used on macOS/Linux + GTEST_SKIP(); + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + // Retrieve all supported header level and record level data + SQLSMALLINT RECORD_1 = 1; + + // SQL_DIAG_MESSAGE_TEXT SQL_NTS + SQLWCHAR message_text[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_text_length; + + message_text[ODBC_BUFFER_SIZE - 1] = '\0'; + + ret = SQLGetDiagField(SQL_HANDLE_DBC, conn, RECORD_1, SQL_DIAG_MESSAGE_TEXT, + message_text, SQL_NTS, &message_text_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_text_length, 100); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { + // ODBC Environment + SQLHENV env; + SQLHDBC conn; + + // Allocate an environment handle + SQLRETURN ret = SQLAllocEnv(&env); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Allocate a connection using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Invalid connect string + std::string connect_str = this->getInvalidConnectionString(); + + ASSERT_OK_AND_ASSIGN(std::wstring wconnect_str, + arrow::util::UTF8ToWideString(connect_str)); + std::vector connect_str0(wconnect_str.begin(), wconnect_str.end()); + + SQLWCHAR outstr[ODBC_BUFFER_SIZE]; + SQLSMALLINT outstrlen; + + // Connecting to ODBC server. + ret = SQLDriverConnect(conn, NULL, &connect_str0[0], + static_cast(connect_str0.size()), outstr, + ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); + + EXPECT_TRUE(ret == SQL_ERROR); + + SQLWCHAR sql_state[6]; + SQLINTEGER native_error; + SQLWCHAR message[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_length; + + ret = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, sql_state, &native_error, message, + ODBC_BUFFER_SIZE, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 120); + + EXPECT_EQ(native_error, 200); + + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"28000")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + // Free connection handle + ret = SQLFreeHandle(SQL_HANDLE_DBC, conn); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free environment handle + ret = SQLFreeHandle(SQL_HANDLE_ENV, env); + + EXPECT_EQ(ret, SQL_SUCCESS); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecInputData) { + // SQLGetDiagRec does not post diagnostic records for itself. + this->connect(); + + SQLWCHAR sql_state[6]; + SQLINTEGER native_error; + SQLWCHAR message[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_length; + + // Pass invalid record number + SQLRETURN ret = SQLGetDiagRec(SQL_HANDLE_DBC, this->conn, 0, sql_state, &native_error, + message, ODBC_BUFFER_SIZE, &message_length); + + EXPECT_EQ(ret, SQL_ERROR); + + // Pass valid record number with null inputs + ret = SQLGetDiagRec(SQL_HANDLE_DBC, this->conn, 1, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_NO_DATA); + + // Invalid handle + ret = SQLGetDiagRec(0, 0, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_INVALID_HANDLE); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorInputData) { + // Test ODBC 2.0 API SQLError. Driver manager maps SQLError to SQLGetDiagRec. + // SQLError does not post diagnostic records for itself. + this->connect(); + + // Pass valid handles with null inputs + SQLRETURN ret = SQLError(this->env, 0, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_NO_DATA); + + ret = SQLError(0, this->conn, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_NO_DATA); + + ret = SQLError(0, 0, this->stmt, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_NO_DATA); + + // Invalid handle + ret = SQLError(0, 0, 0, 0, 0, 0, 0, 0); + + EXPECT_EQ(ret, SQL_INVALID_HANDLE); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorEnvErrorFromDriverManager) { + // Test ODBC 2.0 API SQLError. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(); + + // Attempt to set environment attribute after connection handle allocation + SQLRETURN ret = SQLSetEnvAttr(this->env, SQL_ATTR_ODBC_VERSION, + reinterpret_cast(SQL_OV_ODBC2), 0); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(this->env, 0, 0, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 50); + + EXPECT_EQ(native_error, 0); + + // Function sequence error state from driver manager + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"HY010")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorConnError) { + // Test ODBC 2.0 API SQLError. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(); + + // Attempt to set unsupported attribute + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TXN_ISOLATION, 0, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, this->conn, 0, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 60); + + EXPECT_EQ(native_error, 100); + + // optional feature not supported error state + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"HYC00")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorStmtError) { + // Test ODBC 2.0 API SQLError. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(); + + std::wstring wsql = L"1"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, 0, this->stmt, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 70); + + EXPECT_EQ(native_error, 100); + + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"HY000")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorStmtWarning) { + // Test ODBC 2.0 API SQLError. + this->connect(); + + std::wstring wsql = L"SELECT 'VERY LONG STRING here' AS string_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + const int len = 17; + SQLCHAR char_val[len]; + SQLLEN buf_len = sizeof(SQLCHAR) * len; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, 0, this->stmt, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 50); + + EXPECT_EQ(native_error, 1000100); + + // Verify string truncation warning is reported + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"01004")); + + EXPECT_TRUE(!std::wstring(message).empty()); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorEnvErrorODBCVer2FromDriverManager) { + // Test ODBC 2.0 API SQLError with ODBC ver 2. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(SQL_OV_ODBC2); + + // Attempt to set environment attribute after connection handle allocation + SQLRETURN ret = SQLSetEnvAttr(this->env, SQL_ATTR_ODBC_VERSION, + reinterpret_cast(SQL_OV_ODBC2), 0); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(this->env, 0, 0, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 50); + + EXPECT_EQ(native_error, 0); + + // Function sequence error state from driver manager + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"S1010")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorConnErrorODBCVer2) { + // Test ODBC 2.0 API SQLError with ODBC ver 2. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(SQL_OV_ODBC2); + + // Attempt to set unsupported attribute + SQLRETURN ret = SQLGetConnectAttr(this->conn, SQL_ATTR_TXN_ISOLATION, 0, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, this->conn, 0, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 60); + + EXPECT_EQ(native_error, 100); + + // optional feature not supported error state. Driver Manager maps state to S1C00 + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"S1C00")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorStmtErrorODBCVer2) { + // Test ODBC 2.0 API SQLError with ODBC ver 2. + // Known Windows Driver Manager (DM) behavior: + // When application passes buffer length greater than SQL_MAX_MESSAGE_LENGTH (512), + // DM passes 512 as buffer length to SQLError. + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"1"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, 0, this->stmt, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 70); + + EXPECT_EQ(native_error, 100); + + // Driver Manager maps error state to S1000 + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"S1000")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLErrorStmtWarningODBCVer2) { + // Test ODBC 2.0 API SQLError. + this->connect(SQL_OV_ODBC2); + + std::wstring wsql = L"SELECT 'VERY LONG STRING here' AS string_col;"; + std::vector sql0(wsql.begin(), wsql.end()); + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + EXPECT_EQ(ret, SQL_SUCCESS); + + const int len = 17; + SQLCHAR char_val[len]; + SQLLEN buf_len = sizeof(SQLCHAR) * len; + SQLLEN ind; + + ret = SQLGetData(this->stmt, 1, SQL_C_CHAR, &char_val, buf_len, &ind); + + EXPECT_EQ(ret, SQL_SUCCESS_WITH_INFO); + + SQLWCHAR sql_state[6] = {0}; + SQLINTEGER native_error = 0; + SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; + SQLSMALLINT message_length = 0; + ret = SQLError(0, 0, this->stmt, sql_state, &native_error, message, + SQL_MAX_MESSAGE_LENGTH, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 50); + + EXPECT_EQ(native_error, 1000100); + + // Verify string truncation warning is reported + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"01004")); + + EXPECT_TRUE(!std::wstring(message).empty()); +} + +} // namespace arrow::flight::sql::odbc 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 960da101fd3..dcd342a62c6 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 @@ -66,13 +66,13 @@ class FlightSQLODBCRemoteTestBase : public ::testing::Test { std::wstring virtual getQueryAllDataTypes(); /** ODBC Environment. */ - SQLHENV env; + SQLHENV env = 0; /** ODBC Connect. */ - SQLHDBC conn; + SQLHDBC conn = 0; /** ODBC Statement. */ - SQLHSTMT stmt; + SQLHSTMT stmt = 0; protected: void SetUp() override; From cf747474991501e42d3c2432ecd5eb362cb3ca16 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:17:29 -0700 Subject: [PATCH 53/74] Add tests for SQLGetFunctions * Using `SQL_FUNC_EXISTS` macro fixed the issue of `api_exists` not read correctly * Fix SQLGetTypeInfo naming for ODBC ver 2 tests * Add more tests Add unsupported API checks. Add `TestSQLGetFunctionsODBCVer2`. Add `TestSQLGetFunctionsCheckSingleAPI`. * Update reset value * Add SQLDescribeCol function check --- .../flight/sql/odbc/tests/CMakeLists.txt | 1 + .../sql/odbc/tests/get_functions_test.cc | 240 ++++++++++++++++++ .../flight/sql/odbc/tests/type_info_test.cc | 14 +- 3 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/tests/get_functions_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/tests/CMakeLists.txt index 606b3cb7e4a..2dc719fa05e 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 connection_attr_test.cc connection_info_test.cc errors_test.cc + get_functions_test.cc statement_attr_test.cc statement_test.cc tables_test.cc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/get_functions_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/get_functions_test.cc new file mode 100644 index 00000000000..3eb54e49195 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/tests/get_functions_test.cc @@ -0,0 +1,240 @@ +// 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" + +#ifdef _WIN32 +# include +#endif + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace arrow::flight::sql::odbc { + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsAllFunctions) { + // Verify driver manager return values for SQLGetFunctions + this->connect(); + + SQLUSMALLINT api_exists[SQL_API_ODBC3_ALL_FUNCTIONS_SIZE]; + const std::vector supported_functions = { + SQL_API_SQLALLOCHANDLE, SQL_API_SQLBINDCOL, SQL_API_SQLGETDIAGFIELD, + SQL_API_SQLCANCEL, SQL_API_SQLCLOSECURSOR, SQL_API_SQLGETDIAGREC, + SQL_API_SQLCOLATTRIBUTE, SQL_API_SQLGETENVATTR, SQL_API_SQLCONNECT, + SQL_API_SQLGETINFO, SQL_API_SQLGETSTMTATTR, SQL_API_SQLDESCRIBECOL, + SQL_API_SQLGETTYPEINFO, SQL_API_SQLDISCONNECT, SQL_API_SQLNUMRESULTCOLS, + SQL_API_SQLPREPARE, SQL_API_SQLEXECDIRECT, SQL_API_SQLEXECUTE, SQL_API_SQLROWCOUNT, + SQL_API_SQLFETCH, SQL_API_SQLSETCONNECTATTR, SQL_API_SQLFETCHSCROLL, + SQL_API_SQLFREEHANDLE, SQL_API_SQLFREESTMT, SQL_API_SQLGETCONNECTATTR, + SQL_API_SQLSETENVATTR, SQL_API_SQLSETSTMTATTR, SQL_API_SQLGETDATA, + SQL_API_SQLCOLUMNS, SQL_API_SQLTABLES, SQL_API_SQLNATIVESQL, + SQL_API_SQLDRIVERCONNECT, SQL_API_SQLMORERESULTS, SQL_API_SQLPRIMARYKEYS, + SQL_API_SQLFOREIGNKEYS, + + // ODBC 2.0 APIs + SQL_API_SQLSETSTMTOPTION, SQL_API_SQLGETSTMTOPTION, SQL_API_SQLSETCONNECTOPTION, + SQL_API_SQLGETCONNECTOPTION, SQL_API_SQLALLOCCONNECT, SQL_API_SQLALLOCENV, + SQL_API_SQLALLOCSTMT, SQL_API_SQLFREEENV, SQL_API_SQLFREECONNECT, + + // Driver Manager APIs + SQL_API_SQLGETFUNCTIONS, SQL_API_SQLDRIVERS, SQL_API_SQLDATASOURCES}; + const std::vector unsupported_functions = { + SQL_API_SQLPUTDATA, SQL_API_SQLGETDESCFIELD, SQL_API_SQLGETDESCREC, + SQL_API_SQLCOPYDESC, SQL_API_SQLPARAMDATA, SQL_API_SQLENDTRAN, + SQL_API_SQLSETCURSORNAME, SQL_API_SQLSETDESCFIELD, SQL_API_SQLSETDESCREC, + SQL_API_SQLGETCURSORNAME, SQL_API_SQLSTATISTICS, SQL_API_SQLSPECIALCOLUMNS, + SQL_API_SQLBINDPARAMETER, SQL_API_SQLBROWSECONNECT, SQL_API_SQLNUMPARAMS, + SQL_API_SQLBULKOPERATIONS, SQL_API_SQLCOLUMNPRIVILEGES, SQL_API_SQLPROCEDURECOLUMNS, + SQL_API_SQLDESCRIBEPARAM, SQL_API_SQLPROCEDURES, SQL_API_SQLSETPOS, + SQL_API_SQLTABLEPRIVILEGES}; + SQLRETURN ret = SQLGetFunctions(this->conn, SQL_API_ODBC3_ALL_FUNCTIONS, api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (int api : supported_functions) { + EXPECT_EQ(SQL_FUNC_EXISTS(api_exists, api), SQL_TRUE); + } + + for (int api : unsupported_functions) { + EXPECT_EQ(SQL_FUNC_EXISTS(api_exists, api), SQL_FALSE); + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsAllFunctionsODBCVer2) { + // Verify driver manager return values for SQLGetFunctions + this->connect(SQL_OV_ODBC2); + + // ODBC 2.0 SQLGetFunctions returns 100 elements according to spec + SQLUSMALLINT api_exists[100]; + const std::vector supported_functions = { + SQL_API_SQLCONNECT, SQL_API_SQLGETINFO, SQL_API_SQLDESCRIBECOL, + SQL_API_SQLGETTYPEINFO, SQL_API_SQLDISCONNECT, SQL_API_SQLNUMRESULTCOLS, + SQL_API_SQLPREPARE, SQL_API_SQLEXECDIRECT, SQL_API_SQLEXECUTE, SQL_API_SQLROWCOUNT, + SQL_API_SQLFETCH, SQL_API_SQLFREESTMT, SQL_API_SQLGETDATA, SQL_API_SQLCOLUMNS, + SQL_API_SQLTABLES, SQL_API_SQLNATIVESQL, SQL_API_SQLDRIVERCONNECT, + SQL_API_SQLMORERESULTS, SQL_API_SQLSETSTMTOPTION, SQL_API_SQLGETSTMTOPTION, + SQL_API_SQLSETCONNECTOPTION, SQL_API_SQLGETCONNECTOPTION, SQL_API_SQLALLOCCONNECT, + SQL_API_SQLALLOCENV, SQL_API_SQLALLOCSTMT, SQL_API_SQLFREEENV, + SQL_API_SQLFREECONNECT, SQL_API_SQLPRIMARYKEYS, SQL_API_SQLFOREIGNKEYS, + + // Driver Manager APIs + SQL_API_SQLGETFUNCTIONS, SQL_API_SQLDRIVERS, SQL_API_SQLDATASOURCES}; + const std::vector unsupported_functions = { + SQL_API_SQLPUTDATA, SQL_API_SQLPARAMDATA, SQL_API_SQLSETCURSORNAME, + SQL_API_SQLGETCURSORNAME, SQL_API_SQLSTATISTICS, SQL_API_SQLSPECIALCOLUMNS, + SQL_API_SQLBINDPARAMETER, SQL_API_SQLBROWSECONNECT, SQL_API_SQLNUMPARAMS, + SQL_API_SQLBULKOPERATIONS, SQL_API_SQLCOLUMNPRIVILEGES, SQL_API_SQLPROCEDURECOLUMNS, + SQL_API_SQLDESCRIBEPARAM, SQL_API_SQLPROCEDURES, SQL_API_SQLSETPOS, + SQL_API_SQLTABLEPRIVILEGES}; + SQLRETURN ret = SQLGetFunctions(this->conn, SQL_API_ALL_FUNCTIONS, api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (int api : supported_functions) { + EXPECT_EQ(api_exists[api], SQL_TRUE); + } + + for (int api : unsupported_functions) { + EXPECT_EQ(api_exists[api], SQL_FALSE); + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsSupportedSingleAPI) { + this->connect(); + + const std::vector supported_functions = { + SQL_API_SQLALLOCHANDLE, SQL_API_SQLBINDCOL, SQL_API_SQLGETDIAGFIELD, + SQL_API_SQLCANCEL, SQL_API_SQLCLOSECURSOR, SQL_API_SQLGETDIAGREC, + SQL_API_SQLCOLATTRIBUTE, SQL_API_SQLGETENVATTR, SQL_API_SQLCONNECT, + SQL_API_SQLGETINFO, SQL_API_SQLGETSTMTATTR, SQL_API_SQLDESCRIBECOL, + SQL_API_SQLGETTYPEINFO, SQL_API_SQLDISCONNECT, SQL_API_SQLNUMRESULTCOLS, + SQL_API_SQLPREPARE, SQL_API_SQLEXECDIRECT, SQL_API_SQLEXECUTE, SQL_API_SQLROWCOUNT, + SQL_API_SQLFETCH, SQL_API_SQLSETCONNECTATTR, SQL_API_SQLFETCHSCROLL, + SQL_API_SQLFREEHANDLE, SQL_API_SQLFREESTMT, SQL_API_SQLGETCONNECTATTR, + SQL_API_SQLSETENVATTR, SQL_API_SQLSETSTMTATTR, SQL_API_SQLGETDATA, + SQL_API_SQLCOLUMNS, SQL_API_SQLTABLES, SQL_API_SQLNATIVESQL, + SQL_API_SQLDRIVERCONNECT, SQL_API_SQLMORERESULTS, SQL_API_SQLPRIMARYKEYS, + SQL_API_SQLFOREIGNKEYS, + + // ODBC 2.0 APIs + SQL_API_SQLSETSTMTOPTION, SQL_API_SQLGETSTMTOPTION, SQL_API_SQLSETCONNECTOPTION, + SQL_API_SQLGETCONNECTOPTION, SQL_API_SQLALLOCCONNECT, SQL_API_SQLALLOCENV, + SQL_API_SQLALLOCSTMT, SQL_API_SQLFREEENV, SQL_API_SQLFREECONNECT, + + // Driver Manager APIs + SQL_API_SQLGETFUNCTIONS, SQL_API_SQLDRIVERS, SQL_API_SQLDATASOURCES}; + SQLUSMALLINT api_exists; + for (SQLUSMALLINT api : supported_functions) { + SQLRETURN ret = SQLGetFunctions(this->conn, api, &api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(api_exists, SQL_TRUE); + + api_exists = -1; + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsUnsupportedSingleAPI) { + this->connect(); + + const std::vector unsupported_functions = { + SQL_API_SQLPUTDATA, SQL_API_SQLGETDESCFIELD, SQL_API_SQLGETDESCREC, + SQL_API_SQLCOPYDESC, SQL_API_SQLPARAMDATA, SQL_API_SQLENDTRAN, + SQL_API_SQLSETCURSORNAME, SQL_API_SQLSETDESCFIELD, SQL_API_SQLSETDESCREC, + SQL_API_SQLGETCURSORNAME, SQL_API_SQLSTATISTICS, SQL_API_SQLSPECIALCOLUMNS, + SQL_API_SQLBINDPARAMETER, SQL_API_SQLBROWSECONNECT, SQL_API_SQLNUMPARAMS, + SQL_API_SQLBULKOPERATIONS, SQL_API_SQLCOLUMNPRIVILEGES, SQL_API_SQLPROCEDURECOLUMNS, + SQL_API_SQLDESCRIBEPARAM, SQL_API_SQLPROCEDURES, SQL_API_SQLSETPOS, + SQL_API_SQLTABLEPRIVILEGES}; + SQLUSMALLINT api_exists; + for (SQLUSMALLINT api : unsupported_functions) { + SQLRETURN ret = SQLGetFunctions(this->conn, api, &api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(api_exists, SQL_FALSE); + + api_exists = -1; + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsSupportedSingleAPIODBCVer2) { + this->connect(SQL_OV_ODBC2); + + const std::vector supported_functions = { + SQL_API_SQLCONNECT, SQL_API_SQLGETINFO, SQL_API_SQLDESCRIBECOL, + SQL_API_SQLGETTYPEINFO, SQL_API_SQLDISCONNECT, SQL_API_SQLNUMRESULTCOLS, + SQL_API_SQLPREPARE, SQL_API_SQLEXECDIRECT, SQL_API_SQLEXECUTE, SQL_API_SQLROWCOUNT, + SQL_API_SQLFETCH, SQL_API_SQLFREESTMT, SQL_API_SQLGETDATA, SQL_API_SQLCOLUMNS, + SQL_API_SQLTABLES, SQL_API_SQLNATIVESQL, SQL_API_SQLDRIVERCONNECT, + SQL_API_SQLMORERESULTS, SQL_API_SQLSETSTMTOPTION, SQL_API_SQLGETSTMTOPTION, + SQL_API_SQLSETCONNECTOPTION, SQL_API_SQLGETCONNECTOPTION, SQL_API_SQLALLOCCONNECT, + SQL_API_SQLALLOCENV, SQL_API_SQLALLOCSTMT, SQL_API_SQLFREEENV, + SQL_API_SQLFREECONNECT, SQL_API_SQLPRIMARYKEYS, SQL_API_SQLFOREIGNKEYS, + + // Driver Manager APIs + SQL_API_SQLGETFUNCTIONS, SQL_API_SQLDRIVERS, SQL_API_SQLDATASOURCES}; + SQLUSMALLINT api_exists; + for (SQLUSMALLINT api : supported_functions) { + SQLRETURN ret = SQLGetFunctions(this->conn, api, &api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(api_exists, SQL_TRUE); + + api_exists = -1; + } + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetFunctionsUnsupportedSingleAPIODBCVer2) { + this->connect(SQL_OV_ODBC2); + + const std::vector unsupported_functions = { + SQL_API_SQLPUTDATA, SQL_API_SQLPARAMDATA, SQL_API_SQLSETCURSORNAME, + SQL_API_SQLGETCURSORNAME, SQL_API_SQLSTATISTICS, SQL_API_SQLSPECIALCOLUMNS, + SQL_API_SQLBINDPARAMETER, SQL_API_SQLBROWSECONNECT, SQL_API_SQLNUMPARAMS, + SQL_API_SQLBULKOPERATIONS, SQL_API_SQLCOLUMNPRIVILEGES, SQL_API_SQLPROCEDURECOLUMNS, + SQL_API_SQLDESCRIBEPARAM, SQL_API_SQLPROCEDURES, SQL_API_SQLSETPOS, + SQL_API_SQLTABLEPRIVILEGES}; + SQLUSMALLINT api_exists; + for (SQLUSMALLINT api : unsupported_functions) { + SQLRETURN ret = SQLGetFunctions(this->conn, api, &api_exists); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(api_exists, SQL_FALSE); + + api_exists = -1; + } + + this->disconnect(); +} + +} // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc index e642575210b..916befb774d 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc @@ -521,7 +521,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { this->connect(SQL_OV_ODBC2); SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_ALL_TYPES); @@ -1546,7 +1546,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLDate) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDateVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDateODBCVer2) { this->connect(SQL_OV_ODBC2); SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_DATE); @@ -1584,7 +1584,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDateVer2) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeDateVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeDateODBCVer2) { this->connect(SQL_OV_ODBC2); // Pass ODBC Ver 3 data type @@ -1675,7 +1675,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTime) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTimeVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTimeODBCVer2) { this->connect(SQL_OV_ODBC2); SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIME); @@ -1713,7 +1713,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTimeVer2) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimeVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimeODBCVer2) { this->connect(SQL_OV_ODBC2); // Pass ODBC Ver 3 data type @@ -1804,7 +1804,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestamp) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestampVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestampODBCVer2) { this->connect(SQL_OV_ODBC2); SQLRETURN ret = SQLGetTypeInfo(this->stmt, SQL_TIMESTAMP); @@ -1842,7 +1842,7 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestampVer2) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimestampVer2) { +TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimestampODBCVer2) { this->connect(SQL_OV_ODBC2); // Pass ODBC Ver 3 data type From 9a375641fb862b7047709f4bd16304f2e5fbc49b Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 12 Aug 2025 11:08:17 -0700 Subject: [PATCH 54/74] Add additional SQLDescribeCol test cases --- .../flight/sql/odbc/tests/columns_test.cc | 52 +++- .../flight/sql/odbc/tests/type_info_test.cc | 222 +++++++++++++++++- 2 files changed, 259 insertions(+), 15 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 04ca7ec9b96..c48339a1103 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2781,12 +2781,22 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeCol) { SQLSMALLINT nullable = 0; size_t columnIndex = 0; - SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", - (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", - (SQLWCHAR*)L"DATA_TYPE"}; - SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, - SQL_SMALLINT}; - SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE", (SQLWCHAR*)L"TYPE_NAME", + (SQLWCHAR*)L"COLUMN_SIZE", (SQLWCHAR*)L"BUFFER_LENGTH", + (SQLWCHAR*)L"DECIMAL_DIGITS", (SQLWCHAR*)L"NUM_PREC_RADIX", + (SQLWCHAR*)L"NULLABLE", (SQLWCHAR*)L"REMARKS", + (SQLWCHAR*)L"COLUMN_DEF", (SQLWCHAR*)L"SQL_DATA_TYPE", + (SQLWCHAR*)L"SQL_DATETIME_SUB", (SQLWCHAR*)L"CHAR_OCTET_LENGTH", + (SQLWCHAR*)L"ORDINAL_POSITION", (SQLWCHAR*)L"IS_NULLABLE"}; + SQLSMALLINT columnDataTypes[] = { + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_SMALLINT, SQL_WVARCHAR, + SQL_INTEGER, SQL_INTEGER, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, SQL_INTEGER, SQL_INTEGER, SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2, 1024, 4, 4, 2, + 2, 2, 1024, 1024, 2, 2, 4, 4, 1024}; SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); @@ -2833,12 +2843,30 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeColODBC2) { SQLSMALLINT nullable = 0; size_t columnIndex = 0; - SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", (SQLWCHAR*)L"TABLE_OWNER", - (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", - (SQLWCHAR*)L"DATA_TYPE"}; - SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, - SQL_SMALLINT}; - SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", + (SQLWCHAR*)L"TABLE_OWNER", + (SQLWCHAR*)L"TABLE_NAME", + (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE", + (SQLWCHAR*)L"TYPE_NAME", + (SQLWCHAR*)L"PRECISION", + (SQLWCHAR*)L"LENGTH", + (SQLWCHAR*)L"SCALE", + (SQLWCHAR*)L"RADIX", + (SQLWCHAR*)L"NULLABLE", + (SQLWCHAR*)L"REMARKS", + (SQLWCHAR*)L"COLUMN_DEF", + (SQLWCHAR*)L"SQL_DATA_TYPE", + (SQLWCHAR*)L"SQL_DATETIME_SUB", + (SQLWCHAR*)L"CHAR_OCTET_LENGTH", + (SQLWCHAR*)L"ORDINAL_POSITION", + (SQLWCHAR*)L"IS_NULLABLE"}; + SQLSMALLINT columnDataTypes[] = { + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_SMALLINT, SQL_WVARCHAR, + SQL_INTEGER, SQL_INTEGER, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, SQL_INTEGER, SQL_INTEGER, SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2, 1024, 4, 4, 2, + 2, 2, 1024, 1024, 2, 2, 4, 4, 1024}; SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc index 916befb774d..e92e8ff3cc3 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc @@ -26,13 +26,117 @@ #include "gtest/gtest.h" -// TODO: add tests with SQLDescribeCol to check metadata of SQLGetTypeInfo for ODBC 2 and -// ODBC 3. - namespace arrow::flight::sql::odbc { using std::optional; +void checkSQLDescribeCol(SQLHSTMT stmt, const SQLUSMALLINT columnIndex, + const std::wstring& expectedName, + const SQLSMALLINT& expectedDataType, + const SQLULEN& expectedColumnSize, + const SQLSMALLINT& expectedDecimalDigits, + const SQLSMALLINT& expectedNullable) { + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + + SQLRETURN ret = SQLDescribeCol(stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, expectedName); + EXPECT_EQ(columnDataType, expectedDataType); + EXPECT_EQ(columnSize, expectedColumnSize); + EXPECT_EQ(decimalDigits, expectedDecimalDigits); + EXPECT_EQ(nullable, expectedNullable); +} + +void checkSQLDescribeColODBC2(SQLHSTMT stmt) { + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TYPE_NAME", + (SQLWCHAR*)L"DATA_TYPE", + (SQLWCHAR*)L"PRECISION", + (SQLWCHAR*)L"LITERAL_PREFIX", + (SQLWCHAR*)L"LITERAL_SUFFIX", + (SQLWCHAR*)L"CREATE_PARAMS", + (SQLWCHAR*)L"NULLABLE", + (SQLWCHAR*)L"CASE_SENSITIVE", + (SQLWCHAR*)L"SEARCHABLE", + (SQLWCHAR*)L"UNSIGNED_ATTRIBUTE", + (SQLWCHAR*)L"MONEY", + (SQLWCHAR*)L"AUTO_INCREMENT", + (SQLWCHAR*)L"LOCAL_TYPE_NAME", + (SQLWCHAR*)L"MINIMUM_SCALE", + (SQLWCHAR*)L"MAXIMUM_SCALE", + (SQLWCHAR*)L"SQL_DATA_TYPE", + (SQLWCHAR*)L"SQL_DATETIME_SUB", + (SQLWCHAR*)L"NUM_PREC_RADIX", + (SQLWCHAR*)L"INTERVAL_PRECISION"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_SMALLINT, SQL_INTEGER, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, + SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, + SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, + SQL_SMALLINT, SQL_INTEGER, SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 2, 4, 1024, 1024, 1024, 2, 2, 2, 2, + 2, 2, 1024, 2, 2, 2, 2, 4, 2}; + SQLSMALLINT columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + SQLSMALLINT columnNullable[] = {SQL_NO_NULLS, SQL_NO_NULLS, SQL_NULLABLE, SQL_NULLABLE, + SQL_NULLABLE, SQL_NULLABLE, SQL_NO_NULLS, SQL_NO_NULLS, + SQL_NO_NULLS, SQL_NULLABLE, SQL_NO_NULLS, SQL_NULLABLE, + SQL_NULLABLE, SQL_NULLABLE, SQL_NULLABLE, SQL_NO_NULLS, + SQL_NULLABLE, SQL_NULLABLE, SQL_NULLABLE}; + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + SQLUSMALLINT columnIndex = i + 1; + checkSQLDescribeCol(stmt, columnIndex, columnNames[i], columnDataTypes[i], + columnSizes[i], columnDecimalDigits[i], columnNullable[i]); + } +} + +void checkSQLDescribeColODBC3(SQLHSTMT stmt) { + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"TYPE_NAME", (SQLWCHAR*)L"DATA_TYPE", + (SQLWCHAR*)L"COLUMN_SIZE", (SQLWCHAR*)L"LITERAL_PREFIX", + (SQLWCHAR*)L"LITERAL_SUFFIX", (SQLWCHAR*)L"CREATE_PARAMS", + (SQLWCHAR*)L"NULLABLE", (SQLWCHAR*)L"CASE_SENSITIVE", + (SQLWCHAR*)L"SEARCHABLE", (SQLWCHAR*)L"UNSIGNED_ATTRIBUTE", + (SQLWCHAR*)L"FIXED_PREC_SCALE", (SQLWCHAR*)L"AUTO_UNIQUE_VALUE", + (SQLWCHAR*)L"LOCAL_TYPE_NAME", (SQLWCHAR*)L"MINIMUM_SCALE", + (SQLWCHAR*)L"MAXIMUM_SCALE", (SQLWCHAR*)L"SQL_DATA_TYPE", + (SQLWCHAR*)L"SQL_DATETIME_SUB", (SQLWCHAR*)L"NUM_PREC_RADIX", + (SQLWCHAR*)L"INTERVAL_PRECISION"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_SMALLINT, SQL_INTEGER, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, + SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, + SQL_WVARCHAR, SQL_SMALLINT, SQL_SMALLINT, SQL_SMALLINT, + SQL_SMALLINT, SQL_INTEGER, SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 2, 4, 1024, 1024, 1024, 2, 2, 2, 2, + 2, 2, 1024, 2, 2, 2, 2, 4, 2}; + SQLSMALLINT columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + SQLSMALLINT columnNullable[] = {SQL_NO_NULLS, SQL_NO_NULLS, SQL_NULLABLE, SQL_NULLABLE, + SQL_NULLABLE, SQL_NULLABLE, SQL_NO_NULLS, SQL_NO_NULLS, + SQL_NO_NULLS, SQL_NULLABLE, SQL_NO_NULLS, SQL_NULLABLE, + SQL_NULLABLE, SQL_NULLABLE, SQL_NULLABLE, SQL_NO_NULLS, + SQL_NULLABLE, SQL_NULLABLE, SQL_NULLABLE}; + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + SQLUSMALLINT columnIndex = i + 1; + checkSQLDescribeCol(stmt, columnIndex, columnNames[i], columnDataTypes[i], + columnSizes[i], columnDecimalDigits[i], columnNullable[i]); + } +} + void checkSQLGetTypeInfo( SQLHSTMT stmt, const std::wstring& expectedTypeName, const SQLSMALLINT& expectedDataType, const SQLINTEGER& expectedColumnSize, @@ -114,6 +218,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check tinyint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -139,6 +245,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check bigint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -164,6 +272,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check longvarbinary data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -189,6 +299,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check varbinary data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -214,6 +326,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check text data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -240,6 +354,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check longvarchar data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -265,6 +381,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check char data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -291,6 +409,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check integer data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -316,6 +436,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check smallint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -341,6 +463,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check float data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -366,6 +490,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check double data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -391,6 +517,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check numeric data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -417,6 +545,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check varchar data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -443,6 +573,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check date data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -468,6 +600,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check time data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -493,6 +627,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check timestamp data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -518,6 +654,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypes) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + this->disconnect(); } @@ -552,6 +690,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check tinyint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -577,6 +717,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check bigint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -602,6 +744,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check longvarbinary data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -627,6 +771,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check varbinary data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -652,6 +798,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check text data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -678,6 +826,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check longvarchar data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -703,6 +853,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check char data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -729,6 +881,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check integer data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -754,6 +908,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check smallint data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -779,6 +935,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check float data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -804,6 +962,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check double data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -829,6 +989,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check numeric data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -855,6 +1017,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check varchar data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -881,6 +1045,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check date data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -906,6 +1072,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check time data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -931,6 +1099,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // Check timestamp data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -956,6 +1126,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoAllTypesODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + this->disconnect(); } @@ -990,6 +1162,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoBit) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1028,6 +1202,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTinyInt) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1066,6 +1242,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoBigInt) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1104,6 +1282,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoLongVarbinary) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1181,6 +1361,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoLongVarchar) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check longvarchar data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -1206,6 +1388,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoLongVarchar) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1245,6 +1429,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoChar) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1283,6 +1469,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoInteger) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1321,6 +1509,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSmallInt) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1359,6 +1549,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoFloat) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1397,6 +1589,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDouble) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // Check numeric data type ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_SUCCESS); @@ -1423,6 +1617,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDouble) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1462,6 +1658,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoVarchar) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1500,6 +1698,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeDate) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1539,6 +1739,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLDate) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1577,6 +1779,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoDateODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1629,6 +1833,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTime) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1668,6 +1874,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTime) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1706,6 +1914,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoTimeODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1758,6 +1968,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTypeTimestamp) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1797,6 +2009,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestamp) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC3(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); @@ -1835,6 +2049,8 @@ TEST_F(FlightSQLODBCMockTestBase, TestSQLGetTypeInfoSQLTimestampODBCVer2) { NULL, // expectedNumPrecRadix NULL); // expectedIntervalPrec + checkSQLDescribeColODBC2(this->stmt); + // No more data ret = SQLFetch(this->stmt); EXPECT_EQ(ret, SQL_NO_DATA); From 625e53435dc0095feeda755bad376d00f7a4164f Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:43:44 -0700 Subject: [PATCH 55/74] Add Descriptor support in SQLAllocHandle and SQLFreeHandle * Descriptor allocation initial impl * Add descriptor handle tests * Add diagnostic error test for descriptor handle * the error is from driver manager as our driver doesn't have descriptor-specific APIs implemented yet * Add SQLGetDescField test from driver manager * Add doxygen doc comment --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 56 ++++++-- .../odbc_impl/odbc_statement.h | 5 + .../flight/sql/odbc/tests/connection_test.cc | 99 ++++++++++++++ .../flight/sql/odbc/tests/errors_test.cc | 127 +++++++++++++++++- 4 files changed, 275 insertions(+), 12 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index e19553160f0..75924f596c4 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -107,9 +107,26 @@ SQLRETURN SQLAllocHandle(SQLSMALLINT type, SQLHANDLE parent, SQLHANDLE* result) }); } - // TODO Implement for case of descriptor - case SQL_HANDLE_DESC: - return SQL_INVALID_HANDLE; + case SQL_HANDLE_DESC: { + using ODBC::ODBCConnection; + using ODBC::ODBCDescriptor; + + *result = SQL_NULL_HDESC; + + ODBCConnection* connection = reinterpret_cast(parent); + + return ODBCConnection::ExecuteWithDiagnostics(connection, SQL_ERROR, [=]() { + std::shared_ptr descriptor = connection->createDescriptor(); + + if (descriptor) { + *result = reinterpret_cast(descriptor.get()); + + return SQL_SUCCESS; + } + + return SQL_ERROR; + }); + } default: break; @@ -164,8 +181,19 @@ SQLRETURN SQLFreeHandle(SQLSMALLINT type, SQLHANDLE handle) { return SQL_SUCCESS; } - case SQL_HANDLE_DESC: - return SQL_INVALID_HANDLE; + case SQL_HANDLE_DESC: { + using ODBC::ODBCDescriptor; + + ODBCDescriptor* descriptor = reinterpret_cast(handle); + + if (!descriptor) { + return SQL_INVALID_HANDLE; + } + + descriptor->ReleaseDescriptor(); + + return SQL_SUCCESS; + } default: break; @@ -242,6 +270,7 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT using driver::odbcabstraction::Diagnostics; using ODBC::GetStringAttribute; using ODBC::ODBCConnection; + using ODBC::ODBCDescriptor; using ODBC::ODBCEnvironment; using ODBC::ODBCStatement; @@ -277,7 +306,9 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT } case SQL_HANDLE_DESC: { - return SQL_ERROR; + ODBCDescriptor* descriptor = reinterpret_cast(handle); + diagnostics = &descriptor->GetDiagnostics(); + break; } case SQL_HANDLE_STMT: { @@ -405,8 +436,12 @@ SQLRETURN SQLGetDiagField(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT } case SQL_HANDLE_DESC: { - // TODO Implement for case of descriptor - return SQL_ERROR; + ODBCDescriptor* descriptor = reinterpret_cast(handle); + ODBCConnection* connection = &descriptor->GetConnection(); + std::string dsn = connection->GetDSN(); + return GetStringAttribute(isUnicode, dsn, true, diagInfoPtr, bufferLength, + stringLengthPtr, *diagnostics); + break; } case SQL_HANDLE_STMT: { @@ -495,6 +530,7 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re using driver::odbcabstraction::Diagnostics; using ODBC::GetStringAttribute; using ODBC::ODBCConnection; + using ODBC::ODBCDescriptor; using ODBC::ODBCEnvironment; using ODBC::ODBCStatement; @@ -525,7 +561,9 @@ SQLRETURN SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT re } case SQL_HANDLE_DESC: { - return SQL_ERROR; + auto* descriptor = ODBCDescriptor::of(handle); + diagnostics = &descriptor->GetDiagnostics(); + break; } case SQL_HANDLE_STMT: { diff --git a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h index 73cdc2448f8..7fb8d5c5741 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h +++ b/cpp/src/arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/odbc_impl/odbc_statement.h @@ -77,6 +77,11 @@ class ODBCStatement : public ODBCHandle { void SetStmtAttr(SQLINTEGER statementAttribute, SQLPOINTER value, SQLINTEGER bufferSize, bool isUnicode); + /** + * @brief Revert back to implicitly allocated internal descriptors. + * isApd as True indicates APD descritor is to be reverted. + * isApd as False indicates ARD descritor is to be reverted. + */ void RevertAppDescriptor(bool isApd); inline ODBCDescriptor* GetIRD() { return m_ird.get(); } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc index c935646de5b..5bf737d5518 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_test.cc @@ -919,6 +919,105 @@ TYPED_TEST(FlightSQLODBCTestBase, TestCloseConnectionWithOpenStatement) { EXPECT_EQ(ret, SQL_SUCCESS); } +TYPED_TEST(FlightSQLODBCTestBase, TestSQLAllocFreeDesc) { + this->connect(); + SQLHDESC descriptor; + + // Allocate a descriptor using alloc handle + SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DESC, this->conn, &descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Free descriptor handle + ret = SQLFreeHandle(SQL_HANDLE_DESC, descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, TestSQLSetStmtAttrDescriptor) { + this->connect(); + + SQLHDESC apd_descriptor, ard_descriptor; + + // Allocate an APD descriptor using alloc handle + SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DESC, this->conn, &apd_descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Allocate an ARD descriptor using alloc handle + ret = SQLAllocHandle(SQL_HANDLE_DESC, this->conn, &ard_descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Save implicitly allocated internal APD and ARD descriptor pointers + SQLPOINTER internal_apd, internal_ard = nullptr; + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, &internal_apd, + sizeof(internal_apd), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, &internal_ard, + sizeof(internal_ard), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Set APD descriptor to explicitly allocated handle + ret = SQLSetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, + reinterpret_cast(apd_descriptor), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Set ARD descriptor to explicitly allocated handle + ret = SQLSetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, + reinterpret_cast(ard_descriptor), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify APD and ARD descriptors are set to explicitly allocated pointers + SQLPOINTER value = nullptr; + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, &value, sizeof(value), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, apd_descriptor); + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, &value, sizeof(value), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, ard_descriptor); + + // Free explicitly allocated APD and ARD descriptor handles + ret = SQLFreeHandle(SQL_HANDLE_DESC, apd_descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFreeHandle(SQL_HANDLE_DESC, ard_descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Verify APD and ARD descriptors has been reverted to implicit descriptors + value = nullptr; + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_PARAM_DESC, &value, sizeof(value), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, internal_apd); + + ret = SQLGetStmtAttr(this->stmt, SQL_ATTR_APP_ROW_DESC, &value, sizeof(value), 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(value, internal_ard); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc int main(int argc, char** argv) { diff --git a/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc index ee0a2846194..276c16a113f 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/errors_test.cc @@ -62,7 +62,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailure) { static_cast(connect_str0.size()), outstr, ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - EXPECT_TRUE(ret == SQL_ERROR); + EXPECT_EQ(ret, SQL_ERROR); // Retrieve all supported header level and record level data SQLSMALLINT HEADER_LEVEL = 0; @@ -170,7 +170,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { static_cast(connect_str0.size()), outstr, ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - EXPECT_TRUE(ret == SQL_ERROR); + EXPECT_EQ(ret, SQL_ERROR); // Retrieve all supported header level and record level data SQLSMALLINT RECORD_1 = 1; @@ -199,6 +199,127 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagFieldWForConnectFailureNTS) { EXPECT_EQ(ret, SQL_SUCCESS); } +TYPED_TEST(FlightSQLODBCTestBase, + TestSQLGetDiagFieldWForDescriptorFailureFromDriverManager) { + this->connect(); + SQLHDESC descriptor; + + // Allocate a descriptor using alloc handle + SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DESC, this->conn, &descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLGetDescField(descriptor, 1, SQL_DESC_DATETIME_INTERVAL_CODE, 0, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + + // Retrieve all supported header level and record level data + SQLSMALLINT HEADER_LEVEL = 0; + SQLSMALLINT RECORD_1 = 1; + + // SQL_DIAG_NUMBER + SQLINTEGER diag_number; + SQLSMALLINT diag_number_length; + + ret = SQLGetDiagField(SQL_HANDLE_DESC, descriptor, HEADER_LEVEL, SQL_DIAG_NUMBER, + &diag_number, sizeof(SQLINTEGER), &diag_number_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(diag_number, 1); + + // SQL_DIAG_SERVER_NAME + SQLWCHAR server_name[ODBC_BUFFER_SIZE]; + SQLSMALLINT server_name_length; + + ret = SQLGetDiagField(SQL_HANDLE_DESC, descriptor, RECORD_1, SQL_DIAG_SERVER_NAME, + server_name, ODBC_BUFFER_SIZE, &server_name_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // SQL_DIAG_MESSAGE_TEXT + SQLWCHAR message_text[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_text_length; + + ret = SQLGetDiagField(SQL_HANDLE_DESC, descriptor, RECORD_1, SQL_DIAG_MESSAGE_TEXT, + message_text, ODBC_BUFFER_SIZE, &message_text_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_text_length, 100); + + // SQL_DIAG_NATIVE + SQLINTEGER diag_native; + SQLSMALLINT diag_native_length; + + ret = SQLGetDiagField(SQL_HANDLE_DESC, descriptor, RECORD_1, SQL_DIAG_NATIVE, + &diag_native, sizeof(diag_native), &diag_native_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(diag_native, 0); + + // SQL_DIAG_SQLSTATE + const SQLSMALLINT sql_state_size = 6; + SQLWCHAR sql_state[sql_state_size]; + SQLSMALLINT sql_state_length; + ret = SQLGetDiagField( + SQL_HANDLE_DESC, descriptor, RECORD_1, SQL_DIAG_SQLSTATE, sql_state, + sql_state_size * driver::odbcabstraction::GetSqlWCharSize(), &sql_state_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"IM001")); + + // Free descriptor handle + ret = SQLFreeHandle(SQL_HANDLE_DESC, descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + +TYPED_TEST(FlightSQLODBCTestBase, + TestSQLGetDiagRecForDescriptorFailureFromDriverManager) { + this->connect(); + SQLHDESC descriptor; + + // Allocate a descriptor using alloc handle + SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DESC, this->conn, &descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLGetDescField(descriptor, 1, SQL_DESC_DATETIME_INTERVAL_CODE, 0, 0, 0); + + EXPECT_EQ(ret, SQL_ERROR); + + SQLWCHAR sql_state[6]; + SQLINTEGER native_error; + SQLWCHAR message[ODBC_BUFFER_SIZE]; + SQLSMALLINT message_length; + + ret = SQLGetDiagRec(SQL_HANDLE_DESC, descriptor, 1, sql_state, &native_error, message, + ODBC_BUFFER_SIZE, &message_length); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(message_length, 60); + + EXPECT_EQ(native_error, 0); + + // API not implemented error from driver manager + EXPECT_EQ(std::wstring(sql_state), std::wstring(L"IM001")); + + EXPECT_TRUE(!std::wstring(message).empty()); + + // Free descriptor handle + ret = SQLFreeHandle(SQL_HANDLE_DESC, descriptor); + + EXPECT_EQ(ret, SQL_SUCCESS); + + this->disconnect(); +} + TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { // ODBC Environment SQLHENV env; @@ -233,7 +354,7 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLGetDiagRecForConnectFailure) { static_cast(connect_str0.size()), outstr, ODBC_BUFFER_SIZE, &outstrlen, SQL_DRIVER_NOPROMPT); - EXPECT_TRUE(ret == SQL_ERROR); + EXPECT_EQ(ret, SQL_ERROR); SQLWCHAR sql_state[6]; SQLINTEGER native_error; From ed6163cdeeed1560a6654009c042607478baf274 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 13 Aug 2025 08:57:26 -0700 Subject: [PATCH 56/74] Fix connection issues servers that require data in handshake * revert back to passing empty string in handshake --- .../flight/sql/odbc/flight_sql/flight_sql_auth_method.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc index b2d57e5df85..1b36f4916fe 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_auth_method.cc @@ -58,9 +58,9 @@ class NoOpClientAuthHandler : public arrow::flight::ClientAuthHandler { arrow::Status Authenticate(arrow::flight::ClientAuthSender* outgoing, arrow::flight::ClientAuthReader* incoming) override { - // Return OK Status. The server should ignore this and just accept any Handshake - // request. - return arrow::Status::OK(); + // The server should ignore this and just accept any Handshake + // request. Some servers do not allow authentication with no handshakes. + return outgoing->Write(std::string()); } arrow::Status GetToken(std::string* token) override { From 74db48bb978f3bfa48bf43eb1af3a787276bfbff Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 13 Aug 2025 08:57:44 -0700 Subject: [PATCH 57/74] Fix bug of system trust store not loaded properly * Bug: system trust store has default value of "true" regardless of "encryption" value. * But if encryption is set to false, system trust store cannot be used, so system trust store should be saved as false if user does not enable encryption. * It may confuse the user if they see encryption is set to false but system trust store is set to true. In this case, the driver will not do system trust store verification. The DSN window should reflect this accurately. --- .../sql/odbc/flight_sql/ui/dsn_configuration_window.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc index c6a13271435..a3a6c30ff51 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/ui/dsn_configuration_window.cc @@ -246,6 +246,7 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in std::string val = config.Get(FlightSqlConnection::USE_ENCRYPTION); + // Enable encryption default value is true const bool enableEncryption = driver::odbcabstraction::AsBool(val).value_or(true); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, ROW_HEIGHT, L"Use Encryption:", ChildId::ENABLE_ENCRYPTION_LABEL)); @@ -270,6 +271,7 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in val = config.Get(FlightSqlConnection::USE_SYSTEM_TRUST_STORE).c_str(); + // System trust store default value is true const bool useSystemCertStore = driver::odbcabstraction::AsBool(val).value_or(true); labels.push_back(CreateLabel(labelPosX, rowPos, LABEL_WIDTH, 2 * ROW_HEIGHT, L"Use System Certificate Store:", @@ -297,6 +299,11 @@ int DsnConfigurationWindow::CreateEncryptionSettingsGroup(int posX, int posY, in CreateGroupBox(posX, posY, sizeX, rowPos - posY, L"Encryption settings", ChildId::AUTH_SETTINGS_GROUP_BOX); + certificateEdit->SetEnabled(enableEncryption); + certificateBrowseButton->SetEnabled(enableEncryption); + useSystemCertStoreCheckBox->SetEnabled(enableEncryption); + disableCertVerificationCheckBox->SetEnabled(enableEncryption); + return rowPos - posY; } @@ -435,7 +442,9 @@ void DsnConfigurationWindow::SaveParameters(Configuration& targetConfig) { targetConfig.Set(FlightSqlConnection::DISABLE_CERTIFICATE_VERIFICATION, disableCertVerificationCheckBox->IsChecked() ? TRUE_STR : FALSE_STR); } else { + // System trust store verification requires encryption targetConfig.Set(FlightSqlConnection::USE_ENCRYPTION, FALSE_STR); + targetConfig.Set(FlightSqlConnection::USE_SYSTEM_TRUST_STORE, FALSE_STR); } // Get all the list properties. From 6c865bc79a3d20fd14181b8f189aa9ff5cc36bf1 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:22:13 -0700 Subject: [PATCH 58/74] CPack ODBC Windows msi installer Add ARROW_FLIGHT_SQL_ODBC_INSTALLER environment variable to enable creation of Windows installer. * Use CPack + WiX to create a `msi` installer. * run command `cpack` under the build directory to generate the installer. --- .github/workflows/cpp.yml | 1 + ci/scripts/cpp_build.sh | 1 + cpp/CMakeLists.txt | 8 ++- cpp/CMakePresets.json | 1 + cpp/cmake_modules/BuildUtils.cmake | 17 ++++-- cpp/cmake_modules/ThirdpartyToolchain.cmake | 13 +++-- cpp/src/arrow/CMakeLists.txt | 13 +++-- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 56 ++++++++++++++++++++ 8 files changed, 96 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index cc62faef2ad..56996c98ef5 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -398,6 +398,7 @@ jobs: ARROW_FLIGHT: ON ARROW_FLIGHT_SQL: ON ARROW_FLIGHT_SQL_ODBC: ON + ARROW_FLIGHT_SQL_ODBC_INSTALLER: ON ARROW_GANDIVA: ON ARROW_GCS: ON ARROW_HDFS: OFF diff --git a/ci/scripts/cpp_build.sh b/ci/scripts/cpp_build.sh index 3197426eaba..ad4f5ac5904 100755 --- a/ci/scripts/cpp_build.sh +++ b/ci/scripts/cpp_build.sh @@ -208,6 +208,7 @@ else -DARROW_FLIGHT=${ARROW_FLIGHT:-OFF} \ -DARROW_FLIGHT_SQL=${ARROW_FLIGHT_SQL:-OFF} \ -DARROW_FLIGHT_SQL_ODBC=${ARROW_FLIGHT_SQL_ODBC:-OFF} \ + -DARROW_FLIGHT_SQL_ODBC_INSTALLER=${ARROW_FLIGHT_SQL_ODBC_INSTALLER:-OFF} \ -DARROW_FUZZING=${ARROW_FUZZING:-OFF} \ -DARROW_GANDIVA_PC_CXX_FLAGS=${ARROW_GANDIVA_PC_CXX_FLAGS:-} \ -DARROW_GANDIVA=${ARROW_GANDIVA:-OFF} \ diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 18841ac874b..8d9e1f7a1ef 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -713,9 +713,13 @@ endif() install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE.txt ${CMAKE_CURRENT_SOURCE_DIR}/../NOTICE.txt - ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION "${ARROW_DOC_DIR}") + ${CMAKE_CURRENT_SOURCE_DIR}/README.md + DESTINATION "${ARROW_DOC_DIR}" + COMPONENT arrow_doc) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/gdb_arrow.py DESTINATION "${ARROW_GDB_DIR}") +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/gdb_arrow.py + DESTINATION "${ARROW_GDB_DIR}" + COMPONENT arrow_gdb) # # Validate and print out Arrow configuration options diff --git a/cpp/CMakePresets.json b/cpp/CMakePresets.json index c9e2444389f..db0a4ddd061 100644 --- a/cpp/CMakePresets.json +++ b/cpp/CMakePresets.json @@ -179,6 +179,7 @@ "ARROW_BUILD_EXAMPLES": "ON", "ARROW_BUILD_UTILITIES": "ON", "ARROW_FLIGHT_SQL_ODBC": "ON", + "ARROW_FLIGHT_SQL_ODBC_INSTALLER": "ON", "ARROW_TENSORFLOW": "ON", "PARQUET_BUILD_EXAMPLES": "ON", "PARQUET_BUILD_EXECUTABLES": "ON" diff --git a/cpp/cmake_modules/BuildUtils.cmake b/cpp/cmake_modules/BuildUtils.cmake index db760400f7c..305546572c4 100644 --- a/cpp/cmake_modules/BuildUtils.cmake +++ b/cpp/cmake_modules/BuildUtils.cmake @@ -178,10 +178,12 @@ function(arrow_install_cmake_package PACKAGE_NAME EXPORT_NAME) write_basic_package_version_file("${BUILT_CONFIG_VERSION_CMAKE}" COMPATIBILITY SameMajorVersion) install(FILES "${BUILT_CONFIG_CMAKE}" "${BUILT_CONFIG_VERSION_CMAKE}" - DESTINATION "${ARROW_CMAKE_DIR}/${PACKAGE_NAME}") + DESTINATION "${ARROW_CMAKE_DIR}/${PACKAGE_NAME}" + COMPONENT config_cmake_file) set(TARGETS_CMAKE "${PACKAGE_NAME}Targets.cmake") install(EXPORT ${EXPORT_NAME} DESTINATION "${ARROW_CMAKE_DIR}/${PACKAGE_NAME}" + COMPONENT config_cmake_export NAMESPACE "${PACKAGE_NAME}::" FILE "${TARGETS_CMAKE}") endfunction() @@ -403,8 +405,11 @@ function(ADD_ARROW_LIB LIB_NAME) install(TARGETS ${LIB_NAME}_shared ${INSTALL_IS_OPTIONAL} EXPORT ${LIB_NAME}_targets ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR} + COMPONENT ${LIB_NAME}_shared_archive LIBRARY DESTINATION ${INSTALL_LIBRARY_DIR} + COMPONENT ${LIB_NAME}_shared_library RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} + COMPONENT ${LIB_NAME}_shared_runtime INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) endif() @@ -471,8 +476,11 @@ function(ADD_ARROW_LIB LIB_NAME) install(TARGETS ${LIB_NAME}_static ${INSTALL_IS_OPTIONAL} EXPORT ${LIB_NAME}_targets ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR} + COMPONENT ${LIB_NAME}_static_library LIBRARY DESTINATION ${INSTALL_LIBRARY_DIR} + COMPONENT ${LIB_NAME}_static_library RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} + COMPONENT ${LIB_NAME}_static_library INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) endif() @@ -934,7 +942,9 @@ function(ARROW_INSTALL_ALL_HEADERS PATH) endif() list(APPEND PUBLIC_HEADERS ${HEADER}) endforeach() - install(FILES ${PUBLIC_HEADERS} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PATH}") + install(FILES ${PUBLIC_HEADERS} + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PATH}" + COMPONENT ${HEADER}_header) endfunction() function(ARROW_ADD_PKG_CONFIG MODULE) @@ -944,7 +954,8 @@ function(ARROW_ADD_PKG_CONFIG MODULE) OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/${MODULE}.pc" INPUT "${CMAKE_CURRENT_BINARY_DIR}/${MODULE}.pc.generate.in") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/$/${MODULE}.pc" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig/") + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig/" + COMPONENT ${MODULE}_pkg_config) endfunction() # Implementations of lisp "car" and "cdr" functions diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake b/cpp/cmake_modules/ThirdpartyToolchain.cmake index 8a319b02c57..be9744828e8 100644 --- a/cpp/cmake_modules/ThirdpartyToolchain.cmake +++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake @@ -236,7 +236,8 @@ function(provide_cmake_module MODULE_NAME ARROW_CMAKE_PACKAGE_NAME) message(STATUS "Providing CMake module for ${MODULE_NAME} as part of ${ARROW_CMAKE_PACKAGE_NAME} CMake package" ) install(FILES "${module}" - DESTINATION "${ARROW_CMAKE_DIR}/${ARROW_CMAKE_PACKAGE_NAME}") + DESTINATION "${ARROW_CMAKE_DIR}/${ARROW_CMAKE_PACKAGE_NAME}" + COMPONENT ${MODULE_NAME}_module) endif() endfunction() @@ -2375,20 +2376,22 @@ function(build_gtest) endforeach() install(DIRECTORY "${googletest_SOURCE_DIR}/googlemock/include/" "${googletest_SOURCE_DIR}/googletest/include/" - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT gtest_dir) add_library(arrow::GTest::gtest_headers INTERFACE IMPORTED) target_include_directories(arrow::GTest::gtest_headers INTERFACE "${googletest_SOURCE_DIR}/googlemock/include/" "${googletest_SOURCE_DIR}/googletest/include/") install(TARGETS gmock gmock_main gtest gtest_main EXPORT arrow_testing_targets - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT gtest_runtime + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT gtest_archive + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT gtest_library) if(MSVC) install(FILES $ $ $ $ DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT gtest_pdb OPTIONAL) endif() add_library(arrow::GTest::gmock ALIAS gmock) diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index 42b0bcc151c..9b2a6567508 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -345,7 +345,8 @@ endmacro() configure_file("util/config.h.cmake" "util/config.h" ESCAPE_QUOTES) configure_file("util/config_internal.h.cmake" "util/config_internal.h" ESCAPE_QUOTES) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/util/config.h" - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/arrow/util") + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/arrow/util" + COMPONENT arrow_config) set(ARROW_SRCS builder.cc @@ -1039,7 +1040,8 @@ if(ARROW_BUILD_BUNDLED_DEPENDENCIES) get_target_property(arrow_bundled_dependencies_path arrow_bundled_dependencies IMPORTED_LOCATION) install(FILES ${arrow_bundled_dependencies_path} ${INSTALL_IS_OPTIONAL} - DESTINATION ${CMAKE_INSTALL_LIBDIR}) + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT arrow_bundled_dependencies) string(PREPEND ARROW_PC_LIBS_PRIVATE " -larrow_bundled_dependencies") list(INSERT ARROW_STATIC_INSTALL_INTERFACE_LIBS 0 "Arrow::arrow_bundled_dependencies") endif() @@ -1156,6 +1158,7 @@ if(ARROW_BUILD_SHARED AND NOT WIN32) if(ARROW_GDB_AUTO_LOAD_LIBARROW_GDB_INSTALL) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libarrow_gdb.py" DESTINATION "${ARROW_GDB_AUTO_LOAD_LIBARROW_GDB_DIR}" + COMPONENT arrow_gdb RENAME "$-gdb.py") endif() endif() @@ -1219,11 +1222,13 @@ arrow_install_all_headers("arrow") config_summary_cmake_setters("${CMAKE_CURRENT_BINARY_DIR}/ArrowOptions.cmake") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ArrowOptions.cmake - DESTINATION "${ARROW_CMAKE_DIR}/Arrow") + DESTINATION "${ARROW_CMAKE_DIR}/Arrow" + COMPONENT arrow_options_cmake) # For backward compatibility for find_package(arrow) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/arrow-config.cmake - DESTINATION "${ARROW_CMAKE_DIR}/Arrow") + DESTINATION "${ARROW_CMAKE_DIR}/Arrow" + COMPONENT arrow_config_cmake) # # Unit tests diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index 29495eb0fe3..d84f4934006 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -91,3 +91,59 @@ add_arrow_lib(arrow_flight_sql_odbc foreach(LIB_TARGET ${ARROW_FLIGHT_SQL_ODBC_LIBRARIES}) target_compile_definitions(${LIB_TARGET} PRIVATE ARROW_FLIGHT_SQL_ODBC_EXPORTING) endforeach() + +# Construct ODBC Windows installer. Only Release installer is supported +if(ARROW_FLIGHT_SQL_ODBC_INSTALLER) + + include(InstallRequiredSystemLibraries) + + set(CPACK_RESOURCE_FILE_LICENSE + "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../LICENSE.txt") + # Tentative version 1.0.0 (patch version is not set) + set(CPACK_PACKAGE_VERSION_MAJOR "1") + set(CPACK_PACKAGE_VERSION_MINOR "0") + + set(CPACK_PACKAGE_NAME "Apache Arrow Flight SQL ODBC") + set(CPACK_PACKAGE_VENDOR "Apache Arrow") + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Apache Arrow Flight SQL ODBC Driver") + set(CPACK_PACKAGE_CONTACT "#TODO arrow maintainers") + + # TODO: set up `flight_sql_odbc_lib` component for macOS Installer + # TODO: set up `flight_sql_odbc_lib` component for Linux Installer + if(WIN32) + install(DIRECTORY "${BUILD_OUTPUT_ROOT_DIRECTORY}${CMAKE_BUILD_TYPE}/" + DESTINATION bin + COMPONENT flight_sql_odbc_lib + FILES_MATCHING + PATTERN "*.dll" + PATTERN "arrow_flight_testing.dll" EXCLUDE + PATTERN "arrow_testing.dll" EXCLUDE + PATTERN "gflags.dll" EXCLUDE + PATTERN "gmock.dll" EXCLUDE + PATTERN "gtest.dll" EXCLUDE + PATTERN "gtest_main.dll" EXCLUDE) + endif() + + get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) + set(CPACK_COMPONENTS_ALL Unspecified) + list(APPEND CPACK_COMPONENTS_ALL "flight_sql_odbc_lib") + + if(WIN32) + # WiX msi installer on Windows + # CPack is compatible with WiX V.5 and V.6 + set(CPACK_GENERATOR "WIX") + set(CPACK_WIX_VERSION 4) + + # Upgrade GUID is required to be unchanged for ODBC installer to upgrade + set(CPACK_WIX_UPGRADE_GUID "DBF27A18-F8BF-423F-9E3A-957414D52C4B") + endif() + # TODO: create macOS Installer using cpack + # TODO: create Linux Installer using cpack + + # Load CPack after all CPACK* variables are set + include(CPack) + cpack_add_component(flight_sql_odbc_lib + DISPLAY_NAME "ODBC library" + DESCRIPTION "ODBC library bin, required to install" + REQUIRED) +endif() From fedad136b59ce1e50a2ba494f8c8b3f304b5a964 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:20:31 -0700 Subject: [PATCH 59/74] Register ODBC Driver on Windows in installer * In `wxs`, cannot use `package`, need to use `fragment` instead * Use component as feature will automatically be generated * Set Patch version + add installer instructions --- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 30 +++++++++++---- cpp/src/arrow/flight/sql/odbc/README | 7 ++++ .../install/arrow-flight-sql-odbc-patch.xml | 22 +++++++++++ .../odbc/install/arrow-flight-sql-odbc.wxs | 37 +++++++++++++++++++ 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc-patch.xml create mode 100644 cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc.wxs diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index d84f4934006..c33374df9c4 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -99,9 +99,10 @@ if(ARROW_FLIGHT_SQL_ODBC_INSTALLER) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../LICENSE.txt") - # Tentative version 1.0.0 (patch version is not set) + # Tentative version 1.0.0 set(CPACK_PACKAGE_VERSION_MAJOR "1") set(CPACK_PACKAGE_VERSION_MINOR "0") + set(CPACK_PACKAGE_VERSION_PATCH "0") set(CPACK_PACKAGE_NAME "Apache Arrow Flight SQL ODBC") set(CPACK_PACKAGE_VENDOR "Apache Arrow") @@ -115,13 +116,26 @@ if(ARROW_FLIGHT_SQL_ODBC_INSTALLER) DESTINATION bin COMPONENT flight_sql_odbc_lib FILES_MATCHING - PATTERN "*.dll" - PATTERN "arrow_flight_testing.dll" EXCLUDE - PATTERN "arrow_testing.dll" EXCLUDE - PATTERN "gflags.dll" EXCLUDE - PATTERN "gmock.dll" EXCLUDE - PATTERN "gtest.dll" EXCLUDE - PATTERN "gtest_main.dll" EXCLUDE) + # Use regex for dll name patterns with versions + PATTERN "abseil_dll.dll" + PATTERN "arrow.dll" + PATTERN "arrow_compute.dll" + PATTERN "arrow_flight.dll" + PATTERN "arrow_flight_sql.dll" + PATTERN "arrow_flight_sql_odbc.dll" + PATTERN "boost_locale*.dll" + PATTERN "cares.dll" + PATTERN "libcrypto*.dll" + PATTERN "libprotobuf.dll" + PATTERN "libssl*.dll" + PATTERN "re2.dll" + PATTERN "utf8proc.dll" + PATTERN "zlib1.dll") + + set(CPACK_WIX_EXTRA_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/install/arrow-flight-sql-odbc.wxs") + set(CPACK_WIX_PATCH_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/install/arrow-flight-sql-odbc-patch.xml") endif() get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) diff --git a/cpp/src/arrow/flight/sql/odbc/README b/cpp/src/arrow/flight/sql/odbc/README index 04749d9b859..da9857b7ecc 100644 --- a/cpp/src/arrow/flight/sql/odbc/README +++ b/cpp/src/arrow/flight/sql/odbc/README @@ -17,3 +17,10 @@ After the build succeeds, the ODBC DLL will be located in If the registration is successful, then Apache Arrow Flight SQL ODBC Driver should show as an available ODBC driver in the x64 ODBC Driver Manager. + +Steps to Generate Windows Installer +1. Build with `ARROW_FLIGHT_SQL_ODBC=ON` and `ARROW_FLIGHT_SQL_ODBC_INSTALLER=ON`. +2. `cd` to `build` folder. +3. Run `cpack`. + +If the generation is successful, you will find `Apache Arrow Flight SQL ODBC--win64.msi` generated under the `build` folder. diff --git a/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc-patch.xml b/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc-patch.xml new file mode 100644 index 00000000000..f1a63ce5d3b --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc-patch.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc.wxs b/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc.wxs new file mode 100644 index 00000000000..bd0216aa766 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/install/arrow-flight-sql-odbc.wxs @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + From 39586222b81b169cd2a2fc9d2f5b12f1775c4314 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:19:34 -0700 Subject: [PATCH 60/74] Use Arrow logo banner * replaces default banner with Arrow logo banner --- cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 2 ++ .../sql/odbc/install/arrow-wix-banner.bmp | Bin 0 -> 29930 bytes 2 files changed, 2 insertions(+) create mode 100644 cpp/src/arrow/flight/sql/odbc/install/arrow-wix-banner.bmp diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index c33374df9c4..d2a4552fab0 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -136,6 +136,8 @@ if(ARROW_FLIGHT_SQL_ODBC_INSTALLER) "${CMAKE_CURRENT_SOURCE_DIR}/install/arrow-flight-sql-odbc.wxs") set(CPACK_WIX_PATCH_FILE "${CMAKE_CURRENT_SOURCE_DIR}/install/arrow-flight-sql-odbc-patch.xml") + + set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/install/arrow-wix-banner.bmp") endif() get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) diff --git a/cpp/src/arrow/flight/sql/odbc/install/arrow-wix-banner.bmp b/cpp/src/arrow/flight/sql/odbc/install/arrow-wix-banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0c82036f4ecfc1e8c34fcb7eae56ccd7d9be4731 GIT binary patch literal 29930 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 MS>rLo%F*-te Date: Mon, 25 Aug 2025 10:20:27 -0700 Subject: [PATCH 61/74] Indicate Driver Company and Version on Driver Manager * use versioninfo.rc.in template * use `1 VERSIONINFO` for it to work properly * Set version variables * Add `.rc` to gitignore * Add `@` variables to rc template --- .gitignore | 3 ++ cpp/src/arrow/flight/sql/odbc/CMakeLists.txt | 30 ++++++++--- .../flight/sql/odbc/install/versioninfo.rc.in | 54 +++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/install/versioninfo.rc.in diff --git a/.gitignore b/.gitignore index d9fda0a1641..64c713a74ed 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ java/.mvn/.develocity/ # rat filtered_rat.txt rat.txt + +# rc +*.rc diff --git a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt index d2a4552fab0..d641873514b 100644 --- a/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt +++ b/cpp/src/arrow/flight/sql/odbc/CMakeLists.txt @@ -54,10 +54,28 @@ add_subdirectory(tests) arrow_install_all_headers("arrow/flight/sql/odbc") +# ODBC Release information +set(ODBC_PACKAGE_VERSION_MAJOR "1") +set(ODBC_PACKAGE_VERSION_MINOR "0") +set(ODBC_PACKAGE_VERSION_PATCH "0") +set(ODBC_PACKAGE_NAME "Apache Arrow Flight SQL ODBC") +set(ODBC_PACKAGE_VENDOR "Apache Arrow") + set(ARROW_FLIGHT_SQL_ODBC_SRCS entry_points.cc odbc_api.cc) if(WIN32) - list(APPEND ARROW_FLIGHT_SQL_ODBC_SRCS odbc.def) + set(VER_FILEVERSION + "${ODBC_PACKAGE_VERSION_MAJOR},${ODBC_PACKAGE_VERSION_MINOR},${ODBC_PACKAGE_VERSION_PATCH},0" + ) + set(VER_FILEVERSION_STR + ${ODBC_PACKAGE_VERSION_MAJOR}.${ODBC_PACKAGE_VERSION_MINOR}.${ODBC_PACKAGE_VERSION_PATCH} + ) + set(VER_COMPANYNAME_STR ${ODBC_PACKAGE_VENDOR}) + set(VER_PRODUCTNAME_STR ${ODBC_PACKAGE_NAME}) + + configure_file("install/versioninfo.rc.in" "install/versioninfo.rc" @ONLY) + + list(APPEND ARROW_FLIGHT_SQL_ODBC_SRCS odbc.def install/versioninfo.rc) endif() add_arrow_lib(arrow_flight_sql_odbc @@ -100,12 +118,12 @@ if(ARROW_FLIGHT_SQL_ODBC_INSTALLER) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../LICENSE.txt") # Tentative version 1.0.0 - set(CPACK_PACKAGE_VERSION_MAJOR "1") - set(CPACK_PACKAGE_VERSION_MINOR "0") - set(CPACK_PACKAGE_VERSION_PATCH "0") + set(CPACK_PACKAGE_VERSION_MAJOR ${ODBC_PACKAGE_VERSION_MAJOR}) + set(CPACK_PACKAGE_VERSION_MINOR ${ODBC_PACKAGE_VERSION_MINOR}) + set(CPACK_PACKAGE_VERSION_PATCH ${ODBC_PACKAGE_VERSION_PATCH}) - set(CPACK_PACKAGE_NAME "Apache Arrow Flight SQL ODBC") - set(CPACK_PACKAGE_VENDOR "Apache Arrow") + set(CPACK_PACKAGE_NAME ${ODBC_PACKAGE_NAME}) + set(CPACK_PACKAGE_VENDOR ${ODBC_PACKAGE_VENDOR}) set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Apache Arrow Flight SQL ODBC Driver") set(CPACK_PACKAGE_CONTACT "#TODO arrow maintainers") diff --git a/cpp/src/arrow/flight/sql/odbc/install/versioninfo.rc.in b/cpp/src/arrow/flight/sql/odbc/install/versioninfo.rc.in new file mode 100644 index 00000000000..13024a7a50b --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/install/versioninfo.rc.in @@ -0,0 +1,54 @@ +// 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. + +#define VER_FILEVERSION @VER_FILEVERSION@ +#define VER_FILEVERSION_STR "@VER_FILEVERSION_STR@\0" + +#define VER_PRODUCTVERSION @VER_FILEVERSION@ +#define VER_PRODUCTVERSION_STR "@VER_FILEVERSION_STR@\0" + +#define VER_COMPANYNAME_STR "@VER_COMPANYNAME_STR@\0" +#define VER_PRODUCTNAME_STR "@VER_PRODUCTNAME_STR@\0" + +1 VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", VER_COMPANYNAME_STR + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "ProductName", VER_PRODUCTNAME_STR + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + /* The following line should only be modified for localized versions. */ + /* It consists of any number of WORD,WORD pairs, with each pair */ + /* describing a language,codepage combination supported by the file. */ + /* */ + /* For example, a file might have values "0x409,1252" indicating that it */ + /* supports English language (0x409) in the Windows ANSI codepage (1252). */ + + VALUE "Translation", 0x409, 1252 + + END +END From 8d35fd1498e41f207c001f2343b9abaa28649a8b Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 26 Aug 2025 09:45:21 -0700 Subject: [PATCH 62/74] Return nameLengthPtr value as length in characters --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 3 +- .../flight/sql/odbc/tests/columns_test.cc | 48 +++++++------------ .../flight/sql/odbc/tests/tables_test.cc | 12 ++--- .../flight/sql/odbc/tests/type_info_test.cc | 6 +-- 4 files changed, 24 insertions(+), 45 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 75924f596c4..82a167b3c16 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1435,7 +1435,8 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* col ird->GetField(columnNumber, SQL_DESC_NAME, columnName, bufferLength, &outputLengthInt); if (nameLengthPtr) { - *nameLengthPtr = static_cast(outputLengthInt); + // returned length should be in characters + *nameLengthPtr = static_cast(outputLengthInt / GetSqlWCharSize()); } } diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index c48339a1103..60a8e251576 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2431,11 +2431,9 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadata) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, 1024); @@ -2515,11 +2513,9 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadata) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -2579,11 +2575,9 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadata) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -2643,11 +2637,9 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataODBC2) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -2701,11 +2693,9 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadata) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -2755,11 +2745,9 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadata) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(expectedColumnName)); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, expectedColumnName); EXPECT_EQ(columnDataType, expectedColumnDataType); EXPECT_EQ(columnSize, expectedColumnSize); @@ -2810,11 +2798,9 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeCol) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -2880,11 +2866,9 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeColODBC2) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index 68405c51583..a6cbd38f881 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -607,11 +607,9 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeCol) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); @@ -660,11 +658,9 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeColODBC2) { EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, wcslen(columnNames[i])); - // Returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc index e92e8ff3cc3..29d737c38d6 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/type_info_test.cc @@ -49,11 +49,9 @@ void checkSQLDescribeCol(SQLHSTMT stmt, const SQLUSMALLINT columnIndex, EXPECT_EQ(ret, SQL_SUCCESS); - EXPECT_GT(nameLength, 0); + EXPECT_EQ(nameLength, expectedName.size()); - // returned nameLength is in bytes so convert to length in characters - size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); - std::wstring returned(columnName, columnName + charCount); + std::wstring returned(columnName, columnName + nameLength); EXPECT_EQ(returned, expectedName); EXPECT_EQ(columnDataType, expectedDataType); EXPECT_EQ(columnSize, expectedColumnSize); From 2e16dd00d2725b46d91194b23d089322fc6ce6a1 Mon Sep 17 00:00:00 2001 From: justing-bq <62349012+justing-bq@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:08:43 -0700 Subject: [PATCH 63/74] Allow spaces while parsing Table Type --- .../flight_sql_statement_get_tables.cc | 47 +++++++++---------- .../odbc/flight_sql/parse_table_types_test.cc | 13 +++++ 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc index ba3389ccba9..7dfedac6392 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/flight_sql_statement_get_tables.cc @@ -22,6 +22,7 @@ #include "arrow/flight/sql/odbc/flight_sql/utils.h" #include "arrow/flight/sql/odbc/odbcabstraction/include/odbcabstraction/platform.h" #include "arrow/flight/types.h" +#include "arrow/util/string.h" namespace driver { namespace flight_sql { @@ -31,6 +32,15 @@ using arrow::flight::FlightClientOptions; using arrow::flight::FlightInfo; using arrow::flight::sql::FlightSqlClient; +static void AddTableType(std::string& table_type, std::vector& table_types) { + std::string trimmed_type = arrow::internal::TrimString(table_type); + + // Only put the string if the trimmed result is non-empty + if (trimmed_type.length() > 0) { + table_types.emplace_back(trimmed_type); + } +} + void ParseTableTypes(const std::string& table_type, std::vector& table_types) { bool encountered = false; // for checking if there is a single quote @@ -39,36 +49,23 @@ void ParseTableTypes(const std::string& table_type, for (char temp : table_type) { // while still in the string switch (temp) { // switch depending on the character case '\'': // if the character is a single quote - if (encountered) { - encountered = false; // if we already found a single quote, reset encountered - } else { - encountered = - true; // if we haven't found a single quote, set encountered to true - } + // track when we've encountered a single opening quote + // and are still looking for the closing quote + encountered = !encountered; break; - case ',': // if it is a comma - if (!encountered) { // if we have not found a single quote - table_types.push_back(curr_parse); // put our current string into our vector - curr_parse = ""; // reset the current string + case ',': // if it is a comma + if (!encountered) { // if we have not found a single quote + AddTableType(curr_parse, table_types); // put current string into vector + curr_parse = ""; // reset the current string break; } - default: // if it is a normal character - if (encountered && isspace(temp)) { - curr_parse.push_back(temp); // if we have found a single quote put the - // whitespace, we don't care - } else if (temp == '\'' || temp == ' ') { - break; // if the current character is a single quote, trash it and go to - // the next character. - } else { - curr_parse.push_back(temp); // if all of the above failed, put the - // character into the current string - } - break; // go to the next character + [[fallthrough]]; + default: // if it is a normal character + curr_parse.push_back(temp); // put the character into the current string + break; // go to the next character } } - table_types.emplace_back( - curr_parse); // if we have found a single quote put the whitespace, - // we don't care + AddTableType(curr_parse, table_types); } std::shared_ptr GetTablesForSQLAllCatalogs( diff --git a/cpp/src/arrow/flight/sql/odbc/flight_sql/parse_table_types_test.cc b/cpp/src/arrow/flight/sql/odbc/flight_sql/parse_table_types_test.cc index 9bfcabb0bbd..9e6e93ed21c 100644 --- a/cpp/src/arrow/flight/sql/odbc/flight_sql/parse_table_types_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/flight_sql/parse_table_types_test.cc @@ -49,5 +49,18 @@ TEST(TableTypeParser, ParsingWithSingleQuotesWithoutLeadingWhiteSpace) { TEST(TableTypeParser, ParsingWithCommaInsideSingleQuotes) { AssertParseTest("'TABLE, TEST', 'VIEW, TEMPORARY'", {"TABLE, TEST", "VIEW, TEMPORARY"}); } + +TEST(TableTypeParser, ParsingWithManyLeadingAndTrailingWhiteSpaces) { + AssertParseTest(" TABLE , VIEW ", {"TABLE", "VIEW"}); +} + +TEST(TableTypeParser, ParsingWithOnlyWhiteSpaceBetweenCommas) { + AssertParseTest("TABLE, ,VIEW", {"TABLE", "VIEW"}); +} + +TEST(TableTypeParser, ParsingWithWhiteSpaceInsideValue) { + AssertParseTest("BASE TABLE", {"BASE TABLE"}); +} + } // namespace flight_sql } // namespace driver From 4ba065c4e8ea676216aeaef242d6d6b88fe3644a Mon Sep 17 00:00:00 2001 From: rscales Date: Fri, 22 Aug 2025 18:03:30 +0100 Subject: [PATCH 64/74] Initial version of table read test --- .../odbc/performance_tests/table_read_test.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py new file mode 100644 index 00000000000..57dc5772b34 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +#---------------------------------- +# IMPORTS +#---------------------------------- +import pyodbc +import pandas as pd +import argparse +import time +from os import environ +import statistics + +#---------------------------------- +# ARGUMENTS +#---------------------------------- +parser = argparse.ArgumentParser(description="Benchmark ODBC Flight SQL Driver query performance against Dremio.") +parser.add_argument("--dsn", required=True, help="ODBC DSN to use (must be pre-configured)") +parser.add_argument("--limit", type=int, default=1000, help="LIMIT for the SELECT query") +parser.add_argument("--iterations", type=int, default=5, help="Number of iterations to run") +args = parser.parse_args() + +#---------------------------------- +# SETUP +#---------------------------------- +token = environ.get("token") +if not token: + raise RuntimeError("Environment variable 'token' must be set.") + +# Connector string +connector = ( + f"Driver={{{args.dsn}}};" + "ConnectionType=Direct;" + "HOST=dremio-clients-demo.test.drem.io;" + "PORT=32010;" + "AuthenticationType=Plain;" + f"UID=improving;PWD={token};ssl=true;" +) + +#---------------------------------- +# CREATE CONNECTION AND CURSOR +#---------------------------------- + +# Original +#cnxn = pyodbc.connect(connector, autocommit=True) +#cnxn.setdecoding(pyodbc.SQL_CHAR, encoding="utf-8") +#cursor = cnxn.cursor() + +# +# Your Apache ODBC driver doesn’t yet fully support the W (wide/UTF-16) entry points → forcing ansi=True lets pyodbc stick to SQLExecDirectA etc., +# which works around it. +# Now queries run to completion, but you’re seeing garbled characters in column names. +# +#cnxn = pyodbc.connect(connector, autocommit=True, ansi=False) +cnxn = pyodbc.connect(connector, autocommit=True, ansi=True) + +# Ensure query text encoding works for drivers without SQLExecDirectW +cnxn.setencoding(encoding='utf-8') + +# Handle both CHAR and WCHAR data returned from the driver +cnxn.setdecoding(pyodbc.SQL_CHAR, encoding="utf-8") +# Comment this out unless you confirm you need it: +# cnxn.setdecoding(pyodbc.SQL_WCHAR, encoding="utf-16le") + +cursor = cnxn.cursor() + +#---------------------------------- +# RUN TEST +#---------------------------------- +query = f'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT {args.limit}' + +times_ms = [] +column_types = None + +for i in range(args.iterations): + start = time.time() + cursor.execute(query) + rows = cursor.fetchall() + elapsed_ms = (time.time() - start) * 1000 + times_ms.append(elapsed_ms) + + # grab column metadata once (first iteration is enough) + if column_types is None: + column_types = [ + (desc[0], desc[1]) # (column_name, sql_type) + for desc in cursor.description + ] + + print(f"Iteration {i+1}: {elapsed_ms:.2f} ms (rows returned: {len(rows)})") + +#---------------------------------- +# SUMMARY +#---------------------------------- +avg_ms = statistics.mean(times_ms) +min_ms = min(times_ms) +max_ms = max(times_ms) + +print("\n=== Benchmark Summary ===") +print(f"DSN used: {args.dsn}") +print(f"Iterations: {args.iterations}") +print(f"Query LIMIT: {args.limit}") +print(f"Average time: {avg_ms:.2f} ms") +print(f"Shortest time: {min_ms:.2f} ms") +print(f"Longest time: {max_ms:.2f} ms") + +# Report column-level SQL type usage +if column_types: + print("\nColumn SQL Types:") + for col_name, sql_type in column_types: + if sql_type == pyodbc.SQL_CHAR: + type_str = "SQL_CHAR (UTF-8)" + elif sql_type == pyodbc.SQL_WCHAR: + type_str = "SQL_WCHAR (UTF-16LE)" + else: + type_str = f"Other (SQL type code {sql_type})" + print(f" {col_name}: {type_str}") From 7dceaa8957250a9728417f69ddb6089e0293c13c Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" <96995091+alinaliBQ@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:56:08 -0700 Subject: [PATCH 65/74] Performance test change to use driver instead of DSN Since the connection string says Driver= and not DSN=, it makes sense to change the command line to driver to indicate that we are passing the driver name value, not the DSN value --- .../flight/sql/odbc/performance_tests/table_read_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py index 57dc5772b34..4f175f788b6 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -13,7 +13,7 @@ # ARGUMENTS #---------------------------------- parser = argparse.ArgumentParser(description="Benchmark ODBC Flight SQL Driver query performance against Dremio.") -parser.add_argument("--dsn", required=True, help="ODBC DSN to use (must be pre-configured)") +parser.add_argument("--driver", required=True, help="ODBC Driver to use (must be pre-configured)") parser.add_argument("--limit", type=int, default=1000, help="LIMIT for the SELECT query") parser.add_argument("--iterations", type=int, default=5, help="Number of iterations to run") args = parser.parse_args() @@ -27,7 +27,7 @@ # Connector string connector = ( - f"Driver={{{args.dsn}}};" + f"Driver={{{args.driver}}};" "ConnectionType=Direct;" "HOST=dremio-clients-demo.test.drem.io;" "PORT=32010;" @@ -94,7 +94,7 @@ max_ms = max(times_ms) print("\n=== Benchmark Summary ===") -print(f"DSN used: {args.dsn}") +print(f"Driver used: {args.driver}") print(f"Iterations: {args.iterations}") print(f"Query LIMIT: {args.limit}") print(f"Average time: {avg_ms:.2f} ms") From e6039eef366237ac73a6103ff4e5a02dd34e1491 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 28 Aug 2025 19:28:50 +0100 Subject: [PATCH 66/74] Refactor to support additional test cases and add query comparison test --- .../performance_tests/compare_limits_plot.py | 100 +++++++++ .../performance_tests/compare_queries_bar.py | 174 +++++++++++++++ .../odbc/performance_tests/table_read_test.py | 203 ++++++++---------- 3 files changed, 368 insertions(+), 109 deletions(-) create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py new file mode 100644 index 00000000000..3461f03b3cc --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import subprocess +import argparse +import json +import matplotlib.pyplot as plt +import csv + +# ---------------------------- +# ARGUMENTS +# ---------------------------- +parser = argparse.ArgumentParser(description="Compare two ODBC Flight SQL drivers using JSON output.") +parser.add_argument("--driver_a", required=True, help="First driver name") +parser.add_argument("--driver_b", required=True, help="Second driver name") +parser.add_argument("--iterations", type=int, default=5, help="Number of iterations per limit") +parser.add_argument("--limits", type=int, nargs="+", default=[100, 1000, 10000], help="List of LIMIT values") +parser.add_argument("--outfile", default="perf_results.csv", help="CSV output filename") +parser.add_argument("--plotfile", default="perf_plot.png", help="Plot output filename") +args = parser.parse_args() + +# ---------------------------- +# HELPER FUNCTION TO RUN DRIVER +# ---------------------------- +def run_driver(driver_name): + results = {} + for limit in args.limits: + cmd = [ + "python", "table_read_test.py", + "--driver", driver_name, + "--limit", str(limit), + "--iterations", str(args.iterations), + "--json" + ] + print(f"\nRunning {driver_name} with LIMIT={limit}...") + + try: + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + output = proc.stdout.strip() + # Extract JSON from output + json_start = output.find("{") + json_end = output.rfind("}") + 1 + json_text = output[json_start:json_end] + data = json.loads(json_text) + avg_ms = data["avg_ms"] + min_ms = data["min_ms"] + max_ms = data["max_ms"] + results[limit] = (avg_ms, min_ms, max_ms) + except subprocess.CalledProcessError as e: + print(f"Error running {driver_name} with LIMIT={limit}:\n{e.stdout}\n{e.stderr}") + results[limit] = (None, None, None) + except Exception as e: + print(f"Failed to parse JSON for {driver_name} with LIMIT={limit}: {e}") + results[limit] = (None, None, None) + + return results + +# ---------------------------- +# RUN DRIVERS +# ---------------------------- +driver_a_results = run_driver(args.driver_a) +driver_b_results = run_driver(args.driver_b) + +# ---------------------------- +# SAVE RESULTS TO CSV +# ---------------------------- +with open(args.outfile, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["Driver", "Limit", "Avg_ms", "Min_ms", "Max_ms"]) + for limit, (avg, min_, max_) in driver_a_results.items(): + writer.writerow([args.driver_a, limit, avg, min_, max_]) + for limit, (avg, min_, max_) in driver_b_results.items(): + writer.writerow([args.driver_b, limit, avg, min_, max_]) +print(f"Results saved to {args.outfile}") + +# ---------------------------- +# PLOT RESULTS WITH ERROR BARS +# ---------------------------- +limits = args.limits +avg_a = [driver_a_results[l][0] for l in limits] +avg_b = [driver_b_results[l][0] for l in limits] + +err_a = [[avg_a[i] - driver_a_results[l][1] if driver_a_results[l][1] else 0 for i, l in enumerate(limits)], + [driver_a_results[l][2] - avg_a[i] if driver_a_results[l][2] else 0 for i, l in enumerate(limits)]] + +err_b = [[avg_b[i] - driver_b_results[l][1] if driver_b_results[l][1] else 0 for i, l in enumerate(limits)], + [driver_b_results[l][2] - avg_b[i] if driver_b_results[l][2] else 0 for i, l in enumerate(limits)]] + +plt.figure(figsize=(10,6)) +plt.errorbar(limits, avg_a, yerr=err_a, fmt='o-', capsize=5, label=args.driver_a) +plt.errorbar(limits, avg_b, yerr=err_b, fmt='s-', capsize=5, label=args.driver_b) +plt.xlabel("LIMIT value") +plt.ylabel("Query time (ms)") +plt.title("ODBC Flight SQL Driver Performance Comparison") +plt.legend() +plt.grid(True) +plt.xscale("log") +plt.yscale("log") +plt.savefig(args.plotfile) +print(f"Plot saved to {args.plotfile}") +plt.show() + diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py new file mode 100644 index 00000000000..776cbcbf693 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import csv +import json +import matplotlib.pyplot as plt + +# ---------------------------- +# Helper function to run a query via table_read_test.py +# ---------------------------- +def run_query(driver, query, iterations, label): + cmd = [ + "python", "table_read_test.py", + "--driver", driver, + "--iterations", str(iterations), + "--json", + "--query", query + ] + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + full_output = [] + json_lines = [] + inside_json = False + for line in proc.stdout: + line = line.rstrip() + full_output.append(line) + print(line) + if line.startswith("{"): + inside_json = True + if inside_json: + json_lines.append(line) + if line.endswith("}"): + inside_json = False + proc.wait() + + if not json_lines: + print(f"\n--- FULL CHILD OUTPUT ({label}, {driver}) ---") + print("\n".join(full_output)) + print("--- END CHILD OUTPUT ---\n") + return None + + json_text = "\n".join(json_lines) + return json.loads(json_text) + + except Exception as e: + print(f"Error running {label} for {driver}: {e}") + return None + +# ---------------------------- +# Main script +# ---------------------------- +def main(): + parser = argparse.ArgumentParser(description="Compare two ODBC Flight SQL drivers on multiple queries") + parser.add_argument("--driver_a", required=True, help="Driver A name") + parser.add_argument("--driver_b", required=True, help="Driver B name") + parser.add_argument("--iterations", type=int, default=5, help="Number of iterations per query") + parser.add_argument("--outfile", default="compare_results.csv", help="CSV output file") + parser.add_argument("--plotfile", default="compare_plot.png", help="Plot output file") + args = parser.parse_args() + + # ---------------------------- + # Hardcoded queries + # ---------------------------- + queries = { + "query1": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 100', + "query2": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 1000', + "query3": 'SELECT passenger_count, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "query4": 'SELECT passenger_count, AVG(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "query5": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" WHERE fare_amount > 50 LIMIT 500', + "query6": 'SELECT passenger_count, SUM(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "query7": 'SELECT passenger_count, fare_amount, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count, fare_amount', + "query8": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" ORDER BY fare_amount DESC LIMIT 100' + } + + results = [] + + # ---------------------------- + # Run all queries for both drivers + # ---------------------------- + for label, query in queries.items(): + print(f"\nRunning {label}...") + result_a = run_query(args.driver_a, query, args.iterations, label) + result_b = run_query(args.driver_b, query, args.iterations, label) + + row = {"query": label} + if result_a: + row.update({ + "driver_a": args.driver_a, + "a_avg": result_a["avg_ms"], + "a_min": result_a["min_ms"], + "a_max": result_a["max_ms"], + }) + else: + row.update({ + "driver_a": args.driver_a, + "a_avg": "N/A", + "a_min": "N/A", + "a_max": "N/A", + }) + + if result_b: + row.update({ + "driver_b": args.driver_b, + "b_avg": result_b["avg_ms"], + "b_min": result_b["min_ms"], + "b_max": result_b["max_ms"], + }) + else: + row.update({ + "driver_b": args.driver_b, + "b_avg": "N/A", + "b_min": "N/A", + "b_max": "N/A", + }) + + results.append(row) + + # ---------------------------- + # Write results to CSV + # ---------------------------- + with open(args.outfile, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=[ + "query", "driver_a", "a_avg", "a_min", "a_max", + "driver_b", "b_avg", "b_min", "b_max" + ]) + writer.writeheader() + for row in results: + writer.writerow(row) + print(f"\nResults written to {args.outfile}") + + # ---------------------------- + # Plot results with error bars using driver color coding + # ---------------------------- + queries_labels = [r["query"] for r in results] + x = range(len(queries_labels)) + width = 0.35 + fig, ax = plt.subplots(figsize=(14, 6)) + + driver_colors = { + "a": "#1f77b4", # Driver A: blue + "b": "#ff7f0e" # Driver B: orange + } + + for i, r in enumerate(results): + # Driver A + if r["a_avg"] != "N/A": + a_mean = r["a_avg"] + a_err = [[a_mean - r["a_min"]], [r["a_max"] - a_mean]] + ax.bar(i - width/2, a_mean, width, yerr=a_err, capsize=5, + color=driver_colors["a"], alpha=0.8, label=args.driver_a if i == 0 else "") + + # Driver B + if r["b_avg"] != "N/A": + b_mean = r["b_avg"] + b_err = [[b_mean - r["b_min"]], [r["b_max"] - b_mean]] + ax.bar(i + width/2, b_mean, width, yerr=b_err, capsize=5, + color=driver_colors["b"], alpha=0.8, label=args.driver_b if i == 0 else "") + + ax.set_ylabel("Execution time (ms)") + ax.set_title("Query Performance Comparison: Driver A vs Driver B") + ax.set_xticks(x) + ax.set_xticklabels(queries_labels, rotation=45, ha="right") + + ax.legend(title="Driver") + + plt.tight_layout() + plt.savefig(args.plotfile) + print(f"Plot saved to {args.plotfile}") + plt.show() + + +if __name__ == "__main__": + main() + diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py index 4f175f788b6..df248ef4445 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -1,114 +1,99 @@ #!/usr/bin/env python3 -#---------------------------------- -# IMPORTS -#---------------------------------- -import pyodbc -import pandas as pd import argparse +import pyodbc import time +import json +import csv from os import environ import statistics - -#---------------------------------- -# ARGUMENTS -#---------------------------------- -parser = argparse.ArgumentParser(description="Benchmark ODBC Flight SQL Driver query performance against Dremio.") -parser.add_argument("--driver", required=True, help="ODBC Driver to use (must be pre-configured)") -parser.add_argument("--limit", type=int, default=1000, help="LIMIT for the SELECT query") -parser.add_argument("--iterations", type=int, default=5, help="Number of iterations to run") -args = parser.parse_args() - -#---------------------------------- -# SETUP -#---------------------------------- -token = environ.get("token") -if not token: - raise RuntimeError("Environment variable 'token' must be set.") - -# Connector string -connector = ( - f"Driver={{{args.driver}}};" - "ConnectionType=Direct;" - "HOST=dremio-clients-demo.test.drem.io;" - "PORT=32010;" - "AuthenticationType=Plain;" - f"UID=improving;PWD={token};ssl=true;" -) - -#---------------------------------- -# CREATE CONNECTION AND CURSOR -#---------------------------------- - -# Original -#cnxn = pyodbc.connect(connector, autocommit=True) -#cnxn.setdecoding(pyodbc.SQL_CHAR, encoding="utf-8") -#cursor = cnxn.cursor() - -# -# Your Apache ODBC driver doesn’t yet fully support the W (wide/UTF-16) entry points → forcing ansi=True lets pyodbc stick to SQLExecDirectA etc., -# which works around it. -# Now queries run to completion, but you’re seeing garbled characters in column names. -# -#cnxn = pyodbc.connect(connector, autocommit=True, ansi=False) -cnxn = pyodbc.connect(connector, autocommit=True, ansi=True) - -# Ensure query text encoding works for drivers without SQLExecDirectW -cnxn.setencoding(encoding='utf-8') - -# Handle both CHAR and WCHAR data returned from the driver -cnxn.setdecoding(pyodbc.SQL_CHAR, encoding="utf-8") -# Comment this out unless you confirm you need it: -# cnxn.setdecoding(pyodbc.SQL_WCHAR, encoding="utf-16le") - -cursor = cnxn.cursor() - -#---------------------------------- -# RUN TEST -#---------------------------------- -query = f'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT {args.limit}' - -times_ms = [] -column_types = None - -for i in range(args.iterations): - start = time.time() - cursor.execute(query) - rows = cursor.fetchall() - elapsed_ms = (time.time() - start) * 1000 - times_ms.append(elapsed_ms) - - # grab column metadata once (first iteration is enough) - if column_types is None: - column_types = [ - (desc[0], desc[1]) # (column_name, sql_type) - for desc in cursor.description - ] - - print(f"Iteration {i+1}: {elapsed_ms:.2f} ms (rows returned: {len(rows)})") - -#---------------------------------- -# SUMMARY -#---------------------------------- -avg_ms = statistics.mean(times_ms) -min_ms = min(times_ms) -max_ms = max(times_ms) - -print("\n=== Benchmark Summary ===") -print(f"Driver used: {args.driver}") -print(f"Iterations: {args.iterations}") -print(f"Query LIMIT: {args.limit}") -print(f"Average time: {avg_ms:.2f} ms") -print(f"Shortest time: {min_ms:.2f} ms") -print(f"Longest time: {max_ms:.2f} ms") - -# Report column-level SQL type usage -if column_types: - print("\nColumn SQL Types:") - for col_name, sql_type in column_types: - if sql_type == pyodbc.SQL_CHAR: - type_str = "SQL_CHAR (UTF-8)" - elif sql_type == pyodbc.SQL_WCHAR: - type_str = "SQL_WCHAR (UTF-16LE)" - else: - type_str = f"Other (SQL type code {sql_type})" - print(f" {col_name}: {type_str}") +import sys + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark ODBC Flight SQL Driver query performance." + ) + parser.add_argument("--driver", required=True, help="ODBC driver name") + parser.add_argument("--limit", type=int, help="LIMIT for SELECT query (ignored if --query provided)") + parser.add_argument("--iterations", type=int, default=1, help="Number of iterations to run") + parser.add_argument("--json", action="store_true", help="Output JSON") + parser.add_argument("--csv", help="Output CSV file") + parser.add_argument("--query", type=str, help="Optional SQL query to run") + args = parser.parse_args() + + token = environ.get("token") + if not token: + raise RuntimeError("Environment variable 'token' must be set.") + + # Build SQL query + if args.query: + sql = args.query + else: + limit = args.limit if args.limit else 100 + sql = f'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT {limit}' + + # Connection string for Arrow Flight SQL ODBC + conn_str = ( + f"Driver={{{args.driver}}};" + "ConnectionType=Direct;" + "HOST=dremio-clients-demo.test.drem.io;" + "PORT=32010;" + "AuthenticationType=Plain;" + f"UID=improving;PWD={token};ssl=true;" + ) + + # Connect + try: + conn = pyodbc.connect(conn_str, autocommit=True) + except pyodbc.Error as e: + print(f"Failed to connect using driver '{args.driver}': {e}") + sys.exit(1) + + # UTF-8 decoding for CHAR columns + conn.setdecoding(pyodbc.SQL_CHAR, encoding="utf-8") + conn.setdecoding(pyodbc.SQL_WCHAR, encoding="utf-16le") + cursor = conn.cursor() + + # Run benchmark + timings = [] + rows_returned = [] + for i in range(args.iterations): + start = time.time() + cursor.execute(sql) + rows = cursor.fetchall() + elapsed_ms = (time.time() - start) * 1000 + timings.append(elapsed_ms) + rows_returned.append(len(rows)) + print(f"Iteration {i+1}: {elapsed_ms:.2f} ms (rows returned: {len(rows)})") + + avg_ms = statistics.mean(timings) + min_ms = min(timings) + max_ms = max(timings) + + result = { + "driver": args.driver, + "iterations": args.iterations, + "limit": args.limit, + "query": sql, + "avg_ms": avg_ms, + "min_ms": min_ms, + "max_ms": max_ms, + "all_runs_ms": timings, + "rows_returned": rows_returned, + } + + # JSON output + if args.json: + print(json.dumps(result, indent=2)) + + # CSV output + if args.csv: + with open(args.csv, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(result.keys()) + writer.writerow(result.values()) + + cursor.close() + conn.close() + +if __name__ == "__main__": + main() From 645cc716b5b36b9089402a49c73735288a4b3be3 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 28 Aug 2025 19:54:27 +0100 Subject: [PATCH 67/74] Change queries to use name for graph display purposes --- .../odbc/performance_tests/compare_queries_bar.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py index 776cbcbf693..b9c15bebd31 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -62,14 +62,13 @@ def main(): # Hardcoded queries # ---------------------------- queries = { - "query1": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 100', - "query2": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 1000', - "query3": 'SELECT passenger_count, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "query4": 'SELECT passenger_count, AVG(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "query5": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" WHERE fare_amount > 50 LIMIT 500', - "query6": 'SELECT passenger_count, SUM(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "query7": 'SELECT passenger_count, fare_amount, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count, fare_amount', - "query8": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" ORDER BY fare_amount DESC LIMIT 100' + "Limit": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 1000', + "AvgGroupBy": 'SELECT passenger_count, AVG(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "SumGroupBy": 'SELECT passenger_count, SUM(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "GreaterThan": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" WHERE fare_amount > 50 LIMIT 500', + "OrderBy": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" ORDER BY fare_amount DESC LIMIT 100', + "SingleCountGroupBy": 'SELECT passenger_count, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', + "MultiCountGroupBy": 'SELECT passenger_count, fare_amount, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count, fare_amount' } results = [] From 5f41acede27ca621e861cc1042dfb5f012722c7a Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 28 Aug 2025 20:05:00 +0100 Subject: [PATCH 68/74] Update to print out intermediate json results --- .../sql/odbc/performance_tests/compare_limits_plot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py index 3461f03b3cc..de314376fc6 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_limits_plot.py @@ -40,6 +40,11 @@ def run_driver(driver_name): json_end = output.rfind("}") + 1 json_text = output[json_start:json_end] data = json.loads(json_text) + + # --- PRINT INTERMEDIATE JSON RESULTS --- + print(f"Intermediate JSON for {driver_name} LIMIT={limit}:") + print(json.dumps(data, indent=2)) + avg_ms = data["avg_ms"] min_ms = data["min_ms"] max_ms = data["max_ms"] @@ -97,4 +102,3 @@ def run_driver(driver_name): plt.savefig(args.plotfile) print(f"Plot saved to {args.plotfile}") plt.show() - From 2095d894307e53d459107952f1106a9e702a4e7a Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 28 Aug 2025 20:20:47 +0100 Subject: [PATCH 69/74] Disable certificate verification --- .../arrow/flight/sql/odbc/performance_tests/table_read_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py index df248ef4445..39e821ec4ed 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -39,6 +39,7 @@ def main(): "PORT=32010;" "AuthenticationType=Plain;" f"UID=improving;PWD={token};ssl=true;" + "DisableCertificateVerification=1;" ) # Connect From ce42a9836bf111c7d92f455f18d222650136efda Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 2 Sep 2025 05:01:36 +0100 Subject: [PATCH 70/74] Update to use 20 varied queries --- .../performance_tests/compare_queries_bar.py | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py index b9c15bebd31..973bf1dfeaf 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -53,6 +53,7 @@ def main(): parser = argparse.ArgumentParser(description="Compare two ODBC Flight SQL drivers on multiple queries") parser.add_argument("--driver_a", required=True, help="Driver A name") parser.add_argument("--driver_b", required=True, help="Driver B name") + parser.add_argument("--table", required=True, help="Table name to query (with schema if needed)") parser.add_argument("--iterations", type=int, default=5, help="Number of iterations per query") parser.add_argument("--outfile", default="compare_results.csv", help="CSV output file") parser.add_argument("--plotfile", default="compare_plot.png", help="Plot output file") @@ -62,13 +63,42 @@ def main(): # Hardcoded queries # ---------------------------- queries = { - "Limit": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT 1000', - "AvgGroupBy": 'SELECT passenger_count, AVG(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "SumGroupBy": 'SELECT passenger_count, SUM(fare_amount) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "GreaterThan": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" WHERE fare_amount > 50 LIMIT 500', - "OrderBy": 'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" ORDER BY fare_amount DESC LIMIT 100', - "SingleCountGroupBy": 'SELECT passenger_count, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count', - "MultiCountGroupBy": 'SELECT passenger_count, fare_amount, COUNT(*) FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" GROUP BY passenger_count, fare_amount' + # Simple selects + "Limit100": f'SELECT * FROM "{args.table}" LIMIT 100', + "Limit1000": f'SELECT * FROM "{args.table}" LIMIT 1000', + + # Aggregations & Group By + "AvgFareByPassenger": f'SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM "{args.table}" GROUP BY passenger_count', + "SumFareByPassenger": f'SELECT passenger_count, SUM(fare_amount) AS total_fare FROM "{args.table}" GROUP BY passenger_count', + "CountByVendor": f'SELECT vendor_id, COUNT(*) AS num_trips FROM "{args.table}" GROUP BY vendor_id', + "TipStatsByPayment": f'SELECT payment_type, AVG(tip_amount) AS avg_tip, MAX(tip_amount) AS max_tip FROM "{args.table}" GROUP BY payment_type', + + # Filters + "FareGreater50": f'SELECT * FROM "{args.table}" WHERE fare_amount > 50 LIMIT 500', + "TripDistance5to10": f'SELECT * FROM "{args.table}" WHERE trip_distance BETWEEN 5 AND 10 LIMIT 500', + "PassengerCount2": f'SELECT * FROM "{args.table}" WHERE passenger_count = 2 LIMIT 500', + "CashPayments": f'SELECT * FROM "{args.table}" WHERE payment_type = \'CSH\' LIMIT 500', + + # Ordering + "OrderByFareDesc": f'SELECT * FROM "{args.table}" ORDER BY fare_amount DESC LIMIT 100', + "OrderByTripDistance": f'SELECT * FROM "{args.table}" ORDER BY trip_distance DESC LIMIT 100', + + # Distincts + "DistinctVendors": f'SELECT DISTINCT vendor_id FROM "{args.table}"', + "DistinctPayments": f'SELECT DISTINCT payment_type FROM "{args.table}"', + + # Aggregates without group by + "TotalRowCount": f'SELECT COUNT(*) AS total_trips FROM "{args.table}"', + "MaxFare": f'SELECT MAX(fare_amount) AS max_fare FROM "{args.table}"', + "MinFare": f'SELECT MIN(fare_amount) AS min_fare FROM "{args.table}"', + + # Date/time functions + "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) AS year, COUNT(*) AS trips FROM "{args.table}" GROUP BY year ORDER BY year', + "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) AS month, COUNT(*) AS trips FROM "{args.table}" GROUP BY month ORDER BY month', + + # Window functions + "TopFaresRanked": f'SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank FROM "{args.table}" LIMIT 100', + "AvgFareByPassengerWindow": f'SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM "{args.table}" LIMIT 500' } results = [] From 5d995ac2b79e1f63e4b4e6ad6f9c9a49dbc17f1f Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 2 Sep 2025 05:45:42 +0100 Subject: [PATCH 71/74] Update to take schema as command line arg --- .../performance_tests/compare_queries_bar.py | 105 ++++++------------ .../odbc/performance_tests/table_read_test.py | 10 +- 2 files changed, 46 insertions(+), 69 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py index 973bf1dfeaf..dabd07df869 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -5,17 +5,19 @@ import json import matplotlib.pyplot as plt -# ---------------------------- -# Helper function to run a query via table_read_test.py -# ---------------------------- -def run_query(driver, query, iterations, label): +def run_query(driver, schema, table, query, iterations, label): cmd = [ "python", "table_read_test.py", "--driver", driver, "--iterations", str(iterations), "--json", - "--query", query + "--table", table ] + if schema: + cmd += ["--schema", schema] + if query: + cmd += ["--query", query] + try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) full_output = [] @@ -46,70 +48,50 @@ def run_query(driver, query, iterations, label): print(f"Error running {label} for {driver}: {e}") return None -# ---------------------------- -# Main script -# ---------------------------- + def main(): parser = argparse.ArgumentParser(description="Compare two ODBC Flight SQL drivers on multiple queries") parser.add_argument("--driver_a", required=True, help="Driver A name") parser.add_argument("--driver_b", required=True, help="Driver B name") - parser.add_argument("--table", required=True, help="Table name to query (with schema if needed)") + parser.add_argument("--schema", help="Schema or space name") + parser.add_argument("--table", required=True, help="Table name") parser.add_argument("--iterations", type=int, default=5, help="Number of iterations per query") parser.add_argument("--outfile", default="compare_results.csv", help="CSV output file") parser.add_argument("--plotfile", default="compare_plot.png", help="Plot output file") args = parser.parse_args() - # ---------------------------- # Hardcoded queries - # ---------------------------- queries = { - # Simple selects - "Limit100": f'SELECT * FROM "{args.table}" LIMIT 100', - "Limit1000": f'SELECT * FROM "{args.table}" LIMIT 1000', - - # Aggregations & Group By - "AvgFareByPassenger": f'SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM "{args.table}" GROUP BY passenger_count', - "SumFareByPassenger": f'SELECT passenger_count, SUM(fare_amount) AS total_fare FROM "{args.table}" GROUP BY passenger_count', - "CountByVendor": f'SELECT vendor_id, COUNT(*) AS num_trips FROM "{args.table}" GROUP BY vendor_id', - "TipStatsByPayment": f'SELECT payment_type, AVG(tip_amount) AS avg_tip, MAX(tip_amount) AS max_tip FROM "{args.table}" GROUP BY payment_type', - - # Filters - "FareGreater50": f'SELECT * FROM "{args.table}" WHERE fare_amount > 50 LIMIT 500', - "TripDistance5to10": f'SELECT * FROM "{args.table}" WHERE trip_distance BETWEEN 5 AND 10 LIMIT 500', - "PassengerCount2": f'SELECT * FROM "{args.table}" WHERE passenger_count = 2 LIMIT 500', - "CashPayments": f'SELECT * FROM "{args.table}" WHERE payment_type = \'CSH\' LIMIT 500', - - # Ordering - "OrderByFareDesc": f'SELECT * FROM "{args.table}" ORDER BY fare_amount DESC LIMIT 100', - "OrderByTripDistance": f'SELECT * FROM "{args.table}" ORDER BY trip_distance DESC LIMIT 100', - - # Distincts - "DistinctVendors": f'SELECT DISTINCT vendor_id FROM "{args.table}"', - "DistinctPayments": f'SELECT DISTINCT payment_type FROM "{args.table}"', - - # Aggregates without group by - "TotalRowCount": f'SELECT COUNT(*) AS total_trips FROM "{args.table}"', - "MaxFare": f'SELECT MAX(fare_amount) AS max_fare FROM "{args.table}"', - "MinFare": f'SELECT MIN(fare_amount) AS min_fare FROM "{args.table}"', - - # Date/time functions - "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) AS year, COUNT(*) AS trips FROM "{args.table}" GROUP BY year ORDER BY year', - "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) AS month, COUNT(*) AS trips FROM "{args.table}" GROUP BY month ORDER BY month', - - # Window functions - "TopFaresRanked": f'SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank FROM "{args.table}" LIMIT 100', - "AvgFareByPassengerWindow": f'SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM "{args.table}" LIMIT 500' + "Limit100": None, + "Limit1000": None, + "AvgFareByPassenger": f'SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', + "SumFareByPassenger": f'SELECT passenger_count, SUM(fare_amount) AS total_fare FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', + "CountByVendor": f'SELECT vendor_id, COUNT(*) AS num_trips FROM "{args.schema}"."{args.table}" GROUP BY vendor_id', + "TipStatsByPayment": f'SELECT payment_type, AVG(tip_amount) AS avg_tip, MAX(tip_amount) AS max_tip FROM "{args.schema}"."{args.table}" GROUP BY payment_type', + "FareGreater50": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE fare_amount > 50 LIMIT 500', + "TripDistance5to10": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE trip_distance BETWEEN 5 AND 10 LIMIT 500', + "PassengerCount2": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE passenger_count = 2 LIMIT 500', + "CashPayments": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE payment_type = \'CSH\' LIMIT 500', + "OrderByFareDesc": f'SELECT * FROM "{args.schema}"."{args.table}" ORDER BY fare_amount DESC LIMIT 100', + "OrderByTripDistance": f'SELECT * FROM "{args.schema}"."{args.table}" ORDER BY trip_distance DESC LIMIT 100', + "DistinctVendors": f'SELECT DISTINCT vendor_id FROM "{args.schema}"."{args.table}"', + "DistinctPayments": f'SELECT DISTINCT payment_type FROM "{args.schema}"."{args.table}"', + "TotalRowCount": f'SELECT COUNT(*) AS total_trips FROM "{args.schema}"."{args.table}"', + "MaxFare": f'SELECT MAX(fare_amount) AS max_fare FROM "{args.schema}"."{args.table}"', + "MinFare": f'SELECT MIN(fare_amount) AS min_fare FROM "{args.schema}"."{args.table}"', + "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) AS year, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY year ORDER BY year', + "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) AS month, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY month ORDER BY month', + "TopFaresRanked": f'SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank FROM "{args.schema}"."{args.table}" LIMIT 100', + "AvgFareByPassengerWindow": f'SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM "{args.schema}"."{args.table}" LIMIT 500' } results = [] - # ---------------------------- - # Run all queries for both drivers - # ---------------------------- for label, query in queries.items(): print(f"\nRunning {label}...") - result_a = run_query(args.driver_a, query, args.iterations, label) - result_b = run_query(args.driver_b, query, args.iterations, label) + # If query is None, use table_read_test.py default LIMIT query + result_a = run_query(args.driver_a, args.schema, args.table, query, args.iterations, label) + result_b = run_query(args.driver_b, args.schema, args.table, query, args.iterations, label) row = {"query": label} if result_a: @@ -144,9 +126,7 @@ def main(): results.append(row) - # ---------------------------- - # Write results to CSV - # ---------------------------- + # Write CSV with open(args.outfile, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=[ "query", "driver_a", "a_avg", "a_min", "a_max", @@ -157,28 +137,20 @@ def main(): writer.writerow(row) print(f"\nResults written to {args.outfile}") - # ---------------------------- - # Plot results with error bars using driver color coding - # ---------------------------- + # Plot results queries_labels = [r["query"] for r in results] x = range(len(queries_labels)) width = 0.35 fig, ax = plt.subplots(figsize=(14, 6)) - driver_colors = { - "a": "#1f77b4", # Driver A: blue - "b": "#ff7f0e" # Driver B: orange - } + driver_colors = {"a": "#1f77b4", "b": "#ff7f0e"} for i, r in enumerate(results): - # Driver A if r["a_avg"] != "N/A": a_mean = r["a_avg"] a_err = [[a_mean - r["a_min"]], [r["a_max"] - a_mean]] ax.bar(i - width/2, a_mean, width, yerr=a_err, capsize=5, color=driver_colors["a"], alpha=0.8, label=args.driver_a if i == 0 else "") - - # Driver B if r["b_avg"] != "N/A": b_mean = r["b_avg"] b_err = [[b_mean - r["b_min"]], [r["b_max"] - b_mean]] @@ -189,9 +161,7 @@ def main(): ax.set_title("Query Performance Comparison: Driver A vs Driver B") ax.set_xticks(x) ax.set_xticklabels(queries_labels, rotation=45, ha="right") - ax.legend(title="Driver") - plt.tight_layout() plt.savefig(args.plotfile) print(f"Plot saved to {args.plotfile}") @@ -200,4 +170,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py index 39e821ec4ed..57db00ffc1f 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -13,6 +13,8 @@ def main(): description="Benchmark ODBC Flight SQL Driver query performance." ) parser.add_argument("--driver", required=True, help="ODBC driver name") + parser.add_argument("--schema", help="Schema or space name") + parser.add_argument("--table", required=True, help="Table name") parser.add_argument("--limit", type=int, help="LIMIT for SELECT query (ignored if --query provided)") parser.add_argument("--iterations", type=int, default=1, help="Number of iterations to run") parser.add_argument("--json", action="store_true", help="Output JSON") @@ -29,7 +31,10 @@ def main(): sql = args.query else: limit = args.limit if args.limit else 100 - sql = f'SELECT * FROM "Samples.samples.dremio.com"."NYC-taxi-trips-iceberg" LIMIT {limit}' + if args.schema: + sql = f'SELECT * FROM "{args.schema}"."{args.table}" LIMIT {limit}' + else: + sql = f'SELECT * FROM "{args.table}" LIMIT {limit}' # Connection string for Arrow Flight SQL ODBC conn_str = ( @@ -74,6 +79,8 @@ def main(): "driver": args.driver, "iterations": args.iterations, "limit": args.limit, + "schema": args.schema, + "table": args.table, "query": sql, "avg_ms": avg_ms, "min_ms": min_ms, @@ -96,5 +103,6 @@ def main(): cursor.close() conn.close() + if __name__ == "__main__": main() From 222611719ad32bdc6174e60c01bf1b6e16c5acca Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 2 Sep 2025 06:10:26 +0100 Subject: [PATCH 72/74] Fix incorrect table names in queries --- .../performance_tests/compare_queries_bar.py | 59 ++++++++++++------- .../odbc/performance_tests/table_read_test.py | 38 ++++++------ 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py index dabd07df869..c2f90ba11d7 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -9,15 +9,12 @@ def run_query(driver, schema, table, query, iterations, label): cmd = [ "python", "table_read_test.py", "--driver", driver, + "--schema", schema, + "--table", table, "--iterations", str(iterations), "--json", - "--table", table + "--query", query ] - if schema: - cmd += ["--schema", schema] - if query: - cmd += ["--query", query] - try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) full_output = [] @@ -48,39 +45,52 @@ def run_query(driver, schema, table, query, iterations, label): print(f"Error running {label} for {driver}: {e}") return None - def main(): parser = argparse.ArgumentParser(description="Compare two ODBC Flight SQL drivers on multiple queries") parser.add_argument("--driver_a", required=True, help="Driver A name") parser.add_argument("--driver_b", required=True, help="Driver B name") - parser.add_argument("--schema", help="Schema or space name") + parser.add_argument("--schema", required=True, help="Schema name") parser.add_argument("--table", required=True, help="Table name") parser.add_argument("--iterations", type=int, default=5, help="Number of iterations per query") parser.add_argument("--outfile", default="compare_results.csv", help="CSV output file") parser.add_argument("--plotfile", default="compare_plot.png", help="Plot output file") args = parser.parse_args() - # Hardcoded queries + # ---------------------------- + # 20 Queries + # ---------------------------- queries = { - "Limit100": None, - "Limit1000": None, + # Simple selects + "Limit100": f'SELECT * FROM "{args.schema}"."{args.table}" LIMIT 100', + "Limit1000": f'SELECT * FROM "{args.schema}"."{args.table}" LIMIT 1000', + + # Aggregations & Group By "AvgFareByPassenger": f'SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', "SumFareByPassenger": f'SELECT passenger_count, SUM(fare_amount) AS total_fare FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', - "CountByVendor": f'SELECT vendor_id, COUNT(*) AS num_trips FROM "{args.schema}"."{args.table}" GROUP BY vendor_id', - "TipStatsByPayment": f'SELECT payment_type, AVG(tip_amount) AS avg_tip, MAX(tip_amount) AS max_tip FROM "{args.schema}"."{args.table}" GROUP BY payment_type', + "AvgTipByPassenger": f'SELECT passenger_count, AVG(tip_amount) AS avg_tip FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', + "TotalAmountByPassenger": f'SELECT passenger_count, SUM(total_amount) AS total_amount FROM "{args.schema}"."{args.table}" GROUP BY passenger_count', + + # Filters "FareGreater50": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE fare_amount > 50 LIMIT 500', - "TripDistance5to10": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE trip_distance BETWEEN 5 AND 10 LIMIT 500', + "TripDistance5to10": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE trip_distance_mi BETWEEN 5 AND 10 LIMIT 500', "PassengerCount2": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE passenger_count = 2 LIMIT 500', - "CashPayments": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE payment_type = \'CSH\' LIMIT 500', + "TipGreater10": f'SELECT * FROM "{args.schema}"."{args.table}" WHERE tip_amount > 10 LIMIT 500', + + # Ordering "OrderByFareDesc": f'SELECT * FROM "{args.schema}"."{args.table}" ORDER BY fare_amount DESC LIMIT 100', - "OrderByTripDistance": f'SELECT * FROM "{args.schema}"."{args.table}" ORDER BY trip_distance DESC LIMIT 100', - "DistinctVendors": f'SELECT DISTINCT vendor_id FROM "{args.schema}"."{args.table}"', - "DistinctPayments": f'SELECT DISTINCT payment_type FROM "{args.schema}"."{args.table}"', + "OrderByTripDistance": f'SELECT * FROM "{args.schema}"."{args.table}" ORDER BY trip_distance_mi DESC LIMIT 100', + + # Aggregates without group by "TotalRowCount": f'SELECT COUNT(*) AS total_trips FROM "{args.schema}"."{args.table}"', "MaxFare": f'SELECT MAX(fare_amount) AS max_fare FROM "{args.schema}"."{args.table}"', "MinFare": f'SELECT MIN(fare_amount) AS min_fare FROM "{args.schema}"."{args.table}"', + "MaxTripDistance": f'SELECT MAX(trip_distance_mi) AS max_distance FROM "{args.schema}"."{args.table}"', + + # Date/time functions "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) AS year, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY year ORDER BY year', "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) AS month, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY month ORDER BY month', + + # Window functions "TopFaresRanked": f'SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank FROM "{args.schema}"."{args.table}" LIMIT 100', "AvgFareByPassengerWindow": f'SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM "{args.schema}"."{args.table}" LIMIT 500' } @@ -89,7 +99,6 @@ def main(): for label, query in queries.items(): print(f"\nRunning {label}...") - # If query is None, use table_read_test.py default LIMIT query result_a = run_query(args.driver_a, args.schema, args.table, query, args.iterations, label) result_b = run_query(args.driver_b, args.schema, args.table, query, args.iterations, label) @@ -126,7 +135,9 @@ def main(): results.append(row) - # Write CSV + # ---------------------------- + # Write results to CSV + # ---------------------------- with open(args.outfile, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=[ "query", "driver_a", "a_avg", "a_min", "a_max", @@ -137,13 +148,18 @@ def main(): writer.writerow(row) print(f"\nResults written to {args.outfile}") + # ---------------------------- # Plot results + # ---------------------------- queries_labels = [r["query"] for r in results] x = range(len(queries_labels)) width = 0.35 fig, ax = plt.subplots(figsize=(14, 6)) - driver_colors = {"a": "#1f77b4", "b": "#ff7f0e"} + driver_colors = { + "a": "#1f77b4", + "b": "#ff7f0e" + } for i, r in enumerate(results): if r["a_avg"] != "N/A": @@ -162,6 +178,7 @@ def main(): ax.set_xticks(x) ax.set_xticklabels(queries_labels, rotation=45, ha="right") ax.legend(title="Driver") + plt.tight_layout() plt.savefig(args.plotfile) print(f"Plot saved to {args.plotfile}") diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py index 57db00ffc1f..f75c82083ba 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/table_read_test.py @@ -4,16 +4,15 @@ import time import json import csv -from os import environ -import statistics import sys +from os import environ def main(): parser = argparse.ArgumentParser( description="Benchmark ODBC Flight SQL Driver query performance." ) parser.add_argument("--driver", required=True, help="ODBC driver name") - parser.add_argument("--schema", help="Schema or space name") + parser.add_argument("--schema", required=True, help="Schema name") parser.add_argument("--table", required=True, help="Table name") parser.add_argument("--limit", type=int, help="LIMIT for SELECT query (ignored if --query provided)") parser.add_argument("--iterations", type=int, default=1, help="Number of iterations to run") @@ -31,10 +30,7 @@ def main(): sql = args.query else: limit = args.limit if args.limit else 100 - if args.schema: - sql = f'SELECT * FROM "{args.schema}"."{args.table}" LIMIT {limit}' - else: - sql = f'SELECT * FROM "{args.table}" LIMIT {limit}' + sql = f'SELECT * FROM "{args.schema}"."{args.table}" LIMIT {limit}' # Connection string for Arrow Flight SQL ODBC conn_str = ( @@ -64,23 +60,28 @@ def main(): rows_returned = [] for i in range(args.iterations): start = time.time() - cursor.execute(sql) - rows = cursor.fetchall() - elapsed_ms = (time.time() - start) * 1000 - timings.append(elapsed_ms) - rows_returned.append(len(rows)) - print(f"Iteration {i+1}: {elapsed_ms:.2f} ms (rows returned: {len(rows)})") + try: + cursor.execute(sql) + rows = cursor.fetchall() + elapsed_ms = (time.time() - start) * 1000 + timings.append(elapsed_ms) + rows_returned.append(len(rows)) + print(f"Iteration {i+1}: {elapsed_ms:.2f} ms (rows returned: {len(rows)})") + except pyodbc.Error as e: + print(f"Query execution failed: {e}") + timings.append(None) + rows_returned.append(None) - avg_ms = statistics.mean(timings) - min_ms = min(timings) - max_ms = max(timings) + valid_timings = [t for t in timings if t is not None] + avg_ms = sum(valid_timings)/len(valid_timings) if valid_timings else None + min_ms = min(valid_timings) if valid_timings else None + max_ms = max(valid_timings) if valid_timings else None result = { "driver": args.driver, - "iterations": args.iterations, - "limit": args.limit, "schema": args.schema, "table": args.table, + "iterations": args.iterations, "query": sql, "avg_ms": avg_ms, "min_ms": min_ms, @@ -103,6 +104,5 @@ def main(): cursor.close() conn.close() - if __name__ == "__main__": main() From e6c645553ffd184fdfba6625c418b0306c5abcff Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 2 Sep 2025 07:23:24 +0100 Subject: [PATCH 73/74] Update to fix query syntax not supported by server --- .../sql/odbc/performance_tests/compare_queries_bar.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py index c2f90ba11d7..6bc009792ca 100644 --- a/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/compare_queries_bar.py @@ -87,11 +87,12 @@ def main(): "MaxTripDistance": f'SELECT MAX(trip_distance_mi) AS max_distance FROM "{args.schema}"."{args.table}"', # Date/time functions - "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) AS year, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY year ORDER BY year', - "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) AS month, COUNT(*) AS trips FROM "{args.schema}"."{args.table}" GROUP BY month ORDER BY month', + "TripsByYear": f'SELECT EXTRACT(YEAR FROM pickup_datetime) "year", COUNT(*) "trips" FROM "{args.schema}"."{args.table}" GROUP BY "year" ORDER BY "year"', + + "TripsByMonth": f'SELECT EXTRACT(MONTH FROM pickup_datetime) "month", COUNT(*) "trips" FROM "{args.schema}"."{args.table}" GROUP BY "month" ORDER BY "month"', # Window functions - "TopFaresRanked": f'SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank FROM "{args.schema}"."{args.table}" LIMIT 100', + "TopFaresRanked": f'SELECT fare_amount, rank_col FROM (SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank_col FROM "{args.schema}"."{args.table}") t LIMIT 100', "AvgFareByPassengerWindow": f'SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM "{args.schema}"."{args.table}" LIMIT 500' } From a1d37bca255a26d8898ac88eeeb27f36caa9e69a Mon Sep 17 00:00:00 2001 From: rscales Date: Tue, 2 Sep 2025 16:43:47 +0000 Subject: [PATCH 74/74] Add test run data --- .../CompareQueriesOutput.txt | 410 ++++++ .../001CompareQueries/queries_plot.png | Bin 0 -> 49203 bytes .../001CompareQueries/queries_results.csv | 8 + .../CompareLimitsOutput.txt | 315 +++++ .../limits_plot_to_100K.png | Bin 0 -> 39943 bytes .../limits_results_to_100K.csv | 13 + .../CompareQueriesOutput.txt | 1205 +++++++++++++++++ .../compare_20_queries.csv | 21 + .../results_20_queries.png | Bin 0 -> 74445 bytes 9 files changed, 1972 insertions(+) create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/CompareQueriesOutput.txt create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/queries_plot.png create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/queries_results.csv create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/002CompareLimits100K/CompareLimitsOutput.txt create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/002CompareLimits100K/limits_plot_to_100K.png create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/002CompareLimits100K/limits_results_to_100K.csv create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/CompareQueriesOutput.txt create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/compare_20_queries.csv create mode 100644 cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/results_20_queries.png diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/CompareQueriesOutput.txt b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/CompareQueriesOutput.txt new file mode 100644 index 00000000000..269667c25d6 --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/CompareQueriesOutput.txt @@ -0,0 +1,410 @@ +PS C:\Users\Administrator\GitHub\arrow\cpp\src\arrow\flight\sql\odbc\performance_tests> python compare_queries_bar.py --driver_a "Arrow Flight SQL ODBC Driver" --driver_b "Apache Arrow Flight SQL ODBC Driver" --iterations 5 --outfile compare_results.csv --plotfile compare_plot.png + +Running Limit... +Iteration 1: 34765.16 ms (rows returned: 1000) +Iteration 2: 33567.84 ms (rows returned: 1000) +Iteration 3: 33591.92 ms (rows returned: 1000) +Iteration 4: 33771.59 ms (rows returned: 1000) +Iteration 5: 33719.72 ms (rows returned: 1000) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 33883.24728012085, + "min_ms": 33567.84391403198, + "max_ms": 34765.1641368866, + "all_runs_ms": [ + 34765.1641368866, + 33567.84391403198, + 33591.91560745239, + 33771.59285545349, + 33719.719886779785 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} +Iteration 1: 36034.89 ms (rows returned: 1000) +Iteration 2: 31752.09 ms (rows returned: 1000) +Iteration 3: 35200.15 ms (rows returned: 1000) +Iteration 4: 34950.92 ms (rows returned: 1000) +Iteration 5: 35200.88 ms (rows returned: 1000) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 34627.785539627075, + "min_ms": 31752.08568572998, + "max_ms": 36034.88802909851, + "all_runs_ms": [ + 36034.88802909851, + 31752.08568572998, + 35200.14786720276, + 34950.92225074768, + 35200.883865356445 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} + +Running AvgGroupBy... +Iteration 1: 17156.91 ms (rows returned: 13) +Iteration 2: 16672.24 ms (rows returned: 13) +Iteration 3: 16697.97 ms (rows returned: 13) +Iteration 4: 16869.58 ms (rows returned: 13) +Iteration 5: 16784.30 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, AVG(fare_amount) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 16836.19828224182, + "min_ms": 16672.237634658813, + "max_ms": 17156.906366348267, + "all_runs_ms": [ + 17156.906366348267, + 16672.237634658813, + 16697.968244552612, + 16869.58408355713, + 16784.295082092285 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 16770.03 ms (rows returned: 13) +Iteration 2: 17078.22 ms (rows returned: 13) +Iteration 3: 16742.22 ms (rows returned: 13) +Iteration 4: 16740.14 ms (rows returned: 13) +Iteration 5: 16666.99 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, AVG(fare_amount) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 16799.520874023438, + "min_ms": 16666.994333267212, + "max_ms": 17078.22322845459, + "all_runs_ms": [ + 16770.0252532959, + 17078.22322845459, + 16742.223978042603, + 16740.137577056885, + 16666.994333267212 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running SumGroupBy... +Iteration 1: 16453.79 ms (rows returned: 13) +Iteration 2: 16430.96 ms (rows returned: 13) +Iteration 3: 16337.73 ms (rows returned: 13) +Iteration 4: 16466.14 ms (rows returned: 13) +Iteration 5: 16312.47 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, SUM(fare_amount) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 16400.220680236816, + "min_ms": 16312.472820281982, + "max_ms": 16466.143131256104, + "all_runs_ms": [ + 16453.79376411438, + 16430.964708328247, + 16337.72897720337, + 16466.143131256104, + 16312.472820281982 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 16365.31 ms (rows returned: 13) +Iteration 2: 16315.78 ms (rows returned: 13) +Iteration 3: 16266.77 ms (rows returned: 13) +Iteration 4: 16450.32 ms (rows returned: 13) +Iteration 5: 16380.79 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, SUM(fare_amount) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 16355.792951583862, + "min_ms": 16266.765356063843, + "max_ms": 16450.316429138184, + "all_runs_ms": [ + 16365.306615829468, + 16315.782070159912, + 16266.765356063843, + 16450.316429138184, + 16380.794286727905 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running GreaterThan... +Iteration 1: 16431.14 ms (rows returned: 500) +Iteration 2: 16581.48 ms (rows returned: 500) +Iteration 3: 17463.49 ms (rows returned: 500) +Iteration 4: 16401.79 ms (rows returned: 500) +Iteration 5: 16410.06 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE fare_amount > 50 LIMIT 500", + "avg_ms": 16657.591247558594, + "min_ms": 16401.78942680359, + "max_ms": 17463.488817214966, + "all_runs_ms": [ + 16431.142330169678, + 16581.480026245117, + 17463.488817214966, + 16401.78942680359, + 16410.05563735962 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 16492.02 ms (rows returned: 500) +Iteration 2: 16609.02 ms (rows returned: 500) +Iteration 3: 16921.74 ms (rows returned: 500) +Iteration 4: 19105.62 ms (rows returned: 500) +Iteration 5: 17268.11 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE fare_amount > 50 LIMIT 500", + "avg_ms": 17279.300785064697, + "min_ms": 16492.016792297363, + "max_ms": 19105.62229156494, + "all_runs_ms": [ + 16492.016792297363, + 16609.01641845703, + 16921.739101409912, + 19105.62229156494, + 17268.10932159424 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Running OrderBy... +Iteration 1: 53609.11 ms (rows returned: 100) +Iteration 2: 51697.37 ms (rows returned: 100) +Iteration 3: 52442.85 ms (rows returned: 100) +Iteration 4: 51915.98 ms (rows returned: 100) +Iteration 5: 52732.26 ms (rows returned: 100) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY fare_amount DESC LIMIT 100", + "avg_ms": 52479.51292991638, + "min_ms": 51697.365045547485, + "max_ms": 53609.11321640015, + "all_runs_ms": [ + 53609.11321640015, + 51697.365045547485, + 52442.848205566406, + 51915.97771644592, + 52732.26046562195 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} +Iteration 1: 51679.21 ms (rows returned: 100) +Iteration 2: 51655.93 ms (rows returned: 100) +Iteration 3: 52763.46 ms (rows returned: 100) +Iteration 4: 52017.11 ms (rows returned: 100) +Iteration 5: 51290.61 ms (rows returned: 100) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY fare_amount DESC LIMIT 100", + "avg_ms": 51881.261920928955, + "min_ms": 51290.605545043945, + "max_ms": 52763.455629348755, + "all_runs_ms": [ + 51679.210901260376, + 51655.92646598816, + 52763.455629348755, + 52017.11106300354, + 51290.605545043945 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running SingleCountGroupBy... +Iteration 1: 12628.46 ms (rows returned: 13) +Iteration 2: 11824.79 ms (rows returned: 13) +Iteration 3: 11797.74 ms (rows returned: 13) +Iteration 4: 11769.57 ms (rows returned: 13) +Iteration 5: 12707.33 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, COUNT(*) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 12145.577430725098, + "min_ms": 11769.570350646973, + "max_ms": 12707.332849502563, + "all_runs_ms": [ + 12628.455400466919, + 11824.785709381104, + 11797.74284362793, + 11769.570350646973, + 12707.332849502563 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 11914.47 ms (rows returned: 13) +Iteration 2: 11833.41 ms (rows returned: 13) +Iteration 3: 11851.74 ms (rows returned: 13) +Iteration 4: 11873.59 ms (rows returned: 13) +Iteration 5: 11799.07 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, COUNT(*) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 11854.455614089966, + "min_ms": 11799.065351486206, + "max_ms": 11914.469003677368, + "all_runs_ms": [ + 11914.469003677368, + 11833.407640457153, + 11851.743936538696, + 11873.592138290405, + 11799.065351486206 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running MultiCountGroupBy... +Iteration 1: 291068.74 ms (rows returned: 14545) +Iteration 2: 276341.55 ms (rows returned: 14545) +Iteration 3: 297687.92 ms (rows returned: 14545) +Iteration 4: 357626.56 ms (rows returned: 14545) +Iteration 5: 333281.98 ms (rows returned: 14545) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, fare_amount, COUNT(*) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count, fare_amount", + "avg_ms": 311201.34949684143, + "min_ms": 276341.55106544495, + "max_ms": 357626.5640258789, + "all_runs_ms": [ + 291068.74084472656, + 276341.55106544495, + 297687.9153251648, + 357626.5640258789, + 333281.97622299194 + ], + "rows_returned": [ + 14545, + 14545, + 14545, + 14545, + 14545 + ] +} +Iteration 1: 308960.54 ms (rows returned: 14545) +Iteration 2: 291849.08 ms (rows returned: 14545) +Iteration 3: 289264.62 ms (rows returned: 14545) +Iteration 4: 287014.85 ms (rows returned: 14545) +Iteration 5: 284922.38 ms (rows returned: 14545) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": null, + "query": "SELECT passenger_count, fare_amount, COUNT(*) FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count, fare_amount", + "avg_ms": 292402.29506492615, + "min_ms": 284922.3837852478, + "max_ms": 308960.5448246002, + "all_runs_ms": [ + 308960.5448246002, + 291849.0788936615, + 289264.61839675903, + 287014.8494243622, + 284922.3837852478 + ], + "rows_returned": [ + 14545, + 14545, + 14545, + 14545, + 14545 + ] +} + +Results written to compare_results.csv +Plot saved to compare_plot.png \ No newline at end of file diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/queries_plot.png b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/001CompareQueries/queries_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..888488f98f185bd1de0721553e2706b18d62116b GIT binary patch literal 49203 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�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 python compare_limits_plot.py --driver_a "Arrow Flight SQL ODBC Driver" --driver_b "Apache Arrow Flight SQL ODBC Driver" --iterations 5 --limits 1 10 100 1000 10000 100000 --outfile limits_results_to_100K.csv --plotfile limits_plot_to_100K.png + +Running Arrow Flight SQL ODBC Driver with LIMIT=1... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=1: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 1, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1", + "avg_ms": 363.750696182251, + "min_ms": 322.5698471069336, + "max_ms": 447.0713138580322, + "all_runs_ms": [ + 447.0713138580322, + 390.6853199005127, + 333.9381217956543, + 324.48887825012207, + 322.5698471069336 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running Arrow Flight SQL ODBC Driver with LIMIT=10... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=10: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 10, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 10", + "avg_ms": 611.6148471832275, + "min_ms": 562.8888607025146, + "max_ms": 669.5723533630371, + "all_runs_ms": [ + 562.8888607025146, + 628.2343864440918, + 669.5723533630371, + 595.0183868408203, + 602.3602485656738 + ], + "rows_returned": [ + 10, + 10, + 10, + 10, + 10 + ] +} + +Running Arrow Flight SQL ODBC Driver with LIMIT=100... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=100: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 100, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100", + "avg_ms": 3315.7440662384033, + "min_ms": 3253.4024715423584, + "max_ms": 3411.7939472198486, + "all_runs_ms": [ + 3253.4024715423584, + 3411.7939472198486, + 3285.139322280884, + 3296.3390350341797, + 3332.045555114746 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running Arrow Flight SQL ODBC Driver with LIMIT=1000... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=1000: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 1000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 30438.987493515015, + "min_ms": 29623.3491897583, + "max_ms": 31157.08327293396, + "all_runs_ms": [ + 31029.332160949707, + 29623.3491897583, + 31157.08327293396, + 29719.9490070343, + 30665.223836898804 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} + +Running Arrow Flight SQL ODBC Driver with LIMIT=10000... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=10000: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 10000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 10000", + "avg_ms": 300643.31245422363, + "min_ms": 290261.10315322876, + "max_ms": 306783.6825847626, + "all_runs_ms": [ + 302672.1565723419, + 300335.0327014923, + 303164.5872592926, + 290261.10315322876, + 306783.6825847626 + ], + "rows_returned": [ + 10000, + 10000, + 10000, + 10000, + 10000 + ] +} + +Running Arrow Flight SQL ODBC Driver with LIMIT=100000... +Intermediate JSON for Arrow Flight SQL ODBC Driver LIMIT=100000: +{ + "driver": "Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 100000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100000", + "avg_ms": 3036322.395181656, + "min_ms": 2996272.8984355927, + "max_ms": 3068238.5454177856, + "all_runs_ms": [ + 3038236.1080646515, + 3056891.6964530945, + 3068238.5454177856, + 3021972.727537155, + 2996272.8984355927 + ], + "rows_returned": [ + 100000, + 100000, + 100000, + 100000, + 100000 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=1... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=1: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 1, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1", + "avg_ms": 361.22307777404785, + "min_ms": 329.59818840026855, + "max_ms": 457.34381675720215, + "all_runs_ms": [ + 457.34381675720215, + 341.2652015686035, + 340.3174877166748, + 337.59069442749023, + 329.59818840026855 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=10... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=10: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 10, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 10", + "avg_ms": 609.0284824371338, + "min_ms": 596.207857131958, + "max_ms": 619.4398403167725, + "all_runs_ms": [ + 619.4398403167725, + 609.3175411224365, + 596.207857131958, + 615.5171394348145, + 604.6600341796875 + ], + "rows_returned": [ + 10, + 10, + 10, + 10, + 10 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=100... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=100: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 100, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100", + "avg_ms": 3322.842216491699, + "min_ms": 3236.36794090271, + "max_ms": 3433.699607849121, + "all_runs_ms": [ + 3396.2011337280273, + 3236.36794090271, + 3247.5204467773438, + 3300.421953201294, + 3433.699607849121 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=1000... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=1000: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 1000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 30824.00050163269, + "min_ms": 29915.04693031311, + "max_ms": 32180.66430091858, + "all_runs_ms": [ + 32180.66430091858, + 29915.04693031311, + 30911.401987075806, + 29925.776720046997, + 31187.11256980896 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=10000... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=10000: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 10000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 10000", + "avg_ms": 303659.582901001, + "min_ms": 300044.9962615967, + "max_ms": 309345.0765609741, + "all_runs_ms": [ + 309345.0765609741, + 304755.68890571594, + 300044.9962615967, + 301692.6624774933, + 302459.49029922485 + ], + "rows_returned": [ + 10000, + 10000, + 10000, + 10000, + 10000 + ] +} + +Running Apache Arrow Flight SQL ODBC Driver with LIMIT=100000... +Intermediate JSON for Apache Arrow Flight SQL ODBC Driver LIMIT=100000: +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "iterations": 5, + "limit": 100000, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100000", + "avg_ms": 3027773.0962753296, + "min_ms": 3013334.800004959, + "max_ms": 3070387.4740600586, + "all_runs_ms": [ + 3013334.800004959, + 3013481.8575382233, + 3024003.721475601, + 3070387.4740600586, + 3017657.628297806 + ], + "rows_returned": [ + 100000, + 100000, + 100000, + 100000, + 100000 + ] +} +Results saved to limits_results_to_100K.csv +Plot saved to limits_plot_to_100K.png \ No newline at end of file diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/002CompareLimits100K/limits_plot_to_100K.png b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/002CompareLimits100K/limits_plot_to_100K.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4bfb46a5ee09c06211312cec845ac8131b2bac GIT binary patch literal 39943 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 python compare_queries_bar.py --driver_a "Arrow Flight SQL ODBC Driver" --driver_b "Apache Arrow Flight SQL ODBC Driver" --schema "Samples.samples.dremio.com" --table "NYC-taxi-trips-iceberg" --iterations 5 --outfile compare_20_queries.csv --plotfile results_20_queries.png + +Running Limit100... +Iteration 1: 3258.13 ms (rows returned: 100) +Iteration 2: 3221.18 ms (rows returned: 100) +Iteration 3: 3090.82 ms (rows returned: 100) +Iteration 4: 3084.55 ms (rows returned: 100) +Iteration 5: 3089.00 ms (rows returned: 100) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100", + "avg_ms": 3148.7356185913086, + "min_ms": 3084.550380706787, + "max_ms": 3258.1324577331543, + "all_runs_ms": [ + 3258.1324577331543, + 3221.1766242980957, + 3090.8217430114746, + 3084.550380706787, + 3088.9968872070312 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} +Iteration 1: 3078.05 ms (rows returned: 100) +Iteration 2: 3135.37 ms (rows returned: 100) +Iteration 3: 3114.44 ms (rows returned: 100) +Iteration 4: 3110.02 ms (rows returned: 100) +Iteration 5: 3073.05 ms (rows returned: 100) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 100", + "avg_ms": 3102.1849632263184, + "min_ms": 3073.0504989624023, + "max_ms": 3135.3671550750732, + "all_runs_ms": [ + 3078.051805496216, + 3135.3671550750732, + 3114.4394874572754, + 3110.015869140625, + 3073.0504989624023 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running Limit1000... +Iteration 1: 28295.13 ms (rows returned: 1000) +Iteration 2: 28336.46 ms (rows returned: 1000) +Iteration 3: 28279.17 ms (rows returned: 1000) +Iteration 4: 28276.08 ms (rows returned: 1000) +Iteration 5: 28206.97 ms (rows returned: 1000) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 28278.762102127075, + "min_ms": 28206.9673538208, + "max_ms": 28336.461544036865, + "all_runs_ms": [ + 28295.12882232666, + 28336.461544036865, + 28279.168844223022, + 28276.083946228027, + 28206.9673538208 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} +Iteration 1: 28357.74 ms (rows returned: 1000) +Iteration 2: 28239.28 ms (rows returned: 1000) +Iteration 3: 28254.01 ms (rows returned: 1000) +Iteration 4: 28299.25 ms (rows returned: 1000) +Iteration 5: 28472.38 ms (rows returned: 1000) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 1000", + "avg_ms": 28324.5304107666, + "min_ms": 28239.282846450806, + "max_ms": 28472.376108169556, + "all_runs_ms": [ + 28357.738256454468, + 28239.282846450806, + 28254.00972366333, + 28299.24511909485, + 28472.376108169556 + ], + "rows_returned": [ + 1000, + 1000, + 1000, + 1000, + 1000 + ] +} + +Running AvgFareByPassenger... +Iteration 1: 18290.48 ms (rows returned: 13) +Iteration 2: 17481.93 ms (rows returned: 13) +Iteration 3: 17523.29 ms (rows returned: 13) +Iteration 4: 17411.47 ms (rows returned: 13) +Iteration 5: 17423.48 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17626.128387451172, + "min_ms": 17411.471366882324, + "max_ms": 18290.47703742981, + "all_runs_ms": [ + 18290.47703742981, + 17481.927156448364, + 17523.289442062378, + 17411.471366882324, + 17423.476934432983 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 17427.29 ms (rows returned: 13) +Iteration 2: 17412.03 ms (rows returned: 13) +Iteration 3: 17631.00 ms (rows returned: 13) +Iteration 4: 17411.21 ms (rows returned: 13) +Iteration 5: 17423.88 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(fare_amount) AS avg_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17461.08193397522, + "min_ms": 17411.21244430542, + "max_ms": 17630.999088287354, + "all_runs_ms": [ + 17427.2882938385, + 17412.02735900879, + 17630.999088287354, + 17411.21244430542, + 17423.882484436035 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running SumFareByPassenger... +Iteration 1: 17029.83 ms (rows returned: 13) +Iteration 2: 17219.08 ms (rows returned: 13) +Iteration 3: 17086.83 ms (rows returned: 13) +Iteration 4: 17048.02 ms (rows returned: 13) +Iteration 5: 17191.18 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, SUM(fare_amount) AS total_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17114.98770713806, + "min_ms": 17029.832124710083, + "max_ms": 17219.08450126648, + "all_runs_ms": [ + 17029.832124710083, + 17219.08450126648, + 17086.82870864868, + 17048.015594482422, + 17191.17760658264 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 16788.63 ms (rows returned: 13) +Iteration 2: 16720.72 ms (rows returned: 13) +Iteration 3: 16698.86 ms (rows returned: 13) +Iteration 4: 16762.84 ms (rows returned: 13) +Iteration 5: 16722.51 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, SUM(fare_amount) AS total_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 16738.711404800415, + "min_ms": 16698.858499526978, + "max_ms": 16788.630723953247, + "all_runs_ms": [ + 16788.630723953247, + 16720.719814300537, + 16698.858499526978, + 16762.84146308899, + 16722.506523132324 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running AvgTipByPassenger... +Iteration 1: 17996.39 ms (rows returned: 13) +Iteration 2: 17289.82 ms (rows returned: 13) +Iteration 3: 17209.62 ms (rows returned: 13) +Iteration 4: 17048.29 ms (rows returned: 13) +Iteration 5: 17133.30 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(tip_amount) AS avg_tip FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17335.482931137085, + "min_ms": 17048.28643798828, + "max_ms": 17996.389627456665, + "all_runs_ms": [ + 17996.389627456665, + 17289.820194244385, + 17209.61618423462, + 17048.28643798828, + 17133.302211761475 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 17100.37 ms (rows returned: 13) +Iteration 2: 17144.93 ms (rows returned: 13) +Iteration 3: 17263.51 ms (rows returned: 13) +Iteration 4: 17126.49 ms (rows returned: 13) +Iteration 5: 17094.25 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(tip_amount) AS avg_tip FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17145.911836624146, + "min_ms": 17094.25401687622, + "max_ms": 17263.51237297058, + "all_runs_ms": [ + 17100.367307662964, + 17144.932985305786, + 17263.51237297058, + 17126.492500305176, + 17094.25401687622 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running TotalAmountByPassenger... +Iteration 1: 18626.53 ms (rows returned: 13) +Iteration 2: 17192.35 ms (rows returned: 13) +Iteration 3: 17178.07 ms (rows returned: 13) +Iteration 4: 17203.18 ms (rows returned: 13) +Iteration 5: 17205.09 ms (rows returned: 13) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, SUM(total_amount) AS total_amount FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17481.04419708252, + "min_ms": 17178.06601524353, + "max_ms": 18626.532077789307, + "all_runs_ms": [ + 18626.532077789307, + 17192.352533340454, + 17178.06601524353, + 17203.18055152893, + 17205.089807510376 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} +Iteration 1: 17208.52 ms (rows returned: 13) +Iteration 2: 17161.80 ms (rows returned: 13) +Iteration 3: 17121.03 ms (rows returned: 13) +Iteration 4: 17118.08 ms (rows returned: 13) +Iteration 5: 17151.74 ms (rows returned: 13) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, SUM(total_amount) AS total_amount FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY passenger_count", + "avg_ms": 17152.232027053833, + "min_ms": 17118.079662322998, + "max_ms": 17208.51707458496, + "all_runs_ms": [ + 17208.51707458496, + 17161.800146102905, + 17121.02508544922, + 17118.079662322998, + 17151.738166809082 + ], + "rows_returned": [ + 13, + 13, + 13, + 13, + 13 + ] +} + +Running FareGreater50... +Iteration 1: 14541.40 ms (rows returned: 500) +Iteration 2: 14376.75 ms (rows returned: 500) +Iteration 3: 14375.08 ms (rows returned: 500) +Iteration 4: 14287.94 ms (rows returned: 500) +Iteration 5: 14298.50 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE fare_amount > 50 LIMIT 500", + "avg_ms": 14375.934505462646, + "min_ms": 14287.942886352539, + "max_ms": 14541.397094726562, + "all_runs_ms": [ + 14541.397094726562, + 14376.750469207764, + 14375.08249282837, + 14287.942886352539, + 14298.499584197998 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 14336.94 ms (rows returned: 500) +Iteration 2: 14282.82 ms (rows returned: 500) +Iteration 3: 14338.50 ms (rows returned: 500) +Iteration 4: 14307.37 ms (rows returned: 500) +Iteration 5: 14303.22 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE fare_amount > 50 LIMIT 500", + "avg_ms": 14313.771390914917, + "min_ms": 14282.81831741333, + "max_ms": 14338.501691818237, + "all_runs_ms": [ + 14336.944103240967, + 14282.81831741333, + 14338.501691818237, + 14307.369232177734, + 14303.223609924316 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Running TripDistance5to10... +Iteration 1: 14320.81 ms (rows returned: 500) +Iteration 2: 14312.74 ms (rows returned: 500) +Iteration 3: 14323.37 ms (rows returned: 500) +Iteration 4: 14342.22 ms (rows returned: 500) +Iteration 5: 14334.44 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE trip_distance_mi BETWEEN 5 AND 10 LIMIT 500", + "avg_ms": 14326.717376708984, + "min_ms": 14312.744140625, + "max_ms": 14342.220067977905, + "all_runs_ms": [ + 14320.80602645874, + 14312.744140625, + 14323.374509811401, + 14342.220067977905, + 14334.442138671875 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 14335.08 ms (rows returned: 500) +Iteration 2: 14351.85 ms (rows returned: 500) +Iteration 3: 14409.90 ms (rows returned: 500) +Iteration 4: 14369.25 ms (rows returned: 500) +Iteration 5: 14333.16 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE trip_distance_mi BETWEEN 5 AND 10 LIMIT 500", + "avg_ms": 14359.848976135254, + "min_ms": 14333.15634727478, + "max_ms": 14409.904479980469, + "all_runs_ms": [ + 14335.081577301025, + 14351.84907913208, + 14409.904479980469, + 14369.253396987915, + 14333.15634727478 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Running PassengerCount2... +Iteration 1: 14400.87 ms (rows returned: 500) +Iteration 2: 14330.71 ms (rows returned: 500) +Iteration 3: 14326.70 ms (rows returned: 500) +Iteration 4: 14347.51 ms (rows returned: 500) +Iteration 5: 14340.93 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE passenger_count = 2 LIMIT 500", + "avg_ms": 14349.34492111206, + "min_ms": 14326.699256896973, + "max_ms": 14400.872230529785, + "all_runs_ms": [ + 14400.872230529785, + 14330.707311630249, + 14326.699256896973, + 14347.514152526855, + 14340.93165397644 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 14389.24 ms (rows returned: 500) +Iteration 2: 14426.34 ms (rows returned: 500) +Iteration 3: 14356.14 ms (rows returned: 500) +Iteration 4: 14306.25 ms (rows returned: 500) +Iteration 5: 14299.65 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE passenger_count = 2 LIMIT 500", + "avg_ms": 14355.523681640625, + "min_ms": 14299.64542388916, + "max_ms": 14426.344871520996, + "all_runs_ms": [ + 14389.240503311157, + 14426.344871520996, + 14356.136083602905, + 14306.251525878906, + 14299.64542388916 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Running TipGreater10... +Iteration 1: 14351.80 ms (rows returned: 500) +Iteration 2: 14319.90 ms (rows returned: 500) +Iteration 3: 14294.81 ms (rows returned: 500) +Iteration 4: 14296.91 ms (rows returned: 500) +Iteration 5: 14397.65 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE tip_amount > 10 LIMIT 500", + "avg_ms": 14332.215118408203, + "min_ms": 14294.811964035034, + "max_ms": 14397.649049758911, + "all_runs_ms": [ + 14351.803302764893, + 14319.896459579468, + 14294.811964035034, + 14296.91481590271, + 14397.649049758911 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 14357.85 ms (rows returned: 500) +Iteration 2: 14396.18 ms (rows returned: 500) +Iteration 3: 14342.35 ms (rows returned: 500) +Iteration 4: 14400.37 ms (rows returned: 500) +Iteration 5: 14313.89 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" WHERE tip_amount > 10 LIMIT 500", + "avg_ms": 14362.126731872559, + "min_ms": 14313.886642456055, + "max_ms": 14400.365591049194, + "all_runs_ms": [ + 14357.853174209595, + 14396.17919921875, + 14342.3490524292, + 14400.365591049194, + 14313.886642456055 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Running OrderByFareDesc... +Iteration 1: 54312.90 ms (rows returned: 100) +Iteration 2: 53397.82 ms (rows returned: 100) +Iteration 3: 53488.77 ms (rows returned: 100) +Iteration 4: 53777.30 ms (rows returned: 100) +Iteration 5: 54228.48 ms (rows returned: 100) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY fare_amount DESC LIMIT 100", + "avg_ms": 53841.05463027954, + "min_ms": 53397.82190322876, + "max_ms": 54312.901735305786, + "all_runs_ms": [ + 54312.901735305786, + 53397.82190322876, + 53488.770961761475, + 53777.30345726013, + 54228.47509384155 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} +Iteration 1: 54100.68 ms (rows returned: 100) +Iteration 2: 53510.49 ms (rows returned: 100) +Iteration 3: 53884.82 ms (rows returned: 100) +Iteration 4: 53539.88 ms (rows returned: 100) +Iteration 5: 54196.78 ms (rows returned: 100) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY fare_amount DESC LIMIT 100", + "avg_ms": 53846.53010368347, + "min_ms": 53510.48803329468, + "max_ms": 54196.778535842896, + "all_runs_ms": [ + 54100.68130493164, + 53510.48803329468, + 53884.81903076172, + 53539.883613586426, + 54196.778535842896 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running OrderByTripDistance... +Iteration 1: 54123.02 ms (rows returned: 100) +Iteration 2: 54615.67 ms (rows returned: 100) +Iteration 3: 54261.92 ms (rows returned: 100) +Iteration 4: 53612.34 ms (rows returned: 100) +Iteration 5: 54194.30 ms (rows returned: 100) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY trip_distance_mi DESC LIMIT 100", + "avg_ms": 54161.45000457764, + "min_ms": 53612.33949661255, + "max_ms": 54615.665674209595, + "all_runs_ms": [ + 54123.01993370056, + 54615.665674209595, + 54261.92402839661, + 53612.33949661255, + 54194.30088996887 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} +Iteration 1: 53885.21 ms (rows returned: 100) +Iteration 2: 53874.51 ms (rows returned: 100) +Iteration 3: 53506.34 ms (rows returned: 100) +Iteration 4: 53779.29 ms (rows returned: 100) +Iteration 5: 53635.18 ms (rows returned: 100) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT * FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" ORDER BY trip_distance_mi DESC LIMIT 100", + "avg_ms": 53736.1065864563, + "min_ms": 53506.33525848389, + "max_ms": 53885.21337509155, + "all_runs_ms": [ + 53885.21337509155, + 53874.51457977295, + 53506.33525848389, + 53779.29162979126, + 53635.178089141846 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running TotalRowCount... +Iteration 1: 5569.19 ms (rows returned: 1) +Iteration 2: 4449.97 ms (rows returned: 1) +Iteration 3: 4016.96 ms (rows returned: 1) +Iteration 4: 4170.72 ms (rows returned: 1) +Iteration 5: 3826.20 ms (rows returned: 1) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT COUNT(*) AS total_trips FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 4406.608629226685, + "min_ms": 3826.195478439331, + "max_ms": 5569.1938400268555, + "all_runs_ms": [ + 5569.1938400268555, + 4449.97239112854, + 4016.963243484497, + 4170.718193054199, + 3826.195478439331 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} +Iteration 1: 3837.28 ms (rows returned: 1) +Iteration 2: 3836.22 ms (rows returned: 1) +Iteration 3: 3894.01 ms (rows returned: 1) +Iteration 4: 3891.32 ms (rows returned: 1) +Iteration 5: 3926.83 ms (rows returned: 1) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT COUNT(*) AS total_trips FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 3877.1294116973877, + "min_ms": 3836.2209796905518, + "max_ms": 3926.828145980835, + "all_runs_ms": [ + 3837.275981903076, + 3836.2209796905518, + 3894.005537033081, + 3891.3164138793945, + 3926.828145980835 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running MaxFare... +Iteration 1: 9391.77 ms (rows returned: 1) +Iteration 2: 9165.55 ms (rows returned: 1) +Iteration 3: 9127.03 ms (rows returned: 1) +Iteration 4: 9082.91 ms (rows returned: 1) +Iteration 5: 9111.72 ms (rows returned: 1) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MAX(fare_amount) AS max_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 9175.795316696167, + "min_ms": 9082.907915115356, + "max_ms": 9391.76869392395, + "all_runs_ms": [ + 9391.76869392395, + 9165.551900863647, + 9127.027034759521, + 9082.907915115356, + 9111.72103881836 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} +Iteration 1: 9277.34 ms (rows returned: 1) +Iteration 2: 9252.20 ms (rows returned: 1) +Iteration 3: 9093.64 ms (rows returned: 1) +Iteration 4: 9075.61 ms (rows returned: 1) +Iteration 5: 9082.72 ms (rows returned: 1) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MAX(fare_amount) AS max_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 9156.304168701172, + "min_ms": 9075.613260269165, + "max_ms": 9277.344703674316, + "all_runs_ms": [ + 9277.344703674316, + 9252.196550369263, + 9093.641996383667, + 9075.613260269165, + 9082.724332809448 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running MinFare... +Iteration 1: 9504.61 ms (rows returned: 1) +Iteration 2: 9677.66 ms (rows returned: 1) +Iteration 3: 9393.83 ms (rows returned: 1) +Iteration 4: 9294.61 ms (rows returned: 1) +Iteration 5: 9556.50 ms (rows returned: 1) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MIN(fare_amount) AS min_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 9485.442781448364, + "min_ms": 9294.612646102905, + "max_ms": 9677.664518356323, + "all_runs_ms": [ + 9504.612684249878, + 9677.664518356323, + 9393.826484680176, + 9294.612646102905, + 9556.497573852539 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} +Iteration 1: 9221.09 ms (rows returned: 1) +Iteration 2: 9285.73 ms (rows returned: 1) +Iteration 3: 9213.77 ms (rows returned: 1) +Iteration 4: 9380.58 ms (rows returned: 1) +Iteration 5: 9288.95 ms (rows returned: 1) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MIN(fare_amount) AS min_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 9278.022623062134, + "min_ms": 9213.771343231201, + "max_ms": 9380.575180053711, + "all_runs_ms": [ + 9221.086978912354, + 9285.732746124268, + 9213.771343231201, + 9380.575180053711, + 9288.946866989136 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running MaxTripDistance... +Iteration 1: 10542.72 ms (rows returned: 1) +Iteration 2: 10567.05 ms (rows returned: 1) +Iteration 3: 10554.66 ms (rows returned: 1) +Iteration 4: 10329.56 ms (rows returned: 1) +Iteration 5: 10375.75 ms (rows returned: 1) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MAX(trip_distance_mi) AS max_distance FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 10473.949575424194, + "min_ms": 10329.558372497559, + "max_ms": 10567.053079605103, + "all_runs_ms": [ + 10542.723655700684, + 10567.053079605103, + 10554.66103553772, + 10329.558372497559, + 10375.751733779907 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} +Iteration 1: 10339.24 ms (rows returned: 1) +Iteration 2: 10281.62 ms (rows returned: 1) +Iteration 3: 11202.08 ms (rows returned: 1) +Iteration 4: 10792.55 ms (rows returned: 1) +Iteration 5: 10545.28 ms (rows returned: 1) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT MAX(trip_distance_mi) AS max_distance FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\"", + "avg_ms": 10632.155895233154, + "min_ms": 10281.619787216187, + "max_ms": 11202.081203460693, + "all_runs_ms": [ + 10339.242935180664, + 10281.619787216187, + 11202.081203460693, + 10792.552709579468, + 10545.28284072876 + ], + "rows_returned": [ + 1, + 1, + 1, + 1, + 1 + ] +} + +Running TripsByYear... +Iteration 1: 24763.79 ms (rows returned: 2) +Iteration 2: 24189.83 ms (rows returned: 2) +Iteration 3: 24179.34 ms (rows returned: 2) +Iteration 4: 24288.34 ms (rows returned: 2) +Iteration 5: 24510.13 ms (rows returned: 2) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT EXTRACT(YEAR FROM pickup_datetime) \"year\", COUNT(*) \"trips\" FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY \"year\" ORDER BY \"year\"", + "avg_ms": 24386.285734176636, + "min_ms": 24179.344177246094, + "max_ms": 24763.78583908081, + "all_runs_ms": [ + 24763.78583908081, + 24189.826726913452, + 24179.344177246094, + 24288.337230682373, + 24510.13469696045 + ], + "rows_returned": [ + 2, + 2, + 2, + 2, + 2 + ] +} +Iteration 1: 24203.49 ms (rows returned: 2) +Iteration 2: 23960.76 ms (rows returned: 2) +Iteration 3: 24465.03 ms (rows returned: 2) +Iteration 4: 24695.98 ms (rows returned: 2) +Iteration 5: 24106.13 ms (rows returned: 2) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT EXTRACT(YEAR FROM pickup_datetime) \"year\", COUNT(*) \"trips\" FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY \"year\" ORDER BY \"year\"", + "avg_ms": 24286.277627944946, + "min_ms": 23960.764408111572, + "max_ms": 24695.979833602905, + "all_runs_ms": [ + 24203.487396240234, + 23960.764408111572, + 24465.02923965454, + 24695.979833602905, + 24106.12726211548 + ], + "rows_returned": [ + 2, + 2, + 2, + 2, + 2 + ] +} + +Running TripsByMonth... +Iteration 1: 24291.11 ms (rows returned: 12) +Iteration 2: 24134.81 ms (rows returned: 12) +Iteration 3: 24545.46 ms (rows returned: 12) +Iteration 4: 24278.44 ms (rows returned: 12) +Iteration 5: 24340.06 ms (rows returned: 12) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT EXTRACT(MONTH FROM pickup_datetime) \"month\", COUNT(*) \"trips\" FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY \"month\" ORDER BY \"month\"", + "avg_ms": 24317.972707748413, + "min_ms": 24134.80544090271, + "max_ms": 24545.45521736145, + "all_runs_ms": [ + 24291.110515594482, + 24134.80544090271, + 24545.45521736145, + 24278.4366607666, + 24340.05570411682 + ], + "rows_returned": [ + 12, + 12, + 12, + 12, + 12 + ] +} +Iteration 1: 24174.38 ms (rows returned: 12) +Iteration 2: 24303.72 ms (rows returned: 12) +Iteration 3: 24389.76 ms (rows returned: 12) +Iteration 4: 24270.72 ms (rows returned: 12) +Iteration 5: 24196.28 ms (rows returned: 12) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT EXTRACT(MONTH FROM pickup_datetime) \"month\", COUNT(*) \"trips\" FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" GROUP BY \"month\" ORDER BY \"month\"", + "avg_ms": 24266.971921920776, + "min_ms": 24174.379348754883, + "max_ms": 24389.7647857666, + "all_runs_ms": [ + 24174.379348754883, + 24303.71594429016, + 24389.7647857666, + 24270.715713500977, + 24196.28381729126 + ], + "rows_returned": [ + 12, + 12, + 12, + 12, + 12 + ] +} + +Running TopFaresRanked... +Iteration 1: 223123.71 ms (rows returned: 100) +Iteration 2: 225523.15 ms (rows returned: 100) +Iteration 3: 229951.83 ms (rows returned: 100) +Iteration 4: 222796.05 ms (rows returned: 100) +Iteration 5: 221088.02 ms (rows returned: 100) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT fare_amount, rank_col FROM (SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank_col FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\") t LIMIT 100", + "avg_ms": 224496.55227661133, + "min_ms": 221088.01984786987, + "max_ms": 229951.82847976685, + "all_runs_ms": [ + 223123.71277809143, + 225523.15068244934, + 229951.82847976685, + 222796.04959487915, + 221088.01984786987 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} +Iteration 1: 234645.46 ms (rows returned: 100) +Iteration 2: 224257.77 ms (rows returned: 100) +Iteration 3: 225608.64 ms (rows returned: 100) +Iteration 4: 221247.26 ms (rows returned: 100) +Iteration 5: 218252.25 ms (rows returned: 100) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT fare_amount, rank_col FROM (SELECT fare_amount, ROW_NUMBER() OVER (ORDER BY fare_amount DESC) AS rank_col FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\") t LIMIT 100", + "avg_ms": 224802.27527618408, + "min_ms": 218252.2463798523, + "max_ms": 234645.4563140869, + "all_runs_ms": [ + 234645.4563140869, + 224257.76839256287, + 225608.64353179932, + 221247.26176261902, + 218252.2463798523 + ], + "rows_returned": [ + 100, + 100, + 100, + 100, + 100 + ] +} + +Running AvgFareByPassengerWindow... +Iteration 1: 193259.05 ms (rows returned: 500) +Iteration 2: 192692.28 ms (rows returned: 500) +Iteration 3: 194490.74 ms (rows returned: 500) +Iteration 4: 191565.32 ms (rows returned: 500) +Iteration 5: 191919.91 ms (rows returned: 500) +{ + "driver": "Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 500", + "avg_ms": 192785.4600906372, + "min_ms": 191565.31715393066, + "max_ms": 194490.74482917786, + "all_runs_ms": [ + 193259.05179977417, + 192692.2812461853, + 194490.74482917786, + 191565.31715393066, + 191919.90542411804 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} +Iteration 1: 193995.94 ms (rows returned: 500) +Iteration 2: 191825.51 ms (rows returned: 500) +Iteration 3: 192794.67 ms (rows returned: 500) +Iteration 4: 192621.31 ms (rows returned: 500) +Iteration 5: 192819.00 ms (rows returned: 500) +{ + "driver": "Apache Arrow Flight SQL ODBC Driver", + "schema": "Samples.samples.dremio.com", + "table": "NYC-taxi-trips-iceberg", + "iterations": 5, + "query": "SELECT passenger_count, AVG(fare_amount) OVER (PARTITION BY passenger_count) AS avg_fare FROM \"Samples.samples.dremio.com\".\"NYC-taxi-trips-iceberg\" LIMIT 500", + "avg_ms": 192811.2874031067, + "min_ms": 191825.510263443, + "max_ms": 193995.94283103943, + "all_runs_ms": [ + 193995.94283103943, + 191825.510263443, + 192794.67058181763, + 192621.31428718567, + 192818.99905204773 + ], + "rows_returned": [ + 500, + 500, + 500, + 500, + 500 + ] +} + +Results written to compare_20_queries.csv +Plot saved to results_20_queries.png +PS C:\Users\Administrator\GitHub\arrow\cpp\src\arrow\flight\sql\odbc\performance_tests> \ No newline at end of file diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/compare_20_queries.csv b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/compare_20_queries.csv new file mode 100644 index 00000000000..c642f84467c --- /dev/null +++ b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/compare_20_queries.csv @@ -0,0 +1,21 @@ +query,driver_a,a_avg,a_min,a_max,driver_b,b_avg,b_min,b_max +Limit100,Arrow Flight SQL ODBC Driver,3148.7356185913086,3084.550380706787,3258.1324577331543,Apache Arrow Flight SQL ODBC Driver,3102.1849632263184,3073.0504989624023,3135.3671550750732 +Limit1000,Arrow Flight SQL ODBC Driver,28278.762102127075,28206.9673538208,28336.461544036865,Apache Arrow Flight SQL ODBC Driver,28324.5304107666,28239.282846450806,28472.376108169556 +AvgFareByPassenger,Arrow Flight SQL ODBC Driver,17626.128387451172,17411.471366882324,18290.47703742981,Apache Arrow Flight SQL ODBC Driver,17461.08193397522,17411.21244430542,17630.999088287354 +SumFareByPassenger,Arrow Flight SQL ODBC Driver,17114.98770713806,17029.832124710083,17219.08450126648,Apache Arrow Flight SQL ODBC Driver,16738.711404800415,16698.858499526978,16788.630723953247 +AvgTipByPassenger,Arrow Flight SQL ODBC Driver,17335.482931137085,17048.28643798828,17996.389627456665,Apache Arrow Flight SQL ODBC Driver,17145.911836624146,17094.25401687622,17263.51237297058 +TotalAmountByPassenger,Arrow Flight SQL ODBC Driver,17481.04419708252,17178.06601524353,18626.532077789307,Apache Arrow Flight SQL ODBC Driver,17152.232027053833,17118.079662322998,17208.51707458496 +FareGreater50,Arrow Flight SQL ODBC Driver,14375.934505462646,14287.942886352539,14541.397094726562,Apache Arrow Flight SQL ODBC Driver,14313.771390914917,14282.81831741333,14338.501691818237 +TripDistance5to10,Arrow Flight SQL ODBC Driver,14326.717376708984,14312.744140625,14342.220067977905,Apache Arrow Flight SQL ODBC Driver,14359.848976135254,14333.15634727478,14409.904479980469 +PassengerCount2,Arrow Flight SQL ODBC Driver,14349.34492111206,14326.699256896973,14400.872230529785,Apache Arrow Flight SQL ODBC Driver,14355.523681640625,14299.64542388916,14426.344871520996 +TipGreater10,Arrow Flight SQL ODBC Driver,14332.215118408203,14294.811964035034,14397.649049758911,Apache Arrow Flight SQL ODBC Driver,14362.126731872559,14313.886642456055,14400.365591049194 +OrderByFareDesc,Arrow Flight SQL ODBC Driver,53841.05463027954,53397.82190322876,54312.901735305786,Apache Arrow Flight SQL ODBC Driver,53846.53010368347,53510.48803329468,54196.778535842896 +OrderByTripDistance,Arrow Flight SQL ODBC Driver,54161.45000457764,53612.33949661255,54615.665674209595,Apache Arrow Flight SQL ODBC Driver,53736.1065864563,53506.33525848389,53885.21337509155 +TotalRowCount,Arrow Flight SQL ODBC Driver,4406.608629226685,3826.195478439331,5569.1938400268555,Apache Arrow Flight SQL ODBC Driver,3877.1294116973877,3836.2209796905518,3926.828145980835 +MaxFare,Arrow Flight SQL ODBC Driver,9175.795316696167,9082.907915115356,9391.76869392395,Apache Arrow Flight SQL ODBC Driver,9156.304168701172,9075.613260269165,9277.344703674316 +MinFare,Arrow Flight SQL ODBC Driver,9485.442781448364,9294.612646102905,9677.664518356323,Apache Arrow Flight SQL ODBC Driver,9278.022623062134,9213.771343231201,9380.575180053711 +MaxTripDistance,Arrow Flight SQL ODBC Driver,10473.949575424194,10329.558372497559,10567.053079605103,Apache Arrow Flight SQL ODBC Driver,10632.155895233154,10281.619787216187,11202.081203460693 +TripsByYear,Arrow Flight SQL ODBC Driver,24386.285734176636,24179.344177246094,24763.78583908081,Apache Arrow Flight SQL ODBC Driver,24286.277627944946,23960.764408111572,24695.979833602905 +TripsByMonth,Arrow Flight SQL ODBC Driver,24317.972707748413,24134.80544090271,24545.45521736145,Apache Arrow Flight SQL ODBC Driver,24266.971921920776,24174.379348754883,24389.7647857666 +TopFaresRanked,Arrow Flight SQL ODBC Driver,224496.55227661133,221088.01984786987,229951.82847976685,Apache Arrow Flight SQL ODBC Driver,224802.27527618408,218252.2463798523,234645.4563140869 +AvgFareByPassengerWindow,Arrow Flight SQL ODBC Driver,192785.4600906372,191565.31715393066,194490.74482917786,Apache Arrow Flight SQL ODBC Driver,192811.2874031067,191825.510263443,193995.94283103943 diff --git a/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/results_20_queries.png b/cpp/src/arrow/flight/sql/odbc/performance_tests/TEST_RUNS/003CompareQueries20/results_20_queries.png new file mode 100644 index 0000000000000000000000000000000000000000..242f2619d58981ac34fba54ae9f98d0fdf741252 GIT binary patch literal 74445 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

H1F@;RBqXwn^I%-T7Brg@M4<7p_M@83T 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 literal 0 HcmV?d00001