From bf8b6beba478d848c7235d400a9db48c8086654f Mon Sep 17 00:00:00 2001 From: Nishant Avasthi Date: Wed, 29 Apr 2026 12:51:17 +0530 Subject: [PATCH] feat(c/db2): add initial connection-only DB2 driver Introduce the IBM DB2 C/C++ driver skeleton with connection lifecycle support, option parsing, and connection-level error handling to establish a small, reviewable baseline. Add focused connection-flow tests and minimal build/docs wiring while deferring execution and metadata APIs to follow-up work. --- c/CMakeLists.txt | 10 + c/cmake_modules/DefineOptions.cmake | 1 + c/driver/db2/AdbcDriverDb2Config.cmake.in | 26 ++ c/driver/db2/CMakeLists.txt | 124 ++++++++ c/driver/db2/README.md | 56 ++++ c/driver/db2/adbc-driver-db2.pc.in | 27 ++ c/driver/db2/connection.cc | 60 ++++ c/driver/db2/connection.h | 61 ++++ c/driver/db2/database.cc | 160 ++++++++++ c/driver/db2/database.h | 79 +++++ c/driver/db2/db2.cc | 366 ++++++++++++++++++++++ c/driver/db2/db2_odbc.h | 31 ++ c/driver/db2/db2_test.cc | 357 +++++++++++++++++++++ c/driver/db2/error.cc | 116 +++++++ c/driver/db2/error.h | 43 +++ c/driver/db2/meson.build | 44 +++ c/driver/db2/statement.h | 44 +++ c/include/arrow-adbc/driver/db2.h | 71 +++++ ci/scripts/cpp_test.sh | 4 + docs/source/driver/db2.rst | 166 ++++++++++ docs/source/index.rst | 1 + 21 files changed, 1847 insertions(+) create mode 100644 c/driver/db2/AdbcDriverDb2Config.cmake.in create mode 100644 c/driver/db2/CMakeLists.txt create mode 100644 c/driver/db2/README.md create mode 100644 c/driver/db2/adbc-driver-db2.pc.in create mode 100644 c/driver/db2/connection.cc create mode 100644 c/driver/db2/connection.h create mode 100644 c/driver/db2/database.cc create mode 100644 c/driver/db2/database.h create mode 100644 c/driver/db2/db2.cc create mode 100644 c/driver/db2/db2_odbc.h create mode 100644 c/driver/db2/db2_test.cc create mode 100644 c/driver/db2/error.cc create mode 100644 c/driver/db2/error.h create mode 100644 c/driver/db2/meson.build create mode 100644 c/driver/db2/statement.h create mode 100644 c/include/arrow-adbc/driver/db2.h create mode 100644 docs/source/driver/db2.rst diff --git a/c/CMakeLists.txt b/c/CMakeLists.txt index d86615909a..22b09ed583 100644 --- a/c/CMakeLists.txt +++ b/c/CMakeLists.txt @@ -95,6 +95,12 @@ if(ADBC_DRIVER_SNOWFLAKE) add_subdirectory(driver/snowflake) endif() +if(ADBC_DRIVER_DB2) + install(FILES "${REPOSITORY_ROOT}/c/include/arrow-adbc/driver/db2.h" + DESTINATION include/arrow-adbc/driver) + add_subdirectory(driver/db2) +endif() + if(ADBC_INTEGRATION_DUCKDB) add_subdirectory(integration/duckdb) endif() @@ -145,6 +151,10 @@ LIBRARY=$" ${Python3_EXECUTABLE} -m pi if(ADBC_DRIVER_SNOWFLAKE) adbc_install_python_package(snowflake) endif() + + if(ADBC_DRIVER_DB2) + adbc_install_python_package(db2) + endif() endif() validate_config() diff --git a/c/cmake_modules/DefineOptions.cmake b/c/cmake_modules/DefineOptions.cmake index d620b43bff..f00403d9b3 100644 --- a/c/cmake_modules/DefineOptions.cmake +++ b/c/cmake_modules/DefineOptions.cmake @@ -229,6 +229,7 @@ if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") define_option(ADBC_DRIVER_POSTGRESQL "Build the PostgreSQL driver" OFF) define_option(ADBC_DRIVER_SQLITE "Build the SQLite driver" OFF) define_option(ADBC_DRIVER_SNOWFLAKE "Build the Snowflake driver" OFF) + define_option(ADBC_DRIVER_DB2 "Build the IBM DB2 driver" OFF) define_option(ADBC_INTEGRATION_DUCKDB "Build the test suite for DuckDB" OFF) diff --git a/c/driver/db2/AdbcDriverDb2Config.cmake.in b/c/driver/db2/AdbcDriverDb2Config.cmake.in new file mode 100644 index 0000000000..c4c9fa33b6 --- /dev/null +++ b/c/driver/db2/AdbcDriverDb2Config.cmake.in @@ -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. + +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +set(ADBC_VERSION "@ADBC_VERSION@") + +include("${CMAKE_CURRENT_LIST_DIR}/AdbcDriverDb2Targets.cmake") + +check_required_components(AdbcDriverDb2) diff --git a/c/driver/db2/CMakeLists.txt b/c/driver/db2/CMakeLists.txt new file mode 100644 index 0000000000..8c4b05b054 --- /dev/null +++ b/c/driver/db2/CMakeLists.txt @@ -0,0 +1,124 @@ +# 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. + +# --- Locate the IBM Db2 CLI driver -------------------------------------------- +# +# We rely on the CLI driver shipped with IBM Data Server Driver Package +# (sometimes called the Db2 "clidriver"), located either via DB2_HOME (set by +# IBM tooling), IBM_DB_HOME (set by `pip install ibm_db`), or the standard +# CMake variables. Generic ODBC libraries are intentionally not used as a +# fallback: Db2-specific behavior (extended SQLSTATEs, native error codes) +# requires the IBM CLI driver. + +set(DB2_HOME_HINTS "") +if(DEFINED ENV{DB2_HOME}) + list(APPEND DB2_HOME_HINTS "$ENV{DB2_HOME}") +endif() +if(DEFINED ENV{IBM_DB_HOME}) + list(APPEND DB2_HOME_HINTS "$ENV{IBM_DB_HOME}") +endif() + +find_path(DB2_INCLUDE_DIR + NAMES sqlcli1.h + HINTS ${DB2_HOME_HINTS} + PATH_SUFFIXES include + DOC "IBM Db2 CLI header directory (contains sqlcli1.h)") + +if(WIN32) + set(_db2_lib_names db2cli64 db2cli) +else() + set(_db2_lib_names db2) +endif() + +find_library(DB2_LIBRARY + NAMES ${_db2_lib_names} + HINTS ${DB2_HOME_HINTS} + PATH_SUFFIXES lib lib64 + DOC "IBM Db2 CLI library") + +if(NOT DB2_INCLUDE_DIR OR NOT DB2_LIBRARY) + message(FATAL_ERROR + "IBM Db2 CLI driver not found. Install the IBM Data Server Driver " + "Package (clidriver) and set DB2_HOME (or IBM_DB_HOME) to its root, " + "or set DB2_INCLUDE_DIR and DB2_LIBRARY directly.") +endif() + +message(STATUS "Db2 CLI include: ${DB2_INCLUDE_DIR}") +message(STATUS "Db2 CLI library: ${DB2_LIBRARY}") + +add_arrow_lib(adbc_driver_db2 + SOURCES + connection.cc + database.cc + db2.cc + error.cc + OUTPUTS + ADBC_LIBRARIES + CMAKE_PACKAGE_NAME + AdbcDriverDb2 + PKG_CONFIG_NAME + adbc-driver-db2 + SHARED_LINK_FLAGS + ${ADBC_LINK_FLAGS} + SHARED_LINK_LIBS + ${DB2_LIBRARY} + adbc_driver_common + adbc_driver_framework + STATIC_LINK_LIBS + ${DB2_LIBRARY} + adbc_driver_common + adbc_driver_framework) + +foreach(LIB_TARGET ${ADBC_LIBRARIES}) + target_compile_definitions(${LIB_TARGET} PRIVATE ADBC_EXPORTING) + target_include_directories(${LIB_TARGET} SYSTEM + PRIVATE ${REPOSITORY_ROOT}/c/ + ${REPOSITORY_ROOT}/c/include/ + ${DB2_INCLUDE_DIR} + ${REPOSITORY_ROOT}/c/driver) + + if(NOT ADBC_DEFINE_COMMON_ENTRYPOINTS) + target_compile_definitions(${LIB_TARGET} PRIVATE ${ADBC_TARGET_COMPILE_DEFINITIONS}) + endif() +endforeach() + +if(ADBC_TEST_LINKAGE STREQUAL "shared") + set(TEST_LINK_LIBS adbc_driver_db2_shared) +else() + set(TEST_LINK_LIBS adbc_driver_db2_static) +endif() + +if(ADBC_BUILD_TESTS) + add_test_case(driver_db2_test + PREFIX + adbc + EXTRA_LABELS + driver-db2 + SOURCES + db2_test.cc + EXTRA_LINK_LIBS + adbc_driver_common + adbc_validation + ${TEST_LINK_LIBS}) + target_compile_features(adbc-driver-db2-test PRIVATE cxx_std_17) + target_include_directories(adbc-driver-db2-test SYSTEM + PRIVATE ${REPOSITORY_ROOT}/c/ + ${REPOSITORY_ROOT}/c/include/ + ${DB2_INCLUDE_DIR} + ${REPOSITORY_ROOT}/c/driver) + adbc_configure_target(adbc-driver-db2-test) +endif() diff --git a/c/driver/db2/README.md b/c/driver/db2/README.md new file mode 100644 index 0000000000..fbfc87a58d --- /dev/null +++ b/c/driver/db2/README.md @@ -0,0 +1,56 @@ + + +# ADBC IBM Db2 Driver + +An ADBC driver for [IBM Db2 LUW](https://www.ibm.com/products/db2), +implemented on top of the Db2 CLI / ODBC API. + +> **Status:** Initial scope is connection management only. The driver +> can establish, configure, and tear down connections, and surfaces +> Db2 SQLSTATEs as ADBC error codes. Statement execution, metadata +> APIs, transactions, and bulk ingestion will be added in follow-up +> pull requests. + +## Building + +Requires the IBM Data Server Driver Package (the so-called +"clidriver"), which provides `sqlcli1.h` and `libdb2`. Generic ODBC +driver managers (unixODBC, iODBC) are not used as a fallback because +the driver relies on Db2-specific SQLSTATE classes for error mapping. + +Point CMake at the clidriver via `DB2_HOME` (set by IBM tooling) or +`IBM_DB_HOME` (set by `pip install ibm_db`): + +```bash +export DB2_HOME=/opt/ibm/db2/V11.5/clidriver # or IBM_DB_HOME=... +cmake -S c -B build -DADBC_DRIVER_DB2=ON +cmake --build build +``` + +## Testing + +The C++ test suite expects a reachable Db2 instance. Set +`ADBC_DB2_TEST_URI` to a Db2 CLI connection string and run: + +```bash +ctest --test-dir build -L driver-db2 --output-on-failure +``` + +When `ADBC_DB2_TEST_URI` is unset, all DB-bound tests skip. diff --git a/c/driver/db2/adbc-driver-db2.pc.in b/c/driver/db2/adbc-driver-db2.pc.in new file mode 100644 index 0000000000..74d785b95f --- /dev/null +++ b/c/driver/db2/adbc-driver-db2.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=@ADBC_PKG_CONFIG_INCLUDEDIR@ +libdir=@ADBC_PKG_CONFIG_LIBDIR@ + +Name: Apache Arrow Database Connectivity (ADBC) DB2 driver +Description: The ADBC DB2 driver provides an ADBC driver for IBM DB2. +URL: https://github.com/apache/arrow-adbc +Version: @ADBC_VERSION@ +Libs: -L${libdir} -ladbc_driver_db2 +Cflags: -I${includedir} diff --git a/c/driver/db2/connection.cc b/c/driver/db2/connection.cc new file mode 100644 index 0000000000..90d9d3114f --- /dev/null +++ b/c/driver/db2/connection.cc @@ -0,0 +1,60 @@ +// 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 "connection.h" + +#include "error.h" + +namespace adbc::db2 { + +Status Db2Connection::InitImpl(void* parent) { + database_ = reinterpret_cast(parent); + + UNWRAP_RESULT(hdbc_, database_->AllocConnection()); + + // SQLDriverConnect mutates the input string; copy to a stable buffer. + const std::string& conn_str = database_->connection_string(); + SQLCHAR out_str[1024]; + SQLSMALLINT out_len = 0; + SQLRETURN rc = SQLDriverConnect( + hdbc_, /*WindowHandle=*/nullptr, + const_cast(reinterpret_cast(conn_str.c_str())), + static_cast(conn_str.size()), out_str, sizeof(out_str), &out_len, + SQL_DRIVER_NOPROMPT); + UNWRAP_STATUS(CheckRc(SQL_HANDLE_DBC, hdbc_, rc, "SQLDriverConnect")); + + // ADBC's contract is autocommit-on by default; set it explicitly so the + // driver behavior is independent of the underlying CLI configuration. + rc = SQLSetConnectAttr(hdbc_, SQL_ATTR_AUTOCOMMIT, + reinterpret_cast(SQL_AUTOCOMMIT_ON), 0); + UNWRAP_STATUS(CheckRc(SQL_HANDLE_DBC, hdbc_, rc, "SQLSetConnectAttr(AUTOCOMMIT)")); + + return status::Ok(); +} + +Status Db2Connection::ReleaseImpl() { + if (hdbc_ != SQL_NULL_HDBC) { + SQLDisconnect(hdbc_); + if (database_ != nullptr) { + database_->FreeConnection(hdbc_); + } + hdbc_ = SQL_NULL_HDBC; + } + return status::Ok(); +} + +} // namespace adbc::db2 diff --git a/c/driver/db2/connection.h b/c/driver/db2/connection.h new file mode 100644 index 0000000000..0d28a0217f --- /dev/null +++ b/c/driver/db2/connection.h @@ -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. + +#pragma once + +#include + +#include + +#include "db2_odbc.h" + +#define ADBC_FRAMEWORK_USE_FMT +#include "driver/framework/connection.h" +#include "driver/framework/status.h" + +#include "database.h" + +namespace adbc::db2 { + +using driver::Option; +using driver::Result; +using driver::Status; +namespace status = adbc::driver::status; + +/// \brief ADBC connection for IBM Db2. +/// +/// Connection lifecycle wraps a single SQLHDBC obtained from the +/// owning Db2Database. Statement execution, metadata retrieval, and +/// transaction management are intentionally not implemented in this +/// initial driver scope and will be added in subsequent pull requests +/// (the framework default returns ADBC_STATUS_NOT_IMPLEMENTED). +class Db2Connection : public driver::Connection { + public: + [[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[DB2]"; + + Db2Connection() = default; + ~Db2Connection() = default; + + Status InitImpl(void* parent); + Status ReleaseImpl(); + + private: + Db2Database* database_ = nullptr; + SQLHDBC hdbc_ = SQL_NULL_HDBC; +}; + +} // namespace adbc::db2 diff --git a/c/driver/db2/database.cc b/c/driver/db2/database.cc new file mode 100644 index 0000000000..7d57c50009 --- /dev/null +++ b/c/driver/db2/database.cc @@ -0,0 +1,160 @@ +// 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 "database.h" + +#include +#include +#include +#include + +#include + +#include "error.h" + +namespace adbc::db2 { + +bool Db2Database::IsValidPort(std::string_view port) const { + if (port.empty()) return false; + int value = 0; + for (char c : port) { + if (!std::isdigit(static_cast(c))) return false; + value = value * 10 + (c - '0'); + if (value > 65535) return false; + } + return value > 0; +} + +void Db2Database::BuildConnectionString() { + // If the user supplied a complete CLI/ODBC connection string via "uri", + // honor it as-is and ignore the per-field options. + if (!conn_str_.empty()) return; + + std::ostringstream oss; + if (!database_.empty()) oss << "DATABASE=" << database_ << ";"; + if (!hostname_.empty()) oss << "HOSTNAME=" << hostname_ << ";"; + if (!port_.empty()) oss << "PORT=" << port_ << ";"; + if (!hostname_.empty() || !port_.empty()) oss << "PROTOCOL=TCPIP;"; + if (!uid_.empty()) oss << "UID=" << uid_ << ";"; + if (!pwd_.empty()) oss << "PWD=" << pwd_ << ";"; + conn_str_ = oss.str(); +} + +Status Db2Database::InitImpl() { + BuildConnectionString(); + + if (conn_str_.empty()) { + return status::InvalidArgument( + kErrorPrefix, + " No connection string provided. Set the standard 'uri' option, " + "or the individual options (", + ADBC_DB2_OPTION_DATABASE, ", ", ADBC_DB2_OPTION_HOSTNAME, ", ", + ADBC_DB2_OPTION_PORT, ", ", ADBC_DB2_OPTION_UID, ", ", + ADBC_DB2_OPTION_PWD, ")."); + } + + SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_); + if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { + return status::Internal(kErrorPrefix, + " Failed to allocate ODBC environment handle (rc=", rc, + ")"); + } + + rc = SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, + reinterpret_cast(SQL_OV_ODBC3), 0); + UNWRAP_STATUS(CheckRc(SQL_HANDLE_ENV, henv_, rc, "SQLSetEnvAttr(ODBC_VERSION)")); + + return status::Ok(); +} + +Status Db2Database::ReleaseImpl() { + if (henv_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, henv_); + henv_ = SQL_NULL_HENV; + } + return Base::ReleaseImpl(); +} + +Status Db2Database::SetOptionImpl(std::string_view key, Option value) { + if (key == "uri") { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + conn_str_.assign(v); + return status::Ok(); + } + if (key == ADBC_DB2_OPTION_DATABASE) { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + database_.assign(v); + return status::Ok(); + } + if (key == ADBC_DB2_OPTION_HOSTNAME) { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + hostname_.assign(v); + return status::Ok(); + } + if (key == ADBC_DB2_OPTION_PORT) { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + if (!IsValidPort(v)) { + return status::InvalidArgument( + kErrorPrefix, " Invalid value for ", ADBC_DB2_OPTION_PORT, + ". Expected an integer in range 1-65535."); + } + port_.assign(v); + return status::Ok(); + } + if (key == ADBC_DB2_OPTION_UID || key == "username") { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + if (v.empty()) { + return status::InvalidArgument(kErrorPrefix, " Empty username/UID is not allowed."); + } + uid_.assign(v); + return status::Ok(); + } + if (key == ADBC_DB2_OPTION_PWD || key == "password") { + std::string_view v; + UNWRAP_RESULT(v, value.AsString()); + if (v.empty()) { + return status::InvalidArgument(kErrorPrefix, " Empty password/PWD is not allowed."); + } + pwd_.assign(v); + return status::Ok(); + } + + return Base::SetOptionImpl(key, value); +} + +Result Db2Database::AllocConnection() { + std::lock_guard lock(mu_); + + SQLHDBC hdbc = SQL_NULL_HDBC; + SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &hdbc); + UNWRAP_STATUS(CheckRc(SQL_HANDLE_ENV, henv_, rc, "SQLAllocHandle(DBC)")); + return hdbc; +} + +void Db2Database::FreeConnection(SQLHDBC hdbc) { + std::lock_guard lock(mu_); + if (hdbc != SQL_NULL_HDBC) { + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + } +} + +} // namespace adbc::db2 diff --git a/c/driver/db2/database.h b/c/driver/db2/database.h new file mode 100644 index 0000000000..e3ffa6db80 --- /dev/null +++ b/c/driver/db2/database.h @@ -0,0 +1,79 @@ +// 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 + +#include +#include +#include + +#include + +#include "db2_odbc.h" + +#define ADBC_FRAMEWORK_USE_FMT +#include "driver/framework/database.h" +#include "driver/framework/status.h" + +namespace adbc::db2 { + +using driver::Option; +using driver::Result; +using driver::Status; +namespace status = adbc::driver::status; + +/// \brief ADBC database for IBM Db2. +/// +/// Owns the shared SQLHENV that all Db2Connection handles allocate +/// from. The connection string is built lazily from the user-provided +/// options on InitImpl(). +class Db2Database : public driver::Database { + public: + [[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[DB2]"; + + Db2Database() = default; + ~Db2Database() = default; + + Status InitImpl() override; + Status ReleaseImpl() override; + Status SetOptionImpl(std::string_view key, Option value) override; + + /// Allocate a new SQLHDBC from the environment handle. Thread-safe. + Result AllocConnection(); + + /// Free a previously allocated SQLHDBC. Thread-safe. + void FreeConnection(SQLHDBC hdbc); + + /// The fully-formed Db2 CLI connection string used by SQLDriverConnect. + const std::string& connection_string() const { return conn_str_; } + + private: + void BuildConnectionString(); + bool IsValidPort(std::string_view port) const; + + SQLHENV henv_ = SQL_NULL_HENV; + std::mutex mu_; + + std::string conn_str_; + std::string database_; + std::string hostname_; + std::string port_; + std::string uid_; + std::string pwd_; +}; + +} // namespace adbc::db2 diff --git a/c/driver/db2/db2.cc b/c/driver/db2/db2.cc new file mode 100644 index 0000000000..0b93e06222 --- /dev/null +++ b/c/driver/db2/db2.cc @@ -0,0 +1,366 @@ +// 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. + +// An ADBC driver for IBM Db2 LUW, using DB2 CLI / ODBC. + +#include +#include + +#define ADBC_FRAMEWORK_USE_FMT +#include "driver/framework/base_driver.h" + +#include "connection.h" +#include "database.h" +#include "statement.h" + +namespace adbc::db2 { + +using Db2Driver = driver::Driver; + +} // namespace adbc::db2 + +extern "C" { +#if !defined(ADBC_NO_COMMON_ENTRYPOINTS) + +AdbcStatusCode AdbcDatabaseGetOption(struct AdbcDatabase* database, const char* key, + char* value, size_t* length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOption<>(database, key, value, length, error); +} + +AdbcStatusCode AdbcDatabaseGetOptionBytes(struct AdbcDatabase* database, const char* key, + uint8_t* value, size_t* length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionBytes<>(database, key, value, length, error); +} + +AdbcStatusCode AdbcDatabaseGetOptionInt(struct AdbcDatabase* database, const char* key, + int64_t* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionInt<>(database, key, value, error); +} + +AdbcStatusCode AdbcDatabaseGetOptionDouble(struct AdbcDatabase* database, const char* key, + double* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionDouble<>(database, key, value, error); +} + +AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CDatabaseInit(database, error); +} + +AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase* database, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CNew<>(database, error); +} + +AdbcStatusCode AdbcDatabaseRelease(struct AdbcDatabase* database, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CRelease<>(database, error); +} + +AdbcStatusCode AdbcDatabaseSetOption(struct AdbcDatabase* database, const char* key, + const char* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOption<>(database, key, value, error); +} + +AdbcStatusCode AdbcDatabaseSetOptionBytes(struct AdbcDatabase* database, const char* key, + const uint8_t* value, size_t length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionBytes<>(database, key, value, length, error); +} + +AdbcStatusCode AdbcDatabaseSetOptionInt(struct AdbcDatabase* database, const char* key, + int64_t value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionInt<>(database, key, value, error); +} + +AdbcStatusCode AdbcDatabaseSetOptionDouble(struct AdbcDatabase* database, const char* key, + double value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionDouble<>(database, key, value, error); +} + +AdbcStatusCode AdbcConnectionCancel(struct AdbcConnection* connection, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionCancel(connection, error); +} + +AdbcStatusCode AdbcConnectionCommit(struct AdbcConnection* connection, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionCommit(connection, error); +} + +AdbcStatusCode AdbcConnectionGetInfo(struct AdbcConnection* connection, + const uint32_t* info_codes, + size_t info_codes_length, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetInfo(connection, info_codes, + info_codes_length, out, error); +} + +AdbcStatusCode AdbcConnectionGetObjects(struct AdbcConnection* connection, int depth, + const char* catalog, const char* db_schema, + const char* table_name, const char** table_types, + const char* column_name, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetObjects(connection, depth, catalog, + db_schema, table_name, table_types, + column_name, out, error); +} + +AdbcStatusCode AdbcConnectionGetOption(struct AdbcConnection* connection, const char* key, + char* value, size_t* length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOption<>(connection, key, value, length, error); +} + +AdbcStatusCode AdbcConnectionGetOptionBytes(struct AdbcConnection* connection, + const char* key, uint8_t* value, + size_t* length, struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionBytes<>(connection, key, value, length, error); +} + +AdbcStatusCode AdbcConnectionGetOptionInt(struct AdbcConnection* connection, + const char* key, int64_t* value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionInt<>(connection, key, value, error); +} + +AdbcStatusCode AdbcConnectionGetOptionDouble(struct AdbcConnection* connection, + const char* key, double* value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionDouble<>(connection, key, value, error); +} + +AdbcStatusCode AdbcConnectionGetStatistics(struct AdbcConnection* connection, + const char* catalog, const char* db_schema, + const char* table_name, char approximate, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetStatistics( + connection, catalog, db_schema, table_name, approximate, out, error); +} + +AdbcStatusCode AdbcConnectionGetStatisticNames(struct AdbcConnection* connection, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetStatisticNames(connection, out, error); +} + +AdbcStatusCode AdbcConnectionGetTableSchema(struct AdbcConnection* connection, + const char* catalog, const char* db_schema, + const char* table_name, + struct ArrowSchema* schema, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetTableSchema(connection, catalog, db_schema, + table_name, schema, error); +} + +AdbcStatusCode AdbcConnectionGetTableTypes(struct AdbcConnection* connection, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionGetTableTypes(connection, out, error); +} + +AdbcStatusCode AdbcConnectionInit(struct AdbcConnection* connection, + struct AdbcDatabase* database, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionInit(connection, database, error); +} + +AdbcStatusCode AdbcConnectionNew(struct AdbcConnection* connection, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CNew<>(connection, error); +} + +AdbcStatusCode AdbcConnectionReadPartition(struct AdbcConnection* connection, + const uint8_t* serialized_partition, + size_t serialized_length, + struct ArrowArrayStream* out, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionReadPartition( + connection, serialized_partition, serialized_length, out, error); +} + +AdbcStatusCode AdbcConnectionRelease(struct AdbcConnection* connection, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CRelease<>(connection, error); +} + +AdbcStatusCode AdbcConnectionRollback(struct AdbcConnection* connection, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CConnectionRollback(connection, error); +} + +AdbcStatusCode AdbcConnectionSetOption(struct AdbcConnection* connection, const char* key, + const char* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOption<>(connection, key, value, error); +} + +AdbcStatusCode AdbcConnectionSetOptionBytes(struct AdbcConnection* connection, + const char* key, const uint8_t* value, + size_t length, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionBytes<>(connection, key, value, length, error); +} + +AdbcStatusCode AdbcConnectionSetOptionInt(struct AdbcConnection* connection, + const char* key, int64_t value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionInt<>(connection, key, value, error); +} + +AdbcStatusCode AdbcConnectionSetOptionDouble(struct AdbcConnection* connection, + const char* key, double value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionDouble<>(connection, key, value, error); +} + +AdbcStatusCode AdbcStatementBind(struct AdbcStatement* statement, + struct ArrowArray* values, struct ArrowSchema* schema, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementBind(statement, values, schema, error); +} + +AdbcStatusCode AdbcStatementBindStream(struct AdbcStatement* statement, + struct ArrowArrayStream* stream, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementBindStream(statement, stream, error); +} + +AdbcStatusCode AdbcStatementCancel(struct AdbcStatement* statement, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementCancel(statement, error); +} + +AdbcStatusCode AdbcStatementExecutePartitions(struct AdbcStatement* statement, + struct ArrowSchema* schema, + struct AdbcPartitions* partitions, + int64_t* rows_affected, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementExecutePartitions( + statement, schema, partitions, rows_affected, error); +} + +AdbcStatusCode AdbcStatementExecuteQuery(struct AdbcStatement* statement, + struct ArrowArrayStream* output, + int64_t* rows_affected, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementExecuteQuery(statement, output, rows_affected, + error); +} + +AdbcStatusCode AdbcStatementExecuteSchema(struct AdbcStatement* statement, + struct ArrowSchema* schema, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementExecuteSchema(statement, schema, error); +} + +AdbcStatusCode AdbcStatementGetOption(struct AdbcStatement* statement, const char* key, + char* value, size_t* length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOption<>(statement, key, value, length, error); +} + +AdbcStatusCode AdbcStatementGetOptionBytes(struct AdbcStatement* statement, + const char* key, uint8_t* value, + size_t* length, struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionBytes<>(statement, key, value, length, error); +} + +AdbcStatusCode AdbcStatementGetOptionInt(struct AdbcStatement* statement, const char* key, + int64_t* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionInt<>(statement, key, value, error); +} + +AdbcStatusCode AdbcStatementGetOptionDouble(struct AdbcStatement* statement, + const char* key, double* value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CGetOptionDouble<>(statement, key, value, error); +} + +AdbcStatusCode AdbcStatementGetParameterSchema(struct AdbcStatement* statement, + struct ArrowSchema* schema, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementGetParameterSchema(statement, schema, error); +} + +AdbcStatusCode AdbcStatementNew(struct AdbcConnection* connection, + struct AdbcStatement* statement, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementNew(connection, statement, error); +} + +AdbcStatusCode AdbcStatementPrepare(struct AdbcStatement* statement, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementPrepare(statement, error); +} + +AdbcStatusCode AdbcStatementRelease(struct AdbcStatement* statement, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CRelease<>(statement, error); +} + +AdbcStatusCode AdbcStatementSetOption(struct AdbcStatement* statement, const char* key, + const char* value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOption<>(statement, key, value, error); +} + +AdbcStatusCode AdbcStatementSetOptionBytes(struct AdbcStatement* statement, + const char* key, const uint8_t* value, + size_t length, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionBytes<>(statement, key, value, length, error); +} + +AdbcStatusCode AdbcStatementSetOptionInt(struct AdbcStatement* statement, const char* key, + int64_t value, struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionInt<>(statement, key, value, error); +} + +AdbcStatusCode AdbcStatementSetOptionDouble(struct AdbcStatement* statement, + const char* key, double value, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CSetOptionDouble<>(statement, key, value, error); +} + +AdbcStatusCode AdbcStatementSetSqlQuery(struct AdbcStatement* statement, + const char* query, struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementSetSqlQuery(statement, query, error); +} + +AdbcStatusCode AdbcStatementSetSubstraitPlan(struct AdbcStatement* statement, + const uint8_t* plan, size_t length, + struct AdbcError* error) { + return adbc::db2::Db2Driver::CStatementSetSubstraitPlan(statement, plan, length, error); +} + +[[maybe_unused]] ADBC_EXPORT AdbcStatusCode AdbcDriverInit(int version, + void* raw_driver, + AdbcError* error) { + return adbc::db2::Db2Driver::Init(version, raw_driver, error); +} + +#endif // ADBC_NO_COMMON_ENTRYPOINTS + +[[maybe_unused]] ADBC_EXPORT AdbcStatusCode AdbcDriverDb2Init(int version, + void* raw_driver, + AdbcError* error) { + return adbc::db2::Db2Driver::Init(version, raw_driver, error); +} + +} // extern "C" diff --git a/c/driver/db2/db2_odbc.h b/c/driver/db2/db2_odbc.h new file mode 100644 index 0000000000..228718e700 --- /dev/null +++ b/c/driver/db2/db2_odbc.h @@ -0,0 +1,31 @@ +// 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 + +// Unified include for DB2 CLI / ODBC headers. +// When building against the DB2 CLI driver, sqlcli1.h is the master +// header that defines all types and includes sqlext.h transitively. +// When building against generic ODBC (unixODBC/iODBC), sql.h + +// sqlext.h provide the equivalent definitions. + +#if __has_include("sqlcli1.h") +#include +#else +#include +#include +#endif diff --git a/c/driver/db2/db2_test.cc b/c/driver/db2/db2_test.cc new file mode 100644 index 0000000000..9d6e0f9195 --- /dev/null +++ b/c/driver/db2/db2_test.cc @@ -0,0 +1,357 @@ +// 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 + +#include "validation/adbc_validation.h" +#include "validation/adbc_validation_util.h" + +using adbc_validation::IsOkStatus; +using adbc_validation::IsStatus; + +namespace { + +// Read the connection URI from the environment. When unset, all +// tests that need a live Db2 server skip. +const char* GetTestUri() { + const char* uri = std::getenv("ADBC_DB2_TEST_URI"); + return (uri != nullptr && uri[0] != '\0') ? uri : nullptr; +} + +class Db2Quirks : public adbc_validation::DriverQuirks { + public: + AdbcStatusCode SetupDatabase(struct AdbcDatabase* database, + struct AdbcError* error) const override { + const char* uri = GetTestUri(); + if (uri == nullptr) return ADBC_STATUS_INVALID_STATE; + return AdbcDatabaseSetOption(database, "uri", uri, error); + } + + // The driver currently scopes itself to connection management; the + // following capabilities are intentionally disabled until follow-up + // PRs add execution, transactions, ingestion, and metadata. + bool supports_bulk_ingest(const char* /*mode*/) const override { return false; } + bool supports_concurrent_statements() const override { return false; } + bool supports_transactions() const override { return false; } + bool supports_get_sql_info() const override { return false; } + bool supports_get_objects() const override { return false; } + bool supports_metadata_current_catalog() const override { return false; } + bool supports_metadata_current_db_schema() const override { return false; } + bool supports_partitioned_data() const override { return false; } + bool supports_dynamic_parameter_binding() const override { return false; } + bool supports_error_on_incompatible_schema() const override { return false; } + bool supports_ingest_view_types() const override { return false; } + bool supports_ingest_float16() const override { return false; } +}; + +} // namespace + +// --------------------------------------------------------------------------- +// AdbcDatabase lifecycle (matches the standard ADBCV_TEST_DATABASE coverage) +// --------------------------------------------------------------------------- + +class Db2DatabaseTest : public ::testing::Test, public adbc_validation::DatabaseTest { + public: + const adbc_validation::DriverQuirks* quirks() const override { return &quirks_; } + void SetUp() override { + if (GetTestUri() == nullptr) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI not set"; + } + ASSERT_NO_FATAL_FAILURE(SetUpTest()); + } + void TearDown() override { + if (GetTestUri() != nullptr) ASSERT_NO_FATAL_FAILURE(TearDownTest()); + } + + protected: + Db2Quirks quirks_; +}; +ADBCV_TEST_DATABASE(Db2DatabaseTest) + +// --------------------------------------------------------------------------- +// AdbcConnection lifecycle +// +// We deliberately do not invoke ADBCV_TEST_CONNECTION here because the bulk +// of those tests exercise metadata APIs (GetObjects/GetTableSchema/...) and +// transactions that are out of scope for this initial driver. We instead +// reuse the framework's connection-flow helpers directly so behavior stays +// in lock-step with the other ADBC drivers. +// --------------------------------------------------------------------------- + +class Db2ConnectionFlowTest : public ::testing::Test, + public adbc_validation::ConnectionTest { + public: + const adbc_validation::DriverQuirks* quirks() const override { return &quirks_; } + void SetUp() override { + if (GetTestUri() == nullptr) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI not set"; + } + ASSERT_NO_FATAL_FAILURE(SetUpTest()); + } + void TearDown() override { + if (GetTestUri() != nullptr) ASSERT_NO_FATAL_FAILURE(TearDownTest()); + } + + protected: + Db2Quirks quirks_; +}; + +TEST_F(Db2ConnectionFlowTest, NewInit) { TestNewInit(); } +TEST_F(Db2ConnectionFlowTest, Release) { TestRelease(); } +TEST_F(Db2ConnectionFlowTest, Concurrent) { TestConcurrent(); } +TEST_F(Db2ConnectionFlowTest, AutocommitDefault) { TestAutocommitDefault(); } + +// --------------------------------------------------------------------------- +// Db2-specific connection-management tests (option parsing, error mapping) +// --------------------------------------------------------------------------- + +class Db2ConnectionOptionsTest : public ::testing::Test { + protected: + void SetUp() override { + if (GetTestUri() == nullptr) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI not set"; + } + std::memset(&error_, 0, sizeof(error_)); + std::memset(&database_, 0, sizeof(database_)); + } + + void TearDown() override { + if (database_.private_data != nullptr) { + AdbcDatabaseRelease(&database_, &error_); + } + if (error_.release) error_.release(&error_); + } + + struct AdbcError error_; + struct AdbcDatabase database_; +}; + +TEST_F(Db2ConnectionOptionsTest, MissingConnectionStringFailsCleanly) { + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + // No "uri" and no per-field options have been set. + ASSERT_THAT(AdbcDatabaseInit(&database_, &error_), + IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error_)); +} + +TEST_F(Db2ConnectionOptionsTest, UnknownOptionIsRejected) { + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + // Unknown option must surface as NOT_IMPLEMENTED for forward compat, + // matching the ADBC framework default. + ASSERT_THAT( + AdbcDatabaseSetOption(&database_, "adbc.db2.does_not_exist", "x", &error_), + IsStatus(ADBC_STATUS_NOT_IMPLEMENTED, &error_)); +} + +TEST_F(Db2ConnectionOptionsTest, InvalidCredentialsAreRejected) { + // Reuse the connection coordinates from ADBC_DB2_TEST_URI but + // substitute an obviously-invalid password. The connect must fail + // with an authentication-class status (Db2 SQLSTATE 28xxx maps to + // UNAUTHENTICATED; some installs report 08001, which maps to IO). + const std::string uri = GetTestUri(); + auto extract = [&](const std::string& key) -> std::string { + std::string needle = key + "="; + auto pos = uri.find(needle); + if (pos == std::string::npos) return ""; + auto end = uri.find(';', pos); + return uri.substr(pos + needle.size(), + (end == std::string::npos) ? std::string::npos + : end - pos - needle.size()); + }; + const std::string database = extract("DATABASE"); + const std::string hostname = extract("HOSTNAME"); + const std::string port = extract("PORT"); + const std::string uid = extract("UID"); + if (database.empty() || hostname.empty() || port.empty() || uid.empty()) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI does not expose all of " + "DATABASE/HOSTNAME/PORT/UID; skipping credential test"; + } + + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_DATABASE, + database.c_str(), &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_HOSTNAME, + hostname.c_str(), &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PORT, port.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_UID, uid.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PWD, + "definitely-not-the-password", &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseInit(&database_, &error_), IsOkStatus(&error_)); + + struct AdbcConnection connection; + std::memset(&connection, 0, sizeof(connection)); + ASSERT_THAT(AdbcConnectionNew(&connection, &error_), IsOkStatus(&error_)); + AdbcStatusCode rc = AdbcConnectionInit(&connection, &database_, &error_); + EXPECT_THAT(rc, ::testing::AnyOf(::testing::Eq(ADBC_STATUS_UNAUTHENTICATED), + ::testing::Eq(ADBC_STATUS_IO))); + AdbcConnectionRelease(&connection, &error_); +} + +TEST_F(Db2ConnectionOptionsTest, IndividualOptionsBuildConnectionString) { + // Verify that the per-field options compose into a usable connection + // string by parsing the values out of the test URI. + const std::string uri = GetTestUri(); + auto extract = [&](const std::string& key) -> std::string { + std::string needle = key + "="; + auto pos = uri.find(needle); + if (pos == std::string::npos) return ""; + auto end = uri.find(';', pos); + return uri.substr(pos + needle.size(), + (end == std::string::npos) ? std::string::npos + : end - pos - needle.size()); + }; + const std::string database = extract("DATABASE"); + const std::string hostname = extract("HOSTNAME"); + const std::string port = extract("PORT"); + const std::string uid = extract("UID"); + const std::string pwd = extract("PWD"); + if (database.empty() || hostname.empty() || port.empty() || uid.empty() || + pwd.empty()) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI does not contain all of " + "DATABASE/HOSTNAME/PORT/UID/PWD; skipping field-style test"; + } + + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_DATABASE, + database.c_str(), &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_HOSTNAME, + hostname.c_str(), &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PORT, port.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_UID, uid.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PWD, pwd.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseInit(&database_, &error_), IsOkStatus(&error_)); + + struct AdbcConnection connection; + std::memset(&connection, 0, sizeof(connection)); + ASSERT_THAT(AdbcConnectionNew(&connection, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcConnectionInit(&connection, &database_, &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcConnectionRelease(&connection, &error_), IsOkStatus(&error_)); +} + +TEST_F(Db2ConnectionOptionsTest, UsernamePasswordSynonyms) { + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + // The standard "username"/"password" option names must be accepted + // as synonyms for adbc.db2.uid / adbc.db2.pwd. + ASSERT_THAT(AdbcDatabaseSetOption(&database_, "username", "ignored", &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, "password", "ignored", &error_), + IsOkStatus(&error_)); +} + +TEST_F(Db2ConnectionOptionsTest, UnreachablePortMapsToIoStatus) { + const std::string uri = GetTestUri(); + auto extract = [&](const std::string& key) -> std::string { + std::string needle = key + "="; + auto pos = uri.find(needle); + if (pos == std::string::npos) return ""; + auto end = uri.find(';', pos); + return uri.substr(pos + needle.size(), + (end == std::string::npos) ? std::string::npos + : end - pos - needle.size()); + }; + const std::string database = extract("DATABASE"); + const std::string uid = extract("UID"); + const std::string pwd = extract("PWD"); + if (database.empty() || uid.empty() || pwd.empty()) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI does not contain DATABASE/UID/PWD"; + } + + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_DATABASE, + database.c_str(), &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_HOSTNAME, "localhost", + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PORT, "1", &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_UID, uid.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PWD, pwd.c_str(), + &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseInit(&database_, &error_), IsOkStatus(&error_)); + + struct AdbcConnection connection; + std::memset(&connection, 0, sizeof(connection)); + ASSERT_THAT(AdbcConnectionNew(&connection, &error_), IsOkStatus(&error_)); + EXPECT_THAT(AdbcConnectionInit(&connection, &database_, &error_), + IsStatus(ADBC_STATUS_IO, &error_)); + AdbcConnectionRelease(&connection, &error_); +} + +TEST_F(Db2ConnectionOptionsTest, MalformedPortIsRejected) { + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + EXPECT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PORT, "fifty-thousand", + &error_), + IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error_)); +} + +TEST_F(Db2ConnectionOptionsTest, EmptyUidOrPwdIsRejected) { + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + EXPECT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_UID, "", &error_), + IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error_)); + EXPECT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PWD, "", &error_), + IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error_)); +} + +TEST_F(Db2ConnectionOptionsTest, UriTakesPrecedenceOverFieldOptions) { + const std::string uri = GetTestUri(); + if (uri.empty()) { + GTEST_SKIP() << "ADBC_DB2_TEST_URI not set"; + } + + ASSERT_THAT(AdbcDatabaseNew(&database_, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, "uri", uri.c_str(), &error_), + IsOkStatus(&error_)); + // Intentionally conflicting field options; these must be ignored when uri is set. + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_HOSTNAME, + "unreachable.invalid", &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseSetOption(&database_, ADBC_DB2_OPTION_PORT, "1", &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcDatabaseInit(&database_, &error_), IsOkStatus(&error_)); + + struct AdbcConnection connection; + std::memset(&connection, 0, sizeof(connection)); + ASSERT_THAT(AdbcConnectionNew(&connection, &error_), IsOkStatus(&error_)); + ASSERT_THAT(AdbcConnectionInit(&connection, &database_, &error_), + IsOkStatus(&error_)); + ASSERT_THAT(AdbcConnectionRelease(&connection, &error_), IsOkStatus(&error_)); +} diff --git a/c/driver/db2/error.cc b/c/driver/db2/error.cc new file mode 100644 index 0000000000..1bfbcbd5c5 --- /dev/null +++ b/c/driver/db2/error.cc @@ -0,0 +1,116 @@ +// 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 "error.h" + +#include +#include +#include + +namespace adbc::db2 { + +AdbcStatusCode SqlStateToAdbcStatus(const char* sqlstate) { + if (!sqlstate || sqlstate[0] == '\0') return ADBC_STATUS_IO; + + // Class 00 -- success + if (std::strncmp(sqlstate, "00", 2) == 0) return ADBC_STATUS_OK; + + // Class 08 -- connection exception + if (std::strncmp(sqlstate, "08", 2) == 0) return ADBC_STATUS_IO; + + // Class 22 -- data exception + if (std::strncmp(sqlstate, "22", 2) == 0) return ADBC_STATUS_INVALID_ARGUMENT; + + // Class 23 -- constraint violation + if (std::strncmp(sqlstate, "23", 2) == 0) return ADBC_STATUS_ALREADY_EXISTS; + + // Class 28 -- authorization + if (std::strncmp(sqlstate, "28", 2) == 0) return ADBC_STATUS_UNAUTHENTICATED; + + // Class 42 -- syntax / access rule violation + if (std::strncmp(sqlstate, "42", 2) == 0) { + if (std::strcmp(sqlstate, "42S02") == 0) return ADBC_STATUS_NOT_FOUND; + if (std::strcmp(sqlstate, "42704") == 0) return ADBC_STATUS_NOT_FOUND; + return ADBC_STATUS_INVALID_ARGUMENT; + } + + // Class HY -- CLI-specific errors + if (std::strncmp(sqlstate, "HY", 2) == 0) { + if (std::strcmp(sqlstate, "HY001") == 0) return ADBC_STATUS_INTERNAL; + if (std::strcmp(sqlstate, "HY008") == 0) return ADBC_STATUS_CANCELLED; + if (std::strcmp(sqlstate, "HY010") == 0) return ADBC_STATUS_INVALID_STATE; + return ADBC_STATUS_INVALID_ARGUMENT; + } + + // Class IM -- driver manager + if (std::strncmp(sqlstate, "IM", 2) == 0) return ADBC_STATUS_INTERNAL; + + return ADBC_STATUS_IO; +} + +Status Db2Error(SQLSMALLINT handle_type, SQLHANDLE handle, const char* context) { + std::ostringstream oss; + oss << "[DB2] " << context; + + SQLCHAR sqlstate[6]; + SQLINTEGER native_error = 0; + SQLCHAR msg_buf[SQL_MAX_MESSAGE_LENGTH + 1]; + SQLSMALLINT msg_len = 0; + SQLSMALLINT rec = 1; + AdbcStatusCode adbc_code = ADBC_STATUS_IO; + + std::string first_sqlstate; + SQLINTEGER first_native_error = 0; + + while (SQLGetDiagRec(handle_type, handle, rec, sqlstate, &native_error, msg_buf, + sizeof(msg_buf), &msg_len) == SQL_SUCCESS) { + sqlstate[5] = '\0'; + oss << " [" << reinterpret_cast(sqlstate) << "] " + << reinterpret_cast(msg_buf) << " (native error " << native_error + << ")"; + + if (rec == 1) { + first_sqlstate.assign(reinterpret_cast(sqlstate), 5); + first_native_error = native_error; + adbc_code = SqlStateToAdbcStatus(first_sqlstate.c_str()); + } + + rec++; + } + + if (rec == 1) { + oss << ": (no diagnostic records available)"; + } + + Status status(adbc_code, oss.str()); + if (!first_sqlstate.empty()) { + status.SetSqlState(first_sqlstate); + status.AddDetail("db2.sqlstate", first_sqlstate); + status.AddDetail("db2.native_error", std::to_string(first_native_error)); + } + return status; +} + +Status CheckRc(SQLSMALLINT handle_type, SQLHANDLE handle, SQLRETURN rc, + const char* context) { + if (rc == SQL_SUCCESS) return Status::Ok(); + if (rc == SQL_SUCCESS_WITH_INFO) return Status::Ok(); + if (rc == SQL_NO_DATA) return Status::Ok(); + return Db2Error(handle_type, handle, context); +} + +} // namespace adbc::db2 diff --git a/c/driver/db2/error.h b/c/driver/db2/error.h new file mode 100644 index 0000000000..7cd2a223bd --- /dev/null +++ b/c/driver/db2/error.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 + +#include + +#include "db2_odbc.h" + +#include "driver/framework/status.h" + +namespace adbc::db2 { + +using driver::Status; + +/// Extract all diagnostic records from a DB2 CLI handle and produce a +/// Status. |context| is prepended to the message for caller +/// identification (e.g. "[DB2] SQLDriverConnect"). +Status Db2Error(SQLSMALLINT handle_type, SQLHANDLE handle, const char* context); + +/// Convenience: check a SQLRETURN and return Status::Ok() on success, +/// or call Db2Error on failure. +Status CheckRc(SQLSMALLINT handle_type, SQLHANDLE handle, SQLRETURN rc, + const char* context); + +/// Map a 5-character SQLSTATE to the most appropriate AdbcStatusCode. +AdbcStatusCode SqlStateToAdbcStatus(const char* sqlstate); + +} // namespace adbc::db2 diff --git a/c/driver/db2/meson.build b/c/driver/db2/meson.build new file mode 100644 index 0000000000..4a6d08ae60 --- /dev/null +++ b/c/driver/db2/meson.build @@ -0,0 +1,44 @@ +# 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. + +db2_dep = meson.get_compiler('c').find_library('db2', required: true) + +adbc_db2_driver_lib = library( + 'adbc_driver_db2', + sources: [ + 'connection.cc', + 'database.cc', + 'db2.cc', + 'error.cc', + ], + include_directories: [include_dir, c_dir, driver_dir], + link_with: [adbc_common_lib, adbc_framework_lib], + dependencies: [nanoarrow_dep, fmt_dep, db2_dep], +) + +pkg.generate( + name: 'Apache Arrow Database Connectivity (ADBC) Db2 driver', + description: 'The ADBC Db2 driver provides an ADBC driver for IBM Db2 LUW.', + url: 'https://github.com/apache/arrow-adbc', + libraries: [adbc_db2_driver_lib], + filebase: 'adbc-driver-db2', +) + +adbc_driver_db2_dep = declare_dependency( + include_directories: include_dir, + link_with: adbc_db2_driver_lib, +) diff --git a/c/driver/db2/statement.h b/c/driver/db2/statement.h new file mode 100644 index 0000000000..4511851aa5 --- /dev/null +++ b/c/driver/db2/statement.h @@ -0,0 +1,44 @@ +// 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 + +#include + +#define ADBC_FRAMEWORK_USE_FMT +#include "driver/framework/base_driver.h" +#include "driver/framework/statement.h" +#include "driver/framework/status.h" + +namespace adbc::db2 { + +/// \brief ADBC statement for IBM Db2. +/// +/// Statement execution is not implemented in this initial driver +/// scope. All execute/prepare/bind paths return +/// ADBC_STATUS_NOT_IMPLEMENTED via the framework defaults; this stub +/// exists only so that the driver instantiates a complete +/// adbc::driver::Driver template. +class Db2Statement : public driver::Statement { + public: + [[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[DB2]"; + + Db2Statement() = default; + ~Db2Statement() = default; +}; + +} // namespace adbc::db2 diff --git a/c/include/arrow-adbc/driver/db2.h b/c/include/arrow-adbc/driver/db2.h new file mode 100644 index 0000000000..8aead096fc --- /dev/null +++ b/c/include/arrow-adbc/driver/db2.h @@ -0,0 +1,71 @@ +// 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. + +/// \file arrow-adbc/driver/db2.h ADBC IBM Db2 Driver +/// +/// An ADBC driver for IBM Db2 LUW that wraps the DB2 CLI library. +/// +/// \warning This driver is currently scoped to connection management +/// only (database/connection lifecycle, options, and authentication). +/// Statement execution, metadata APIs, transactions, and bulk +/// ingestion are not yet implemented and will be added in +/// subsequent pull requests. + +#pragma once + +#include + +/// \defgroup adbc-driver-db2 ADBC Db2 Driver +/// +/// Connection-string options understood by the Db2 driver. Set them on +/// the AdbcDatabase via \ref AdbcDatabaseSetOption. The standard +/// ``"uri"`` option is also supported and, when present, takes priority +/// over the individual ``adbc.db2.*`` options. +/// +/// @{ + +/// \brief The Db2 database (catalog) name. +#define ADBC_DB2_OPTION_DATABASE "adbc.db2.database" + +/// \brief The Db2 server hostname. +#define ADBC_DB2_OPTION_HOSTNAME "adbc.db2.hostname" + +/// \brief The Db2 server port (as a string, e.g. ``"50000"``). +#define ADBC_DB2_OPTION_PORT "adbc.db2.port" + +/// \brief The Db2 user identifier. The standard ``"username"`` option +/// is also accepted as a synonym. +#define ADBC_DB2_OPTION_UID "adbc.db2.uid" + +/// \brief The Db2 password. The standard ``"password"`` option is also +/// accepted as a synonym. +#define ADBC_DB2_OPTION_PWD "adbc.db2.pwd" + +/// @} + +#ifdef __cplusplus +extern "C" { +#endif + +/// \brief Entrypoint for the Db2 driver. +ADBC_EXPORT +AdbcStatusCode AdbcDriverDb2Init(int version, void* raw_driver, + struct AdbcError* error); + +#ifdef __cplusplus +} +#endif diff --git a/ci/scripts/cpp_test.sh b/ci/scripts/cpp_test.sh index 2990a7b790..6171703f70 100755 --- a/ci/scripts/cpp_test.sh +++ b/ci/scripts/cpp_test.sh @@ -19,6 +19,7 @@ set -e : ${BUILD_ALL:=1} +: ${BUILD_DRIVER_DB2:=${BUILD_ALL}} : ${BUILD_DRIVER_MANAGER:=${BUILD_ALL}} : ${BUILD_DRIVER_POSTGRESQL:=${BUILD_ALL}} : ${BUILD_DRIVER_SQLITE:=${BUILD_ALL}} @@ -32,6 +33,9 @@ test_project() { pushd "${build_dir}/" local labels="" + if [[ "${BUILD_DRIVER_DB2}" -gt 0 ]]; then + labels="${labels}|driver-db2" + fi if [[ "${BUILD_DRIVER_FLIGHTSQL}" -gt 0 ]]; then labels="${labels}|driver-flightsql" fi diff --git a/docs/source/driver/db2.rst b/docs/source/driver/db2.rst new file mode 100644 index 0000000000..703808e86f --- /dev/null +++ b/docs/source/driver/db2.rst @@ -0,0 +1,166 @@ +.. 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. + +========== +Db2 Driver +========== + +.. note:: + + This driver is in the early stages of development. The current + release implements **connection management only**: opening and + closing :c:struct:`AdbcDatabase` / :c:struct:`AdbcConnection` + handles, parsing connection options, authenticating against the + server, and mapping Db2 SQLSTATEs to ADBC error codes. + + Statement execution, metadata retrieval (``GetObjects``, + ``GetTableSchema``, ...), transaction management, and bulk + ingestion are **not yet implemented** and will be added in + subsequent pull requests. Calling those entry points returns + ``ADBC_STATUS_NOT_IMPLEMENTED``. + +The Db2 driver targets `IBM Db2 LUW +`_ and is built on the IBM Db2 +Call Level Interface (CLI / ODBC). + +Installation +============ + +The driver requires the IBM Data Server Driver Package (the +"clidriver"), which provides ``sqlcli1.h`` and the ``libdb2`` shared +library. Generic ODBC driver managers (unixODBC, iODBC) are not +used as a fallback because Db2-specific SQLSTATE classes are required +for accurate error mapping. + +The simplest way to obtain the clidriver is via the ``ibm_db`` Python +package: + +.. code-block:: shell + + pip install ibm_db + export IBM_DB_HOME=$(python -c "import importlib, pathlib; \ + p = pathlib.Path(importlib.import_module('ibm_db').__file__).resolve().parent; \ + print(p.parent / 'clidriver' if p.name == 'ibm_db' else p / 'clidriver')") + export LD_LIBRARY_PATH="${IBM_DB_HOME}/lib:${LD_LIBRARY_PATH}" # Linux + # macOS users: export DYLD_LIBRARY_PATH with the same value. + +Alternatively, point CMake at any clidriver install via +``DB2_HOME``, ``IBM_DB_HOME``, or by setting ``DB2_INCLUDE_DIR`` and +``DB2_LIBRARY`` directly. + +Build the driver with: + +.. code-block:: shell + + cmake -S c -B build -DADBC_DRIVER_DB2=ON + cmake --build build + +Usage +===== + +Configure an :c:struct:`AdbcDatabase` with either a complete Db2 CLI +connection string via the standard ``"uri"`` option, or with the +individual ``adbc.db2.*`` options: + +.. code-block:: cpp + + #include + #include + + struct AdbcDatabase database; + AdbcDatabaseNew(&database, /*error=*/nullptr); + AdbcDatabaseSetOption( + &database, "uri", + "DATABASE=mydb;HOSTNAME=localhost;PORT=50000;" + "PROTOCOL=TCPIP;UID=user;PWD=pass", + /*error=*/nullptr); + AdbcDatabaseInit(&database, /*error=*/nullptr); + + struct AdbcConnection connection; + AdbcConnectionNew(&connection, /*error=*/nullptr); + AdbcConnectionInit(&connection, &database, /*error=*/nullptr); + + // ... + + AdbcConnectionRelease(&connection, /*error=*/nullptr); + AdbcDatabaseRelease(&database, /*error=*/nullptr); + +Database Options +~~~~~~~~~~~~~~~~ + +``uri`` + A complete Db2 CLI connection string. When set, the per-field + options below are ignored. + +``adbc.db2.database`` + The Db2 database (catalog) name. + +``adbc.db2.hostname`` + The Db2 server hostname. + +``adbc.db2.port`` + The Db2 server port (as a string, e.g. ``"50000"``). + +``adbc.db2.uid`` / ``username`` + The Db2 user identifier. ``"username"`` is accepted as a synonym. + +``adbc.db2.pwd`` / ``password`` + The Db2 password. ``"password"`` is accepted as a synonym. + +Error Mapping +============= + +Db2 ``SQLSTATE`` classes are mapped to ADBC status codes: + +.. list-table:: + :header-rows: 1 + + * - SQLSTATE class + - ADBC status + * - ``08`` (connection exception) + - ``ADBC_STATUS_IO`` + * - ``22`` (data exception) + - ``ADBC_STATUS_INVALID_ARGUMENT`` + * - ``23`` (constraint violation) + - ``ADBC_STATUS_ALREADY_EXISTS`` + * - ``28`` (authorization) + - ``ADBC_STATUS_UNAUTHENTICATED`` + * - ``42`` (syntax / access) + - ``ADBC_STATUS_INVALID_ARGUMENT`` (``42S02`` / ``42704`` → ``ADBC_STATUS_NOT_FOUND``) + * - ``HY008`` + - ``ADBC_STATUS_CANCELLED`` + * - ``HY010`` + - ``ADBC_STATUS_INVALID_STATE`` + * - ``IM`` (driver manager) + - ``ADBC_STATUS_INTERNAL`` + +The first diagnostic record's SQLSTATE and native error code are also +attached as error details (``db2.sqlstate``, ``db2.native_error``). + +Testing +======= + +Set ``ADBC_DB2_TEST_URI`` to a reachable Db2 CLI connection string and +run the C++ test suite: + +.. code-block:: shell + + export ADBC_DB2_TEST_URI="DATABASE=testdb;UID=db2inst1;PWD=password;\ + HOSTNAME=localhost;PORT=50000;PROTOCOL=TCPIP" + ctest --test-dir build -L driver-db2 --output-on-failure + +When ``ADBC_DB2_TEST_URI`` is unset the tests skip automatically. diff --git a/docs/source/index.rst b/docs/source/index.rst index f7b1ee6da3..d7d784c4fa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -247,6 +247,7 @@ Why ADBC? driver/installation driver/status driver/bigquery + driver/db2 driver/duckdb driver/flight_sql driver/jdbc