diff --git a/.github/scripts/native-linux-env.sh b/.github/scripts/native-linux-env.sh new file mode 100644 index 0000000000000..74493cd92d4a1 --- /dev/null +++ b/.github/scripts/native-linux-env.sh @@ -0,0 +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. +# Shared Linux release baseline for the C++ SDK and ODBC driver. +NATIVE_GLIBC_BASELINE=2.28 +NATIVE_GCC_MIN_MAJOR=14 +NATIVE_LINUX_IMAGE="quay.io/pypa/manylinux_2_28_$(uname -m)" diff --git a/.github/scripts/package-client-cpp-manylinux228.sh b/.github/scripts/package-client-cpp-manylinux228.sh index 6bfef0aa415b6..a5506854f7ac6 100644 --- a/.github/scripts/package-client-cpp-manylinux228.sh +++ b/.github/scripts/package-client-cpp-manylinux228.sh @@ -16,6 +16,7 @@ # # Build client-cpp in manylinux_2_28 and verify max required GLIBC symbol <= 2.28. set -euxo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/native-linux-env.sh" MACHINE=$(uname -m) case "${MACHINE}" in @@ -64,7 +65,7 @@ export JAVA_HOME gcc --version c++ --version gcc_major=$(gcc -dumpversion | cut -d. -f1) -if (( gcc_major < 14 )); then +if (( gcc_major < NATIVE_GCC_MIN_MAJOR )); then echo "ERROR: GCC >= 14 is required; got $(gcc -dumpversion)" exit 1 fi @@ -78,9 +79,9 @@ java -version # modules (IPC::Cmd, Data::Dumper) that are not on the minimal image - install # them even when perl itself is already present. if command -v dnf >/dev/null 2>&1; then - dnf install -y perl perl-IPC-Cmd perl-Data-Dumper + dnf install -y perl perl-IPC-Cmd perl-Data-Dumper perl-Time-Piece else - yum install -y perl perl-IPC-Cmd perl-Data-Dumper + yum install -y perl perl-IPC-Cmd perl-Data-Dumper perl-Time-Piece fi cd "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is not set}" @@ -106,8 +107,9 @@ if [[ -z "${max_glibc}" ]]; then exit 1 fi -if awk -v max="${max_glibc}" "BEGIN { exit !(max > 2.28) }"; then - echo "ERROR: libiotdb_session.so requires glibc > 2.28 (max=${max_glibc})" +highest_glibc=$(printf '%s\n' "${max_glibc}" "${NATIVE_GLIBC_BASELINE}" | sort -V | tail -1) +if [[ "${highest_glibc}" != "${NATIVE_GLIBC_BASELINE}" ]]; then + echo "ERROR: libiotdb_session.so requires glibc > ${NATIVE_GLIBC_BASELINE} (max=${max_glibc})" exit 1 fi diff --git a/.github/scripts/package-odbc-manylinux228.sh b/.github/scripts/package-odbc-manylinux228.sh new file mode 100644 index 0000000000000..3a458f1a9407c --- /dev/null +++ b/.github/scripts/package-odbc-manylinux228.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# 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. +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/native-linux-env.sh" +if (( $(gcc -dumpversion | cut -d. -f1) < NATIVE_GCC_MIN_MAJOR )); then + echo "GCC ${NATIVE_GCC_MIN_MAJOR}+ is required" >&2 + exit 1 +fi +dnf install -y unixODBC-devel perl-IPC-Cmd perl-Data-Dumper perl-Time-Piece +odbc_build="${ODBC_BUILD_DIR:-iotdb-client/odbc/target/build}" +cmake -S iotdb-client/odbc -B "${odbc_build}" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_SKIP_RPATH=ON \ + -DODBC_PACKAGE_VERSION="${ODBC_PACKAGE_VERSION:-$(sed -n 's:.*\([^<]*\).*:\1:p' iotdb-client/pom.xml | head -1)}" \ + -DODBC_PACKAGE_CLASSIFIER="linux-x86_64-glibc${NATIVE_GLIBC_BASELINE}" +cmake --build "${odbc_build}" --parallel "${CMAKE_BUILD_PARALLEL_LEVEL:-4}" +ctest --test-dir "${odbc_build}" --output-on-failure +cmake -DDRIVER="${odbc_build}/libapache_iotdb_odbc.so" \ + -DGLIBC_BASELINE="${NATIVE_GLIBC_BASELINE}" -P iotdb-client/odbc/cmake/CheckLinuxPackage.cmake +(cd "${odbc_build}" && cpack) diff --git a/.github/workflows/client-cpp-package.yml b/.github/workflows/client-cpp-package.yml index 70f2bffdddda6..c59665f2b1e26 100644 --- a/.github/workflows/client-cpp-package.yml +++ b/.github/workflows/client-cpp-package.yml @@ -64,7 +64,7 @@ jobs: cpp=false while IFS= read -r file; do case "$file" in - iotdb-client/client-cpp/*|iotdb-client/pom.xml|iotdb-protocol/thrift-datanode/src/main/thrift/client.thrift|iotdb-protocol/thrift-commons/src/main/thrift/common.thrift|.github/workflows/client-cpp-package.yml|.github/scripts/package-client-cpp-*.sh) + iotdb-client/client-cpp/*|iotdb-client/pom.xml|iotdb-protocol/thrift-datanode/src/main/thrift/client.thrift|iotdb-protocol/thrift-commons/src/main/thrift/common.thrift|.github/workflows/client-cpp-package.yml|.github/scripts/package-client-cpp-*.sh|.github/scripts/native-linux-env.sh) cpp=true break ;; @@ -140,7 +140,7 @@ jobs: echo "run_windows=${run_windows}" >> "$GITHUB_OUTPUT" # Compact JSON (no leading whitespace); required for GITHUB_OUTPUT + fromJSON(). - WINDOWS_MATRIX='[{"name":"windows-vs2026","runs-on":"windows-2025-vs2026","boost_choco":"boost-msvc-14.3","boost_choco_version":"","cmake_generator":"Visual Studio 18 2026","package_classifier":"windows-x86_64-msvc14.4","vs_choco":"","vs_choco_params":""},{"name":"windows-vs2022","runs-on":"windows-2022","boost_choco":"boost-msvc-14.3","boost_choco_version":"","cmake_generator":"","package_classifier":"windows-x86_64-msvc14.3","vs_choco":"","vs_choco_params":""},{"name":"windows-vs2019","runs-on":"windows-2022","boost_choco":"boost-msvc-14.2","boost_choco_version":"","cmake_generator":"Visual Studio 16 2019","package_classifier":"windows-x86_64-msvc14.2","vs_choco":"visualstudio2019buildtools","vs_choco_params":"--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"},{"name":"windows-vs2017","runs-on":"windows-2022","boost_choco":"boost-msvc-14.1","boost_choco_version":"1.74.0","cmake_generator":"Visual Studio 15 2017","package_classifier":"windows-x86_64-msvc14.1","vs_choco":"visualstudio2017buildtools","vs_choco_params":"--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"}]' + WINDOWS_MATRIX='[{"name":"windows-vs2026","runs-on":"windows-2025-vs2026","cmake_generator":"Visual Studio 18 2026","package_classifier":"windows-x86_64-msvc14.4","vs_choco":"","vs_choco_params":""},{"name":"windows-vs2022","runs-on":"windows-2022","cmake_generator":"","package_classifier":"windows-x86_64-msvc14.3","vs_choco":"","vs_choco_params":""},{"name":"windows-vs2019","runs-on":"windows-2022","cmake_generator":"Visual Studio 16 2019","package_classifier":"windows-x86_64-msvc14.2","vs_choco":"visualstudio2019buildtools","vs_choco_params":"--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"},{"name":"windows-vs2017","runs-on":"windows-2022","cmake_generator":"Visual Studio 15 2017","package_classifier":"windows-x86_64-msvc14.1","vs_choco":"visualstudio2017buildtools","vs_choco_params":"--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"}]' write_windows_matrix_output() { local matrix_json="$1" @@ -189,12 +189,13 @@ jobs: run: | set -euxo pipefail chmod +x .github/scripts/package-client-cpp-manylinux228.sh + source .github/scripts/native-linux-env.sh docker run --rm \ -v "${{ github.workspace }}:/workspace" \ -v "${HOME}/.m2:/root/.m2" \ -w /workspace \ -e GITHUB_WORKSPACE=/workspace \ - quay.io/pypa/manylinux_2_28_x86_64 \ + "${NATIVE_LINUX_IMAGE}" \ bash .github/scripts/package-client-cpp-manylinux228.sh - name: Restore workspace ownership after container build if: always() @@ -247,12 +248,13 @@ jobs: run: | set -euxo pipefail chmod +x .github/scripts/package-client-cpp-manylinux228.sh + source .github/scripts/native-linux-env.sh docker run --rm \ -v "${{ github.workspace }}:/workspace" \ -v "${HOME}/.m2:/root/.m2" \ -w /workspace \ -e GITHUB_WORKSPACE=/workspace \ - quay.io/pypa/manylinux_2_28_aarch64 \ + "${NATIVE_LINUX_IMAGE}" \ bash .github/scripts/package-client-cpp-manylinux228.sh - name: Restore workspace ownership after container build if: always() @@ -432,20 +434,11 @@ jobs: shell: pwsh run: | choco install winflexbison3 strawberryperl -y --no-progress + if ($LASTEXITCODE) { exit $LASTEXITCODE } echo 'C:\Strawberry\perl\bin' >> $env:GITHUB_PATH - $boostArgs = @('install', '${{ matrix.boost_choco }}', '-y', '--no-progress') - if ('${{ matrix.boost_choco_version }}' -ne '') { - $boostArgs += @("--version=${{ matrix.boost_choco_version }}") - } - & choco @boostArgs - if (-not (Test-Path 'C:\local')) { - New-Item -ItemType Directory -Path 'C:\local' -Force | Out-Null - } - $boostDir = Get-ChildItem -Path 'C:\local\' -Filter 'boost_*' -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $boostDir) { - throw "Boost not found under C:\local after installing ${{ matrix.boost_choco }}" - } - echo $boostDir.FullName >> $env:GITHUB_PATH + # Thrift only needs Boost headers. FetchBoost.cmake provides the same + # source headers for every toolchain; precompiled Chocolatey Boost + # installers add an unnecessary download and can fail checksum checks. - name: Cache Maven packages uses: actions/cache@v5 with: diff --git a/.github/workflows/multi-language-client.yml b/.github/workflows/multi-language-client.yml index b49b63b2e5c77..4b0695c1cf878 100644 --- a/.github/workflows/multi-language-client.yml +++ b/.github/workflows/multi-language-client.yml @@ -15,6 +15,7 @@ on: - '.github/workflows/multi-language-client.yml' - '.github/workflows/client-cpp-package.yml' - '.github/scripts/package-client-cpp-*.sh' + - '.github/scripts/native-linux-env.sh' pull_request: branches: - master @@ -30,6 +31,7 @@ on: - '.github/workflows/multi-language-client.yml' - '.github/workflows/client-cpp-package.yml' - '.github/scripts/package-client-cpp-*.sh' + - '.github/scripts/native-linux-env.sh' # allow manually run the action: workflow_dispatch: @@ -82,7 +84,7 @@ jobs: go=false while IFS= read -r file; do case "$file" in - pom.xml|iotdb-client/pom.xml|iotdb-client/client-cpp/*|iotdb-protocol/thrift-datanode/src/main/thrift/client.thrift|iotdb-protocol/thrift-commons/src/main/thrift/common.thrift|.github/workflows/multi-language-client.yml|.github/workflows/client-cpp-package.yml|.github/scripts/package-client-cpp-*.sh) + pom.xml|iotdb-client/pom.xml|iotdb-client/client-cpp/*|iotdb-protocol/thrift-datanode/src/main/thrift/client.thrift|iotdb-protocol/thrift-commons/src/main/thrift/common.thrift|.github/workflows/multi-language-client.yml|.github/workflows/client-cpp-package.yml|.github/scripts/package-client-cpp-*.sh|.github/scripts/native-linux-env.sh) cpp=true ;; esac diff --git a/.github/workflows/odbc-package.yml b/.github/workflows/odbc-package.yml new file mode 100644 index 0000000000000..1f11d32c4c80e --- /dev/null +++ b/.github/workflows/odbc-package.yml @@ -0,0 +1,180 @@ +# 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. +name: ODBC package +on: + workflow_dispatch: + push: + paths: + - 'iotdb-client/odbc/**' + - 'iotdb-client/client-cpp/**' + - 'iotdb-protocol/**/thrift/**' + - '.github/scripts/*odbc*' + - '.github/scripts/native-linux-env.sh' + - '.github/workflows/odbc-package.yml' + pull_request: + paths: + - 'iotdb-client/odbc/**' + - 'iotdb-client/client-cpp/**' + - 'iotdb-protocol/**/thrift/**' + - '.github/scripts/*odbc*' + - '.github/scripts/native-linux-env.sh' + - '.github/workflows/odbc-package.yml' +jobs: + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build and check glibc baseline + run: | + source .github/scripts/native-linux-env.sh + docker run --rm -v "$PWD:/workspace" -w /workspace "${NATIVE_LINUX_IMAGE}" \ + bash .github/scripts/package-odbc-manylinux228.sh + - name: Restore ownership + if: always() + run: sudo chown -R "$(id -u):$(id -g)" iotdb-client/odbc/target iotdb-client/client-cpp/third-party + - uses: actions/upload-artifact@v7 + with: + name: odbc-linux-x86_64-glibc2.28 + path: iotdb-client/odbc/target/build/iotdb-odbc-*.zip* + if-no-files-found: error + windows: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v7 + - name: Install build prerequisites + shell: pwsh + run: | + choco install winflexbison3 strawberryperl -y --no-progress + 'C:\Strawberry\perl\bin' >> $env:GITHUB_PATH + - name: Build, test and package + shell: pwsh + run: | + $version = ([xml](Get-Content iotdb-client/pom.xml)).project.parent.version + cmake -S iotdb-client/odbc -B iotdb-client/odbc/target/build -A x64 "-DODBC_PACKAGE_VERSION=$version" -DODBC_PACKAGE_CLASSIFIER=windows-x86_64 + if ($LASTEXITCODE) { exit $LASTEXITCODE } + cmake --build iotdb-client/odbc/target/build --config Release --parallel 4 + if ($LASTEXITCODE) { exit $LASTEXITCODE } + ctest --test-dir iotdb-client/odbc/target/build -C Release --output-on-failure + if ($LASTEXITCODE) { exit $LASTEXITCODE } + & iotdb-client/odbc/test/check-windows-dependencies.ps1 iotdb-client/odbc/target/build/Release/apache_iotdb_odbc.dll + Push-Location iotdb-client/odbc/target/build + cpack -C Release + if ($LASTEXITCODE) { exit $LASTEXITCODE } + Pop-Location + - uses: actions/upload-artifact@v7 + with: + name: odbc-windows-x86_64 + path: iotdb-client/odbc/target/build/iotdb-odbc-*.zip* + if-no-files-found: error + linux-runtime: + needs: linux + runs-on: ubuntu-latest + strategy: + matrix: + image: ['almalinux:8', 'ubuntu:22.04', 'ubuntu:24.04'] + steps: + - uses: actions/download-artifact@v8 + with: + name: odbc-linux-x86_64-glibc2.28 + path: artifacts + - name: Extract release + run: unzip -q artifacts/*.zip -d package + - name: Load with system ODBC manager in a clean runtime + run: | + docker run --rm -v "$PWD/package:/package:ro" '${{ matrix.image }}' sh -ec ' + if command -v dnf >/dev/null; then + dnf install -y unixODBC + else + apt-get update && apt-get install -y --no-install-recommends unixodbc + fi + pkg=$(find /package -mindepth 1 -maxdepth 1 -type d) + "$pkg/test/odbc_manager" "$pkg/lib/libapache_iotdb_odbc.so" + ' + server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + distribution: corretto + java-version: 17 + cache: maven + - name: Check ODBC source formatting + run: | + sudo apt-get update + sudo apt-get install -y clang-format-17 + sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-17 100 + sudo update-alternatives --set clang-format /usr/bin/clang-format-17 + ./mvnw -B -P with-odbc -pl iotdb-client/odbc spotless:check + - name: Build disposable test server + run: ./mvnw -B clean package -pl distribution -am -DskipTests + - uses: actions/upload-artifact@v7 + with: + name: odbc-test-server + path: distribution/target/apache-iotdb-*-all-bin.zip + if-no-files-found: error + server-runtime: + needs: [linux, windows, server] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact: odbc-linux-x86_64-glibc2.28 + - os: windows-2022 + artifact: odbc-windows-x86_64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + distribution: corretto + java-version: 17 + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - uses: actions/download-artifact@v8 + with: + name: ${{ matrix.artifact }} + path: artifacts/driver + - uses: actions/download-artifact@v8 + with: + name: odbc-test-server + path: artifacts/server + - name: Install runtime ODBC manager + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y unixodbc lsof + - name: Extract packages + shell: pwsh + run: | + Expand-Archive (Get-ChildItem artifacts/driver/*.zip).FullName package/driver + Expand-Archive (Get-ChildItem artifacts/server/*.zip).FullName package/server + - name: Test real server and application consumers + shell: pwsh + env: + IOTDB_TEST_PASSWORD: root + run: | + $package = (Get-ChildItem package/driver -Directory).FullName + $server = (Get-ChildItem package/server -Directory).FullName + if ($IsWindows) { + python iotdb-client/odbc/test/run_e2e.py "$package/test/odbc_manager.exe" "$package/bin/apache_iotdb_odbc.dll" $server --windows-consumers + } else { + chmod +x "$package/test/odbc_manager" + Get-ChildItem "$server/sbin/*.sh" | ForEach-Object { chmod +x $_.FullName } + python iotdb-client/odbc/test/run_e2e.py "$package/test/odbc_manager" "$package/lib/libapache_iotdb_odbc.so" $server + } + if ($LASTEXITCODE) { exit $LASTEXITCODE } diff --git a/iotdb-client/client-cpp/CMakeLists.txt b/iotdb-client/client-cpp/CMakeLists.txt index f6178d183d896..431aa5b98800b 100644 --- a/iotdb-client/client-cpp/CMakeLists.txt +++ b/iotdb-client/client-cpp/CMakeLists.txt @@ -22,6 +22,10 @@ cmake_minimum_required(VERSION 3.16) project(iotdb_session CXX C) +option(IOTDB_SESSION_STATIC "Build the session client as a static library" OFF) +option(IOTDB_STATIC_CRT "Use the static MSVC runtime" OFF) +option(IOTDB_SESSION_INSTALL "Install the standalone session SDK" ON) + set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -46,7 +50,11 @@ endif() if(MSVC) # /MD: matches default Visual Studio projects; CRT lives in the VC redistributable. - set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + if(IOTDB_STATIC_CRT) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + else() + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + endif() endif() if(NOT IOTDB_USE_CXX11_ABI STREQUAL "" AND NOT IOTDB_USE_CXX11_ABI MATCHES "^[01]$") @@ -59,7 +67,7 @@ if(NOT MSVC) else() set(_iotdb_cxx11_abi_stamp_value "default") endif() - set(_iotdb_cxx11_abi_stamp "${CMAKE_BINARY_DIR}/.iotdb-cxx11-abi") + set(_iotdb_cxx11_abi_stamp "${CMAKE_CURRENT_BINARY_DIR}/.iotdb-cxx11-abi") if(EXISTS "${_iotdb_cxx11_abi_stamp}") file(READ "${_iotdb_cxx11_abi_stamp}" _iotdb_previous_cxx11_abi) else() @@ -69,8 +77,8 @@ if(NOT MSVC) message(STATUS "IOTDB_USE_CXX11_ABI changed from '${_iotdb_previous_cxx11_abi}' " "to '${_iotdb_cxx11_abi_stamp_value}'; removing cached iotdb_session objects") - file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/CMakeFiles/iotdb_session.dir") - file(GLOB _iotdb_stale_session_libs "${CMAKE_BINARY_DIR}/libiotdb_session.so*") + file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/iotdb_session.dir") + file(GLOB _iotdb_stale_session_libs "${CMAKE_CURRENT_BINARY_DIR}/libiotdb_session.so*") if(_iotdb_stale_session_libs) file(REMOVE ${_iotdb_stale_session_libs}) endif() @@ -110,7 +118,7 @@ endif() file(MAKE_DIRECTORY "${IOTDB_OS_DEPS_DIR}") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" + set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install" CACHE PATH "Install prefix" FORCE) endif() @@ -130,12 +138,17 @@ file(GLOB SESSION_PUBLIC_SRCS CONFIGURE_DEPENDS file(GLOB SESSION_RPC_SRCS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/rpc/*.cpp") -add_library(iotdb_session SHARED +if(IOTDB_SESSION_STATIC) + set(_iotdb_library_type STATIC) +else() + set(_iotdb_library_type SHARED) +endif() +add_library(iotdb_session ${_iotdb_library_type} ${SESSION_PUBLIC_SRCS} ${SESSION_RPC_SRCS} ${THRIFT_GENERATED_SRCS}) -if(WIN32) +if(WIN32 AND NOT IOTDB_SESSION_STATIC) set_target_properties(iotdb_session PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() @@ -163,7 +176,12 @@ endif() add_dependencies(iotdb_session iotdb_thrift_external iotdb_thrift_codegen) -target_compile_definitions(iotdb_session PRIVATE THRIFT_STATIC_DEFINE IOTDB_BUILDING_SHARED) +target_compile_definitions(iotdb_session PRIVATE THRIFT_STATIC_DEFINE) +if(IOTDB_SESSION_STATIC) + target_compile_definitions(iotdb_session PUBLIC IOTDB_SESSION_STATIC) +else() + target_compile_definitions(iotdb_session PRIVATE IOTDB_BUILDING_SHARED) +endif() if(NOT IOTDB_USE_CXX11_ABI STREQUAL "" AND NOT MSVC) target_compile_definitions(iotdb_session PUBLIC _GLIBCXX_USE_CXX11_ABI=${IOTDB_USE_CXX11_ABI}) @@ -179,7 +197,9 @@ target_include_directories(iotdb_session ${THRIFT_INCLUDE_DIR} $<$:${BOOST_INCLUDE_DIR}>) -if(APPLE) +if(IOTDB_SESSION_STATIC) + target_link_libraries(iotdb_session PRIVATE iotdb_thrift_static) +elseif(APPLE) target_link_libraries(iotdb_session PRIVATE "-Wl,-force_load,${THRIFT_STATIC_LIB_PATH}") elseif(UNIX AND NOT MSVC) # whole-archive pulls all Thrift objects into libiotdb_session.so; allow-multiple-definition @@ -214,6 +234,8 @@ endif() include(GNUInstallDirs) +if(IOTDB_SESSION_INSTALL) + set(IOTDB_PUBLIC_HEADERS Export.h SessionConfig.h @@ -265,11 +287,11 @@ if(NOT IOTDB_USE_CXX11_ABI STREQUAL "" AND NOT MSVC) endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/iotdb-session-config.cmake.in" - "${CMAKE_BINARY_DIR}/package-metadata/cmake/iotdb-session-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/cmake/iotdb-session-config.cmake" @ONLY) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/iotdb-session.pc.in" - "${CMAKE_BINARY_DIR}/package-metadata/pkgconfig/iotdb-session.pc" + "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/pkgconfig/iotdb-session.pc" @ONLY) execute_process( COMMAND git rev-parse HEAD @@ -296,9 +318,9 @@ else() set(IOTDB_SESSION_OPENSSL_VERSION "disabled") set(IOTDB_SESSION_OPENSSL_PROVIDER "disabled") endif() -file(WRITE "${CMAKE_BINARY_DIR}/package-metadata/VERSION" +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/VERSION" "${IOTDB_SESSION_VERSION}\n") -file(WRITE "${CMAKE_BINARY_DIR}/package-metadata/BUILD-INFO.txt" +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/BUILD-INFO.txt" "Apache IoTDB C++ session client build information\n\n" "version=${IOTDB_SESSION_VERSION}\n" "commit=${IOTDB_SESSION_GIT_COMMIT}\n" @@ -313,14 +335,15 @@ file(WRITE "${CMAKE_BINARY_DIR}/package-metadata/BUILD-INFO.txt" "iotdb.offline=${IOTDB_OFFLINE}\n" "iotdb.use.cxx11.abi=${IOTDB_USE_CXX11_ABI}\n" "iotdb.extra.cxx.flags=${IOTDB_EXTRA_CXX_FLAGS}\n") -install(FILES "${CMAKE_BINARY_DIR}/package-metadata/cmake/iotdb-session-config.cmake" +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/cmake/iotdb-session-config.cmake" DESTINATION cmake) -install(FILES "${CMAKE_BINARY_DIR}/package-metadata/pkgconfig/iotdb-session.pc" +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/pkgconfig/iotdb-session.pc" DESTINATION pkgconfig) install(FILES - "${CMAKE_BINARY_DIR}/package-metadata/VERSION" - "${CMAKE_BINARY_DIR}/package-metadata/BUILD-INFO.txt" + "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/VERSION" + "${CMAKE_CURRENT_BINARY_DIR}/package-metadata/BUILD-INFO.txt" DESTINATION .) +endif() if(BUILD_TESTING) enable_testing() diff --git a/iotdb-client/client-cpp/cmake/FetchBoost.cmake b/iotdb-client/client-cpp/cmake/FetchBoost.cmake index d1ab9c18ae1b9..75eec3fa4462e 100644 --- a/iotdb-client/client-cpp/cmake/FetchBoost.cmake +++ b/iotdb-client/client-cpp/cmake/FetchBoost.cmake @@ -103,9 +103,9 @@ if(NOT _boost_tarball) endif() # --------------------------------------------------------------------------- -# Extract headers-only into ${CMAKE_BINARY_DIR}/_deps/boost +# Extract headers-only into ${CMAKE_CURRENT_BINARY_DIR}/_deps/boost # --------------------------------------------------------------------------- -set(_boost_extract_dir "${CMAKE_BINARY_DIR}/_deps/boost") +set(_boost_extract_dir "${CMAKE_CURRENT_BINARY_DIR}/_deps/boost") set(_boost_marker "${_boost_extract_dir}/.extracted-${BOOST_VERSION}") if(NOT EXISTS "${_boost_marker}") diff --git a/iotdb-client/client-cpp/cmake/FetchBuildTools.cmake b/iotdb-client/client-cpp/cmake/FetchBuildTools.cmake index 5860d68ac195f..f24a78bd4726e 100644 --- a/iotdb-client/client-cpp/cmake/FetchBuildTools.cmake +++ b/iotdb-client/client-cpp/cmake/FetchBuildTools.cmake @@ -23,7 +23,7 @@ # Otherwise we provision them locally: # # * Linux / macOS - configure-make-install each tool from a tarball into -# ${CMAKE_BINARY_DIR}/tools (no sudo required). +# ${CMAKE_CURRENT_BINARY_DIR}/tools (no sudo required). # * Windows - extract the winflexbison zip and copy # win_flex.exe -> flex.exe, win_bison.exe -> bison.exe. # @@ -33,7 +33,7 @@ # 3. FATAL_ERROR otherwise # ============================================================================= -set(_tools_prefix "${CMAKE_BINARY_DIR}/tools") +set(_tools_prefix "${CMAKE_CURRENT_BINARY_DIR}/tools") set(_tools_bin "${_tools_prefix}/bin") file(MAKE_DIRECTORY "${_tools_bin}") @@ -109,7 +109,7 @@ endfunction() # Configure-make-install from into ${_tools_prefix}. function(_iotdb_build_autotools NAME TARBALL EXTRACTED_DIRNAME) - set(_src_root "${CMAKE_BINARY_DIR}/_deps/${NAME}") + set(_src_root "${CMAKE_CURRENT_BINARY_DIR}/_deps/${NAME}") set(_marker "${_tools_prefix}/.${NAME}-installed") if(EXISTS "${_marker}") return() diff --git a/iotdb-client/client-cpp/cmake/FetchOpenSSL.cmake b/iotdb-client/client-cpp/cmake/FetchOpenSSL.cmake index 165736dd1602c..9ab01108e7529 100644 --- a/iotdb-client/client-cpp/cmake/FetchOpenSSL.cmake +++ b/iotdb-client/client-cpp/cmake/FetchOpenSSL.cmake @@ -21,7 +21,7 @@ # Apache Thrift 0.24 (bundled by this client) builds against OpenSSL 3.x. # # By default, a fixed OpenSSL source release is downloaded, checksum-verified, -# built as shared libraries, and installed under ${CMAKE_BINARY_DIR}/_deps. +# built as shared libraries, and installed under ${CMAKE_CURRENT_BINARY_DIR}/_deps. # Set IOTDB_OPENSSL_FROM_SOURCE=OFF to opt into a compatible system OpenSSL. # # Side effects: @@ -41,6 +41,9 @@ set(OPENSSL_FALLBACK_SHA256 # packages independent of the build host's OpenSSL installation. option(IOTDB_OPENSSL_FROM_SOURCE "Ignore any system OpenSSL and build OpenSSL ${OPENSSL_FALLBACK_VERSION} from source" ON) +option(IOTDB_OPENSSL_STATIC "Build OpenSSL as static PIC libraries" OFF) +set(OPENSSL_USE_STATIC_LIBS ${IOTDB_OPENSSL_STATIC}) +set(OPENSSL_MSVC_STATIC_RT ${IOTDB_STATIC_CRT}) if(NOT IOTDB_OPENSSL_FROM_SOURCE) find_package(OpenSSL QUIET) @@ -88,7 +91,24 @@ if(_ossl_download_required) endif() endif() -set(_ossl_root "${CMAKE_BINARY_DIR}/_deps/openssl") +set(_ossl_variant "shared") +set(_ossl_options shared no-tests) +if(IOTDB_OPENSSL_STATIC) + set(_ossl_variant "static-${CMAKE_SYSTEM_PROCESSOR}-${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}-crt${IOTDB_STATIC_CRT}") + set(_ossl_options no-shared no-module no-tests) + if(NOT WIN32) + list(APPEND _ossl_options -fPIC) + elseif(NOT IOTDB_STATIC_CRT) + list(APPEND _ossl_options /MD) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND _ossl_options --debug) + endif() +endif() +set(_ossl_root "${CMAKE_CURRENT_BINARY_DIR}/_deps/openssl") +if(IOTDB_OPENSSL_STATIC) + string(APPEND _ossl_root "-${_ossl_variant}") +endif() set(_ossl_src "${_ossl_root}/src/openssl-${OPENSSL_FALLBACK_VERSION}") set(_ossl_inst "${_ossl_root}/install") set(_ossl_stamp "${_ossl_root}/.built-${OPENSSL_FALLBACK_VERSION}") @@ -134,16 +154,20 @@ if(NOT EXISTS "${_ossl_stamp}") file(TO_NATIVE_PATH "${_ossl_src}" _ossl_src_native) file(TO_NATIVE_PATH "${_ossl_perl}" _ossl_perl_native) set(_ossl_build_script "${_ossl_root}/build-openssl.cmd") + string(REPLACE ";" " " _ossl_windows_options "${_ossl_options}") + # file(WRITE) translates LF to CRLF on Windows; explicit CRLF produces + # CRCRLF and cmd.exe can skip commands. Also catch negative process exit + # codes (for example a missing Perl runtime DLL), not only positive ones. file(WRITE "${_ossl_build_script}" - "@echo on\r\n" - "call \"${_vcvars}\"\r\n" - "if errorlevel 1 exit /b %errorlevel%\r\n" - "cd /d \"${_ossl_src_native}\"\r\n" - "\"${_ossl_perl_native}\" Configure VC-WIN64A --prefix=\"${_ossl_inst_native}\" --openssldir=\"${_ossl_inst_native}\\ssl\" shared no-tests no-asm\r\n" - "if errorlevel 1 exit /b %errorlevel%\r\n" - "nmake\r\n" - "if errorlevel 1 exit /b %errorlevel%\r\n" - "nmake install_sw\r\n") + "@echo on\n" + "call \"${_vcvars}\"\n" + "if not \"%errorlevel%\"==\"0\" exit /b %errorlevel%\n" + "cd /d \"${_ossl_src_native}\"\n" + "\"${_ossl_perl_native}\" Configure VC-WIN64A --prefix=\"${_ossl_inst_native}\" --openssldir=\"${_ossl_inst_native}\\ssl\" ${_ossl_windows_options} no-asm\n" + "if not \"%errorlevel%\"==\"0\" exit /b %errorlevel%\n" + "nmake\n" + "if not \"%errorlevel%\"==\"0\" exit /b %errorlevel%\n" + "nmake install_sw\n") execute_process(COMMAND cmd /d /c "${_ossl_build_script}" RESULT_VARIABLE _rc) if(NOT _rc EQUAL 0) message(FATAL_ERROR "[OpenSSL] Windows source build failed (rc=${_rc})") @@ -155,7 +179,8 @@ if(NOT EXISTS "${_ossl_stamp}") set(_jobs 1) endif() execute_process( - COMMAND ./config --prefix=${_ossl_inst} --openssldir=${_ossl_inst}/ssl shared no-tests + COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_C_COMPILER}" + ./config --prefix=${_ossl_inst} --openssldir=${_ossl_inst}/ssl ${_ossl_options} WORKING_DIRECTORY "${_ossl_src}" RESULT_VARIABLE _rc) if(NOT _rc EQUAL 0) @@ -180,7 +205,7 @@ if(NOT EXISTS "${_ossl_stamp}") file(TOUCH "${_ossl_stamp}") endif() -if(APPLE) +if(APPLE AND NOT IOTDB_OPENSSL_STATIC) # OpenSSL's Darwin build records its absolute installation prefix in each dylib. Rewrite the # IDs before iotdb_session links against them so both the client and libssl resolve the bundled # libraries relative to the package's lib/ directory. @@ -233,6 +258,11 @@ if(APPLE) endif() set(OPENSSL_ROOT_DIR "${_ossl_inst}" CACHE PATH "OpenSSL root" FORCE) -set(OPENSSL_USE_STATIC_LIBS OFF) +# Do not reuse a previous shared-library discovery when switching variants. +unset(OPENSSL_SSL_LIBRARY CACHE) +unset(OPENSSL_CRYPTO_LIBRARY CACHE) +unset(OPENSSL_INCLUDE_DIR CACHE) find_package(OpenSSL REQUIRED) -message(STATUS "[OpenSSL] built locally (shared) at ${OPENSSL_ROOT_DIR}") +# The ODBC parent also consumes these targets (FindOpenSSL creates local imports). +set_property(TARGET OpenSSL::SSL OpenSSL::Crypto PROPERTY IMPORTED_GLOBAL TRUE) +message(STATUS "[OpenSSL] built locally (${_ossl_variant}) at ${OPENSSL_ROOT_DIR}") diff --git a/iotdb-client/client-cpp/cmake/FetchThrift.cmake b/iotdb-client/client-cpp/cmake/FetchThrift.cmake index d69b2a47ad9e5..a5b614ad281cf 100644 --- a/iotdb-client/client-cpp/cmake/FetchThrift.cmake +++ b/iotdb-client/client-cpp/cmake/FetchThrift.cmake @@ -67,9 +67,12 @@ if(NOT EXISTS "${_thrift_tarball}") endif() # --------------------------------------------------------------------------- -# Extract once into ${CMAKE_BINARY_DIR}/_deps/thrift/src +# Extract once into ${CMAKE_CURRENT_BINARY_DIR}/_deps/thrift/src # --------------------------------------------------------------------------- -set(_thrift_root "${CMAKE_BINARY_DIR}/_deps/thrift") +set(_thrift_root "${CMAKE_CURRENT_BINARY_DIR}/_deps/thrift") +if(IOTDB_SESSION_STATIC) + string(APPEND _thrift_root "-static-${CMAKE_SYSTEM_PROCESSOR}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}-crt${IOTDB_STATIC_CRT}-ssl${WITH_SSL}-abi${IOTDB_USE_CXX11_ABI}") +endif() set(_thrift_src "${_thrift_root}/src/${_thrift_dirname}") set(_thrift_build "${_thrift_root}/build") set(_thrift_install "${_thrift_root}/install") @@ -125,19 +128,28 @@ endif() if(MSVC) list(APPEND _thrift_cmake_args - "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$:Debug>DLL") + "-DWITH_MT=${IOTDB_STATIC_CRT}" + "-DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY}") else() set(_thrift_cxxflags "-fPIC") - if(IOTDB_USE_CXX11_ABI) + if(NOT IOTDB_USE_CXX11_ABI STREQUAL "") set(_thrift_cxxflags "${_thrift_cxxflags} -D_GLIBCXX_USE_CXX11_ABI=${IOTDB_USE_CXX11_ABI}") endif() list(APPEND _thrift_cmake_args + "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}" + "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}" "-DCMAKE_C_FLAGS=-fPIC" "-DCMAKE_CXX_FLAGS=${_thrift_cxxflags}") endif() if(WITH_SSL) list(APPEND _thrift_cmake_args "-DWITH_OPENSSL=ON") + list(APPEND _thrift_cmake_args + "-DOPENSSL_USE_STATIC_LIBS=${IOTDB_OPENSSL_STATIC}" + "-DOPENSSL_MSVC_STATIC_RT=${IOTDB_STATIC_CRT}" + "-DOPENSSL_SSL_LIBRARY=${OPENSSL_SSL_LIBRARY}" + "-DOPENSSL_CRYPTO_LIBRARY=${OPENSSL_CRYPTO_LIBRARY}" + "-DOPENSSL_INCLUDE_DIR=${OPENSSL_INCLUDE_DIR}") # Build Thrift's TSSLSocket against the same OpenSSL that iotdb_session links # and bundles, so the runtime libraries match. find_package does not set # OPENSSL_ROOT_DIR itself, so derive it from the resolved include dir. diff --git a/iotdb-client/client-cpp/cmake/GenerateThriftSources.cmake b/iotdb-client/client-cpp/cmake/GenerateThriftSources.cmake index 6a3685af45847..ba1c931fd5e17 100644 --- a/iotdb-client/client-cpp/cmake/GenerateThriftSources.cmake +++ b/iotdb-client/client-cpp/cmake/GenerateThriftSources.cmake @@ -51,12 +51,12 @@ endforeach() # Mirror the bash reference script: stage both .thrift files into a flat # working directory so `include "common.thrift"` resolves without -I args. -set(_thrift_proto_dir "${CMAKE_BINARY_DIR}/thrift-protocols") +set(_thrift_proto_dir "${CMAKE_CURRENT_BINARY_DIR}/thrift-protocols") file(MAKE_DIRECTORY "${_thrift_proto_dir}") configure_file("${_common_thrift}" "${_thrift_proto_dir}/common.thrift" COPYONLY) configure_file("${_client_thrift}" "${_thrift_proto_dir}/client.thrift" COPYONLY) -set(THRIFT_GEN_CPP_DIR "${CMAKE_BINARY_DIR}/gen-cpp" CACHE PATH "" FORCE) +set(THRIFT_GEN_CPP_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen-cpp" CACHE PATH "" FORCE) file(MAKE_DIRECTORY "${THRIFT_GEN_CPP_DIR}") # A stamp file lets us re-run thrift only when one of the inputs changes diff --git a/iotdb-client/client-cpp/src/include/Export.h b/iotdb-client/client-cpp/src/include/Export.h index 71262d0cffb77..d4f46d3ed8732 100644 --- a/iotdb-client/client-cpp/src/include/Export.h +++ b/iotdb-client/client-cpp/src/include/Export.h @@ -19,7 +19,9 @@ #ifndef IOTDB_EXPORT_H #define IOTDB_EXPORT_H -#if defined(_WIN32) && defined(IOTDB_BUILDING_SHARED) +#if defined(IOTDB_SESSION_STATIC) +#define IOTDB_SESSION_API +#elif defined(_WIN32) && defined(IOTDB_BUILDING_SHARED) #define IOTDB_SESSION_API __declspec(dllexport) #elif defined(_WIN32) #define IOTDB_SESSION_API __declspec(dllimport) diff --git a/iotdb-client/odbc/.gitignore b/iotdb-client/odbc/.gitignore new file mode 100644 index 0000000000000..c49c7033d8e37 --- /dev/null +++ b/iotdb-client/odbc/.gitignore @@ -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. +# Root rules ignore *.txt and *.cmake; these are source inputs here. +!/CMakeLists.txt +!/cmake/*.cmake +!/test/CMakeLists.txt +!/licenses/*.txt +!/examples/python-example/requirements.txt +/target/ +/build/ +__pycache__/ +*.log diff --git a/iotdb-client/odbc/CMakeLists.txt b/iotdb-client/odbc/CMakeLists.txt new file mode 100644 index 0000000000000..5a72d7ab110be --- /dev/null +++ b/iotdb-client/odbc/CMakeLists.txt @@ -0,0 +1,149 @@ +# 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. +cmake_minimum_required(VERSION 3.20) +cmake_policy(SET CMP0091 NEW) +project(apache_iotdb_odbc VERSION 1.0.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR + NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|Linux)$" OR + NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" OR + CMAKE_GENERATOR_PLATFORM MATCHES "[Aa][Rr][Mm]") + message(FATAL_ERROR "ODBC release builds support Windows/Linux x64 only") +endif() +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() +if(MSVC) + # OpenSSL and Thrift are configured once per build directory. Avoid linking + # a Debug driver against dependencies built with the Release CRT. + set(CMAKE_CONFIGURATION_TYPES "${CMAKE_BUILD_TYPE}" CACHE STRING "Configurations" FORCE) +endif() +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +option(ODBC_BUILD_TESTS "Build ODBC tests" ON) +option(ODBC_ENABLE_LOGGING "Compile file logging support into the ODBC driver" OFF) +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/../pom.xml" _version_line REGEX "") +list(GET _version_line 0 _version_line) +string(REGEX REPLACE ".*([^<]+).*" "\\1" _repository_version "${_version_line}") +set(ODBC_PACKAGE_VERSION "${_repository_version}" CACHE STRING "Package version") +set(ODBC_PACKAGE_CLASSIFIER "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}" CACHE STRING "Release platform") + +# Scope SDK configuration to this build; its standalone defaults remain unchanged. +set(IOTDB_SESSION_STATIC ON CACHE BOOL "" FORCE) +set(IOTDB_SESSION_INSTALL OFF CACHE BOOL "" FORCE) +set(IOTDB_STATIC_CRT ON CACHE BOOL "" FORCE) +set(IOTDB_OPENSSL_STATIC ON CACHE BOOL "" FORCE) +set(IOTDB_OPENSSL_FROM_SOURCE ON CACHE BOOL "" FORCE) +set(WITH_SSL ON CACHE BOOL "" FORCE) +set(IOTDB_SESSION_VERSION "${ODBC_PACKAGE_VERSION}" CACHE STRING "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +add_subdirectory(../client-cpp client-cpp EXCLUDE_FROM_ALL) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include(FetchDependencies) + +set(DRIVER_VERSION "${ODBC_PACKAGE_VERSION}") +set(DEBUG_LOG_OUTPUT_PATH "apache_iotdb_odbc.log") +configure_file(src/DriverConfig.h.in generated/DriverConfig.h @ONLY) +set(ODBC_SOURCES + src/Catalog.cpp + src/ConnectionHandle.cpp + src/CopyField.cpp + src/DiagnosticManager.cpp + src/EnvironmentHandle.cpp + src/Log.cpp + src/ODBCField.cpp + src/ODBCHandle.cpp + src/ODBCRestResultSet.cpp + src/ODBCResultSet.cpp + src/ODBCSessionResultSet.cpp + src/StatementHandle.cpp + src/StatementUtils.cpp + src/Unicode.cpp + src/driver.cpp + src/rest_api_client.cpp + src/session_api_client.cpp) +add_library(odbc_objects OBJECT ${ODBC_SOURCES}) +target_include_directories(odbc_objects PUBLIC src "${CMAKE_CURRENT_BINARY_DIR}/generated") +target_link_libraries(odbc_objects PUBLIC iotdb_session CURL::libcurl nlohmann_json::nlohmann_json) +target_compile_definitions(odbc_objects PUBLIC CURL_STATICLIB) +add_library(apache_iotdb_odbc SHARED $) +target_link_libraries(apache_iotdb_odbc PRIVATE odbc_objects) +if(WIN32) + enable_language(RC) + target_compile_definitions(odbc_objects PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN) + target_compile_options(odbc_objects PUBLIC /utf-8) + target_sources(odbc_objects PRIVATE src/setup/setup_dialog.cpp) + target_sources(apache_iotdb_odbc PRIVATE src/setup/setup.rc src/driver.def) + target_link_libraries(odbc_objects PUBLIC odbccp32 legacy_stdio_definitions) + add_executable(install_driver src/install_driver.cpp) + target_compile_definitions(install_driver PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) + target_link_libraries(install_driver PRIVATE odbccp32 legacy_stdio_definitions) + install(TARGETS install_driver RUNTIME DESTINATION bin) + install(FILES scripts/windows/install.bat scripts/windows/uninstall.bat DESTINATION .) +else() + find_path(ODBC_INCLUDE_DIR sql.h REQUIRED) + find_library(ODBCINST_LIBRARY odbcinst REQUIRED) + target_include_directories(odbc_objects PUBLIC "${ODBC_INCLUDE_DIR}") + target_link_libraries(odbc_objects PUBLIC "${ODBCINST_LIBRARY}") + target_link_options(apache_iotdb_odbc PRIVATE -static-libstdc++ -static-libgcc + "LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/cmake/exports.map" + "LINKER:--exclude-libs,ALL" "LINKER:-z,defs" "LINKER:-Bsymbolic-functions") + set_property(TARGET apache_iotdb_odbc APPEND PROPERTY LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/exports.map") +endif() +install(TARGETS apache_iotdb_odbc RUNTIME DESTINATION bin LIBRARY DESTINATION lib) +install(FILES README.md DESTINATION .) +install(DIRECTORY conf/ DESTINATION conf) +install(FILES "${odbc_curl_SOURCE_DIR}/COPYING" DESTINATION licenses RENAME curl.txt) +install(FILES "${odbc_json_SOURCE_DIR}/LICENSE.MIT" DESTINATION licenses RENAME json.txt) +install(DIRECTORY ../client-cpp/src/assembly/package-metadata/third_party/ DESTINATION licenses/sdk) +install(FILES ../../LICENSE ../../NOTICE DESTINATION .) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # GCC runtime notices belong to the combined GNU/Linux binary, not the + # Apache source tree or the MSVC package. + configure_file(cmake/PackageGccRuntime.cmake.in generated/PackageGccRuntime.cmake @ONLY) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/generated/PackageGccRuntime.cmake") +endif() +get_filename_component(ODBC_REPOSITORY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." REALPATH) +# Container builds read the host checkout as root. Trust only this configured +# source repository for the provenance query, without changing global Git policy. +execute_process(COMMAND git -c "safe.directory=${ODBC_REPOSITORY_ROOT}" rev-parse HEAD + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + OUTPUT_VARIABLE ODBC_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) +if(NOT ODBC_COMMIT) + set(ODBC_COMMIT "unknown") +endif() +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/BUILD-INFO.txt" + "version=${ODBC_PACKAGE_VERSION}\ncommit=${ODBC_COMMIT}\n" + "platform=${ODBC_PACKAGE_CLASSIFIER}\ncompiler=${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\n" + "thrift=${THRIFT_VERSION}\nopenssl=${OPENSSL_FALLBACK_VERSION}\n" + "curl=${ODBC_CURL_VERSION}\njson=${ODBC_JSON_VERSION}\n" + "session.linkage=static\nopenssl.linkage=static\ncurl.linkage=static\n") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/BUILD-INFO.txt" DESTINATION .) + +if(ODBC_BUILD_TESTS) + enable_testing() + add_subdirectory(test) +endif() + +set(CPACK_GENERATOR ZIP) +set(CPACK_PACKAGE_FILE_NAME "iotdb-odbc-${ODBC_PACKAGE_VERSION}-${ODBC_PACKAGE_CLASSIFIER}") +set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON) +set(CPACK_PACKAGE_CHECKSUM SHA512) +include(CPack) diff --git a/iotdb-client/odbc/README.md b/iotdb-client/odbc/README.md new file mode 100644 index 0000000000000..a78c9d744a53a --- /dev/null +++ b/iotdb-client/odbc/README.md @@ -0,0 +1,203 @@ + +# IoTDB ODBC driver + +The driver builds directly against `../client-cpp`. Each release contains one +driver library with the Session client, Thrift, OpenSSL and curl linked statically. +It does not require a separately installed C++ SDK, OpenSSL or VC redistributable. +The OS libraries and ODBC manager/installer remain system prerequisites. + +## Build + +Use CMake 3.20+, a C++14 compiler and the C++ SDK build prerequisites. Windows +requires Visual Studio 2022 x64, Strawberry Perl and winflexbison3. Linux requires +unixODBC development headers. CMake downloads curl 8.4.0 and nlohmann/json 3.11.3 +into the build directory and verifies their SHA-256 checksums. The C++ SDK keeps +its dependency archives under `../client-cpp/third-party`; `-DIOTDB_OFFLINE=ON` +requires that SDK cache to be populated. + +```sh +cmake -S iotdb-client/odbc -B iotdb-client/odbc/target/build -DCMAKE_BUILD_TYPE=Release +cmake --build iotdb-client/odbc/target/build --config Release --parallel 4 +ctest --test-dir iotdb-client/odbc/target/build -C Release --output-on-failure +cd iotdb-client/odbc/target/build +cpack -C Release +``` + +On Windows pass `-A x64` to configure. Maven also supports +`./mvnw package -P with-odbc -pl iotdb-client/odbc`; CMake and CPack must be on PATH. +`-DskipTests` skips test execution. Builds do not register the driver on the host. + +For Linux release packages use the same manylinux 2.28 baseline as the C++ SDK: + +```sh +source .github/scripts/native-linux-env.sh +docker run --rm -v "$PWD:/workspace" -w /workspace "$NATIVE_LINUX_IMAGE" \ + bash .github/scripts/package-odbc-manylinux228.sh +``` + +The release script builds all dependencies in this environment, runs tests and +rejects unexpected shared-library dependencies or GLIBC requirements above 2.28. +A build on a newer host is a development build, not evidence of glibc 2.28 compatibility. +Release scope is Linux x86_64 and Windows x64 (Windows 10/11, Server 2016+). +Packages and SHA-512 checksums are written into the CMake build directory. +The dependency URLs, versions and checksums are exposed as `ODBC_CURL_*` and +`ODBC_JSON_*` CMake cache variables for controlled mirrors or version updates. +File logging is excluded from release builds by default; enable it explicitly +with `-DODBC_ENABLE_LOGGING=ON` when troubleshooting. + +GNU/Linux binary packages combine GCC's libstdc++ and libgcc runtime object code +under GPL-3.0-or-later WITH GCC-exception-3.1. The +[ASF GCC Runtime Library exception policy](https://www.apache.org/legal/resolved.html#gcc-runtime-library) +addresses this binary combination; the project PMC must review the inclusion. +No GCC runtime source or GPL license text is vendored in the IoTDB source tree. +At install/package time, GNU builds download the two license texts from the GCC +14.2.0 upstream release, verify pinned SHA-256 hashes and include them under +`licenses/gcc`, with a disclosure in the binary's `LICENSE`. See the +[GCC runtime licensing documentation](https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html). +Windows/MSVC packages do not include GCC runtime notices. Offline GNU packaging +requires prepopulating `/runtime-licenses/GPL-3.0.txt` and +`GCC-Runtime-Exception.txt` with those verified upstream files. + +## ODBC conformance + +The implementation reference is Microsoft's official +[ODBC Programmer's Reference](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-programmer-s-reference), +especially the [Core Interface Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/core-interface-conformance) +function requirements, API reference, state-transition +tables and diagnostic-record rules linked from that page. The driver uses the +ODBC 3.8 ABI and targets core interface conformance. + +Capability discovery is treated as a compatibility contract: `SQLGetFunctions` +reports only callable implementations, and `SQLGetInfo` reports only behavior +provided by the driver and IoTDB. Unsupported optional behavior returns a +standard SQLSTATE instead of being silently accepted. Core behavior includes +explicit and implicit descriptors, input parameter arrays (including +data-at-execution parameters), +rowset fetching, and the required catalog result schemas. The current intentional +optional limits are forward-only, read-only cursors; no true transactions, output +parameters, stored procedures, asynchronous execution or bookmarks. Applications +must use `SQLGetFunctions` and `SQLGetInfo` rather than infer optional support from +exported symbols. + +`SQL_C_NUMERIC` conversion supports INT32, INT64 and epoch TIMESTAMP values, +including INT64_MIN, with a default precision of 38 and scale of zero. +Bound numeric columns and `SQLGetData(SQL_ARD_TYPE)` honor the application row +descriptor's precision (1–38) and scale (-38–38). Fractional truncation returns +`SQL_SUCCESS_WITH_INFO`/`01S07`; magnitude overflow returns `22003`. +FLOAT/DOUBLE to `SQL_C_NUMERIC` is currently unsupported and returns `07006` +without modifying the output. Retrieve approximate values as `SQL_C_FLOAT` or +`SQL_C_DOUBLE` instead. `SQLDisconnect` drops all connection-owned statement and +descriptor handles; applications must allocate new ones after reconnecting. + +## Install + +Windows: extract the release, then run `install.bat "C:\Program Files\Apache IoTDB ODBC Driver"` +from an administrator command prompt. Use the **64-bit** ODBC administrator to +configure a DSN. `uninstall.bat` unregisters the driver, preserving DSNs and files. + +Linux: install unixODBC, extract the release, update `Driver` in `conf/odbcinst.ini` +to the absolute library path, then register with `odbcinst -i -d -f conf/odbcinst.ini`. +Copy the example DSN into your user `odbc.ini` and provide your credentials. + +## Connection and encryption + +Existing `SERVER`, `PORT`, `UID`, `PWD`, `DATABASE`, `ISTABLEMODEL`, `LOGLEVEL`, +`SESSIONTIMEOUTMS` and `BATCHSIZE` parameters remain available. `RESTFUL=1` selects +the retained HTTP REST implementation; specify its HTTP port explicitly. +The default is Session RPC on port 6667. Explicit connection attributes override +DSN values. Values containing semicolons can be braced, with `}}` escaping `}`. + +```text +DRIVER={Apache IoTDB ODBC Driver};SERVER=localhost;PORT=6667;UID=root;PWD={your password};DATABASE=information_schema;SSL=1;SSLCA={C:\certs\ca.pem}; +``` + +| Attribute | Default | Meaning | +| --- | --- | --- | +| SSL | 0 | Enable Session TLS; accepts 0/1, false/true, no/yes, off/on | +| SSLCA | empty | Trusted CA file in PEM format; required for TLS | +| SSLCERT | empty | PEM client certificate for mutual TLS | +| SSLKEY | empty | PEM client private key; required together with SSLCERT | + +These attributes are available in connection strings, DSNs and the Windows setup +dialog. Certificates are external files readable by the application account; +they are not compiled into the driver. The server must enable the corresponding +RPC TLS mode. The client verifies trust and server identity and never falls back +to plaintext. Connection testing and queries use the same Session construction. +Certificate attributes with SSL disabled are rejected. REST combined with these +TLS options is rejected: HTTPS is outside this migration's scope. + +## Validation + +CTest covers configuration, type conversions, diagnostics and loading through the +system ODBC manager. The manager test deliberately rejects an invalid TLS/REST +combination without contacting a server. Windows tests use a temporary user DSN +and do not install or modify a machine-wide ODBC driver registration. +To test a running server: + +```sh +export IOTDB_ODBC_CONNECTION='SERVER=localhost;PORT=6667;UID=root;PWD=your-password;ISTABLEMODEL=0' +target/build/test/odbc_manager /absolute/path/libapache_iotdb_odbc.so +``` + +Set `IOTDB_ODBC_QUERY` to exercise a query and `IOTDB_ODBC_EXPECT_FAILURE=1` for +negative connection tests. For plain/TLS/mTLS server phases, run +`python test/run_e2e.py --help`; use a disposable server distribution and test data. +The runner requires Java 17+, `keytool`, Python 3 and (on Linux) `lsof` or `netstat`. +Set `IOTDB_TEST_PASSWORD` to the test server password. It copies the supplied +distribution, refuses occupied test ports and removes the temporary copy afterward. + +The package workflow also builds an IoTDB server and tests the **extracted release +packages** in plain, TLS and mutual-TLS modes on Linux and Windows. Certificate +rejection tests cover missing trust material, wrong trust, missing/mismatched +client credentials and a trusted server certificate with the wrong identity. +`--windows-consumers` adds real `System.Data.Odbc` and MSDASQL/ADO tests, including +tree/table catalog browsing, parameterized inserts, Unicode, long text, NULL, +empty strings, DATE, BOOLEAN and Session batch boundaries. It creates temporary +user DSNs and disposable databases, then removes them. CTest additionally runs +public-ABI regressions against a local HTTP fixture for catalog schemas, +enumeration, patterns, Unicode lengths and REST type conversion. + +## Excel and other Unicode applications + +Use **64-bit Microsoft Excel** with the Windows x64 package and a 64-bit user or +system DSN. In Excel choose **Data > Get Data > From Other Sources > From ODBC**, +select the DSN and select a table in Navigator. For an explicit query use the +advanced SQL option, for example `SELECT time,device,val,txt,day FROM samples` in +the DSN's table-model database. SQL must follow IoTDB's dialect; optional ODBC SQL +escapes and transactions are not implemented. Refresh uses a forward-only, +read-only cursor. + +The driver exports Unicode `W` entry points and describes STRING/TEXT as +`SQL_WVARCHAR`/`SQL_WLONGVARCHAR`. Unicode applications therefore pass UTF-16 SQL +and retrieve UTF-16 data without the system ANSI code page corrupting Chinese or +other characters. Native applications should use `SQLDriverConnectW`, +`SQLExecDirectW` and `SQLPrepareW` through the Driver Manager. The driver's narrow +entry points accept UTF-8 directly; an ANSI application going through Windows' +Driver Manager is subject to its ANSI code-page conversion. TIMESTAMP query +columns retain epoch-integer semantics; DATE columns use `SQL_TYPE_DATE`. + +The Windows application tests exercise .NET and Office's MSDASQL/ADO data path. +GitHub-hosted runners do not include Excel; those tests are not an Excel or Power +Query UI test. Validate Navigator, loading into a worksheet and refresh on the +target Excel installation before claiming that particular Office version has +been tested. WPS registering `Excel.Application` does not establish Microsoft +Excel compatibility. diff --git a/iotdb-client/odbc/cmake/CheckLinuxPackage.cmake b/iotdb-client/odbc/cmake/CheckLinuxPackage.cmake new file mode 100644 index 0000000000000..cbd06c990e530 --- /dev/null +++ b/iotdb-client/odbc/cmake/CheckLinuxPackage.cmake @@ -0,0 +1,53 @@ +# 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. +if(NOT EXISTS "${DRIVER}") + message(FATAL_ERROR "Missing driver: ${DRIVER}") +endif() +if(NOT DEFINED GLIBC_BASELINE) + message(FATAL_ERROR "GLIBC_BASELINE must come from the shared native Linux release configuration") +endif() +find_program(READELF readelf REQUIRED) +execute_process(COMMAND "${READELF}" --version-info "${DRIVER}" + OUTPUT_VARIABLE versions RESULT_VARIABLE rc) +if(NOT rc EQUAL 0) + message(FATAL_ERROR "Cannot inspect ELF versions") +endif() +string(REGEX MATCHALL "GLIBC_[0-9]+\\.[0-9]+(\\.[0-9]+)?" requirements "${versions}") +if(NOT requirements) + message(FATAL_ERROR "Cannot determine GLIBC requirements") +endif() +foreach(requirement IN LISTS requirements) + string(REPLACE "GLIBC_" "" version "${requirement}") + if(version VERSION_GREATER GLIBC_BASELINE) + message(FATAL_ERROR "${DRIVER} requires ${requirement}, baseline is ${GLIBC_BASELINE}") + endif() +endforeach() +execute_process(COMMAND "${READELF}" -d "${DRIVER}" OUTPUT_VARIABLE dynamic RESULT_VARIABLE rc) +if(NOT rc EQUAL 0) + message(FATAL_ERROR "Cannot inspect ELF dependencies") +endif() +string(REGEX MATCHALL "Shared library: \\[[^]]+\\]" dependencies "${dynamic}") +foreach(dependency IN LISTS dependencies) + string(REGEX REPLACE "Shared library: \\[([^]]+)\\]" "\\1" name "${dependency}") + if(NOT name MATCHES "^(lib(c|m|dl|pthread|rt)\\.so\\.[0-9]+|libodbcinst\\.so\\.2|ld-linux-x86-64\\.so\\.2)$") + message(FATAL_ERROR "Unexpected runtime dependency: ${name}") + endif() +endforeach() +if(dynamic MATCHES "(RPATH|RUNPATH)") + message(FATAL_ERROR "Release driver must not contain a build-time library search path") +endif() +message(STATUS "Single-library dependency check and GLIBC <= ${GLIBC_BASELINE} passed") diff --git a/iotdb-client/odbc/cmake/FetchDependencies.cmake b/iotdb-client/odbc/cmake/FetchDependencies.cmake new file mode 100644 index 0000000000000..1515fde81c1d7 --- /dev/null +++ b/iotdb-client/odbc/cmake/FetchDependencies.cmake @@ -0,0 +1,91 @@ +# 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(FetchContent) + +set(ODBC_CURL_VERSION "8.4.0" CACHE STRING "curl version used by the ODBC driver") +string(REPLACE "." "_" _odbc_curl_tag_version "${ODBC_CURL_VERSION}") +set(ODBC_CURL_URL + "https://github.com/curl/curl/releases/download/curl-${_odbc_curl_tag_version}/curl-${ODBC_CURL_VERSION}.tar.xz" + CACHE STRING "curl source archive") +set(ODBC_CURL_SHA256 + "16c62a9c4af0f703d28bda6d7bbf37ba47055ad3414d70dec63e2e6336f2a82d" + CACHE STRING "curl source archive SHA-256") +set(ODBC_JSON_VERSION "3.11.3" CACHE STRING "nlohmann/json version used by the ODBC driver") +set(ODBC_JSON_URL + "https://github.com/nlohmann/json/releases/download/v${ODBC_JSON_VERSION}/json.tar.xz" + CACHE STRING "nlohmann/json source archive") +set(ODBC_JSON_SHA256 + "d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d" + CACHE STRING "nlohmann/json source archive SHA-256") + +# The REST transport only needs HTTP. Session TLS is provided by client-cpp. +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(BUILD_LIBCURL_DOCS OFF CACHE BOOL "" FORCE) +set(ENABLE_MANUAL OFF CACHE BOOL "" FORCE) +set(HTTP_ONLY ON CACHE BOOL "" FORCE) +set(CURL_USE_OPENSSL OFF CACHE BOOL "" FORCE) +set(CURL_USE_SCHANNEL OFF CACHE BOOL "" FORCE) +set(CURL_ENABLE_SSL OFF CACHE BOOL "" FORCE) +set(CURL_ZLIB OFF CACHE BOOL "" FORCE) +set(CURL_BROTLI OFF CACHE BOOL "" FORCE) +set(CURL_ZSTD OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE) +set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE) +set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE) +set(ENABLE_LDAP OFF CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE) +set(CURL_STATIC_CRT ${MSVC} CACHE BOOL "" FORCE) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) + +FetchContent_Declare( + odbc_curl + URL "${ODBC_CURL_URL}" + URL_HASH "SHA256=${ODBC_CURL_SHA256}" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) +FetchContent_Declare( + odbc_json + URL "${ODBC_JSON_URL}" + URL_HASH "SHA256=${ODBC_JSON_SHA256}" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + +# CMake 3.20 does not yet support FetchContent's EXCLUDE_FROM_ALL argument. +# Populate explicitly so dependency targets remain buildable while their +# install rules do not leak headers, archives, or tools into the ODBC package. +if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) +endif() + +function(odbc_make_dependency_available dependency) + FetchContent_GetProperties(${dependency}) + if(NOT ${dependency}_POPULATED) + FetchContent_Populate(${dependency}) + add_subdirectory( + "${${dependency}_SOURCE_DIR}" + "${${dependency}_BINARY_DIR}" + EXCLUDE_FROM_ALL) + endif() + set(${dependency}_SOURCE_DIR "${${dependency}_SOURCE_DIR}" PARENT_SCOPE) +endfunction() + +odbc_make_dependency_available(odbc_curl) +odbc_make_dependency_available(odbc_json) diff --git a/iotdb-client/odbc/cmake/PackageGccRuntime.cmake.in b/iotdb-client/odbc/cmake/PackageGccRuntime.cmake.in new file mode 100644 index 0000000000000..5ffee10235832 --- /dev/null +++ b/iotdb-client/odbc/cmake/PackageGccRuntime.cmake.in @@ -0,0 +1,66 @@ +# 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. + +# Only the combined GNU/Linux binary incorporates GCC runtime object code. +# Fetch notices at install/package time; never vendor runtime sources or license +# texts in the Apache source release. See the GCC Runtime Library exception at +# https://www.apache.org/legal/resolved.html#gcc-runtime-library +set(runtime_license_cache "@CMAKE_CURRENT_BINARY_DIR@/runtime-licenses") +file(MAKE_DIRECTORY "${runtime_license_cache}") + +function(package_gcc_license upstream_name package_name sha256) + set(cached "${runtime_license_cache}/${package_name}") + if(NOT EXISTS "${cached}") + if("@IOTDB_OFFLINE@") + message(FATAL_ERROR "Offline packaging requires ${cached}") + endif() + file(DOWNLOAD + "https://raw.githubusercontent.com/gcc-mirror/gcc/releases/gcc-14.2.0/${upstream_name}" + "${cached}.download" TLS_VERIFY ON STATUS download_status) + list(GET download_status 0 download_code) + if(NOT download_code EQUAL 0) + message(FATAL_ERROR "Cannot obtain GCC runtime notice ${upstream_name}: ${download_status}") + endif() + file(SHA256 "${cached}.download" actual_sha256) + if(NOT actual_sha256 STREQUAL sha256) + message(FATAL_ERROR "GCC runtime notice checksum mismatch: ${upstream_name}") + endif() + file(RENAME "${cached}.download" "${cached}") + endif() + file(SHA256 "${cached}" actual_sha256) + if(NOT actual_sha256 STREQUAL sha256) + message(FATAL_ERROR "Cached GCC runtime notice checksum mismatch: ${cached}") + endif() + file(INSTALL "${cached}" DESTINATION "${CMAKE_INSTALL_PREFIX}/licenses/gcc") +endfunction() + +package_gcc_license(COPYING3 GPL-3.0.txt + 8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903) +package_gcc_license(COPYING.RUNTIME GCC-Runtime-Exception.txt + 9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74) + +file(APPEND "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/LICENSE" "\n +GNU/Linux GCC runtime inclusion +------------------------------- +This binary contains libstdc++ and libgcc runtime object code combined with the +Apache IoTDB driver and its test executables by GCC. These runtime portions are +covered by GPL-3.0-or-later WITH GCC-exception-3.1; the IoTDB source remains under +the Apache License 2.0. See licenses/gcc/GPL-3.0.txt and +licenses/gcc/GCC-Runtime-Exception.txt for the corresponding license texts. +GCC homepage and upstream sources: https://gcc.gnu.org/ +Runtime license: https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html +") diff --git a/iotdb-client/odbc/cmake/exports.map b/iotdb-client/odbc/cmake/exports.map new file mode 100644 index 0000000000000..6f598e38c0c54 --- /dev/null +++ b/iotdb-client/odbc/cmake/exports.map @@ -0,0 +1,22 @@ +/** + * 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. + */ +{ + global: SQL*; ConfigDSN; + local: *; +}; diff --git a/iotdb-client/odbc/conf/odbc.ini b/iotdb-client/odbc/conf/odbc.ini new file mode 100644 index 0000000000000..dd7a3c6fcfffd --- /dev/null +++ b/iotdb-client/odbc/conf/odbc.ini @@ -0,0 +1,29 @@ +; 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. +[IoTDB] +Driver=Apache IoTDB ODBC Driver +SERVER=127.0.0.1 +PORT=6667 +UID=root +DATABASE=information_schema +ISTABLEMODEL=1 +SSL=0 +; For TLS set SSL=1 and SSLCA to a trusted PEM CA file. +; SSLCA=/etc/iotdb/ca.pem +; For mutual TLS also set both of these PEM file paths: +; SSLCERT=/etc/iotdb/client.pem +; SSLKEY=/etc/iotdb/client.key diff --git a/iotdb-client/odbc/conf/odbcinst.ini b/iotdb-client/odbc/conf/odbcinst.ini new file mode 100644 index 0000000000000..a447be86d7ae3 --- /dev/null +++ b/iotdb-client/odbc/conf/odbcinst.ini @@ -0,0 +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. +[Apache IoTDB ODBC Driver] +Description=Apache IoTDB ODBC Driver +Driver=/opt/iotdb-odbc/lib/libapache_iotdb_odbc.so +Threading=2 diff --git a/iotdb-client/odbc/examples/cpp-example/README.md b/iotdb-client/odbc/examples/cpp-example/README.md new file mode 100644 index 0000000000000..72748a8de89ec --- /dev/null +++ b/iotdb-client/odbc/examples/cpp-example/README.md @@ -0,0 +1,103 @@ + +# C++ ODBC Example + +This directory contains C++ example programs that connect to IoTDB using the Apache IoTDB ODBC driver. + +## Example Files + +| File | Description | +|------|-------------| +| **TableTest.cpp** | Table Model example. Uses table-style SQL such as `CREATE TABLE`, `INSERT INTO table`, suitable for IoTDB Table Model. | +| **TreeTest.cpp** | Tree Model example. Uses timeseries-style SQL such as `CREATE DATABASE`, `CREATE TIMESERIES`, `INSERT INTO root.xxx.xxx`, suitable for IoTDB native Tree Model. | + +### Table Model vs Tree Model + +- **Table Model**: Connection string defaults to or explicitly sets `istablemodel=1`. Uses `use database`, `CREATE TABLE`, `INSERT INTO tablename`; data is stored in tables. +- **Tree Model**: Connection string requires `istablemodel=0`. Uses full paths like `root.xxx.xxx`, creates timeseries via `CREATE TIMESERIES`; data is organized by device/measurement. + +## Prerequisites + +1. **Build Environment**: Visual Studio 2022 (or x64 Native Tools Command Prompt) +2. **ODBC Driver**: Apache IoTDB ODBC driver installed and configured, with DSN name `Apache IoTDB DSN` +3. **IoTDB Service**: IoTDB is running, default `127.0.0.1:6667` +4. **Dependencies**: `odbc32.lib` (Windows ODBC). When installing Visual Studio with the Windows SDK and C++ development tools options selected, `odbc32.lib` is automatically included. + +## Build and Run + +### 1. Open Command Prompt + +Open **x64 Native Tools Command Prompt for VS 2022** (or the corresponding version of VS Command Prompt). + +### 2. Navigate to Example Directory + +```bash +cd examples\cpp-example +``` + +### 3. Build + +**Table Model example:** +```bash +cl /EHsc /W4 /O2 TableTest.cpp /link odbc32.lib +``` + +**Tree Model example:** +```bash +cl /EHsc /W4 /O2 TreeTest.cpp /link odbc32.lib +``` + +### 4. Run + +**Table Model:** +```bash +TableTest +``` + +**Tree Model:** +```bash +TreeTest +``` + +## Connection String Parameters + +| Parameter | Description | Example | +|-----------|-------------|---------| +| DSN | Data Source Name | `Apache IoTDB DSN` | +| Server | IoTDB server address | `127.0.0.1` | +| UID | Username | `root` | +| PWD | Password | `root` | +| Database | Database name (Table Model) | `test` | +| loglevel | Log level 0–4 | `4` | +| istablemodel | 0=Tree Model, 1=Table Model | `0` or `1` | + +## Encoding Notes + +STRING and TEXT columns use UTF-8 encoding. Direct output in the Windows console may show garbled characters; you can redirect output to a file to view correctly: + +```bash +TreeTest > output.txt +``` + +## FAQ + +1. **Connection failed**: Check whether IoTDB is running, the DSN is configured correctly, and the Server/port are correct. +2. **Garbled characters**: If SQL statements contain non-ASCII characters, make sure the source file is saved with code page encoding. Alternatively, use English test data to avoid encoding issues. diff --git a/iotdb-client/odbc/examples/cpp-example/TableTest.cpp b/iotdb-client/odbc/examples/cpp-example/TableTest.cpp new file mode 100644 index 0000000000000..a31f4534777bc --- /dev/null +++ b/iotdb-client/odbc/examples/cpp-example/TableTest.cpp @@ -0,0 +1,534 @@ +/** + * 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 WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef SQL_DIAG_COLUMN_SIZE +#define SQL_DIAG_COLUMN_SIZE 33L +#endif + +// Error handling function +void CheckOdbcError(SQLRETURN retCode, SQLSMALLINT handleType, SQLHANDLE handle, + const char* functionName) { + if (retCode == SQL_SUCCESS || retCode == SQL_SUCCESS_WITH_INFO) { + return; + } + + SQLCHAR sqlState[6]; + SQLCHAR message[SQL_MAX_MESSAGE_LENGTH]; + SQLINTEGER nativeError; + SQLSMALLINT textLength; + SQLRETURN errRet; + errRet = SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError, message, sizeof(message), + &textLength); + + std::cerr << "ODBC Error in " << functionName << ":\n"; + std::cerr << " SQL State: " << sqlState << "\n"; + std::cerr << " Native Error: " << nativeError << "\n"; + std::cerr << " Message: " << message << "\n"; + std::cerr << " SQLGetDiagRec Return: " << errRet << "\n"; + + if (retCode == SQL_ERROR || retCode == SQL_INVALID_HANDLE) { + exit(1); + } +} + +// Simplified table output - display basic data only +void PrintSimpleTable(const std::vector& headers, + const std::vector>& rows) { + // Print header + for (size_t i = 0; i < headers.size(); i++) { + std::cout << headers[i]; + if (i < headers.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + + // Print separator + for (size_t i = 0; i < headers.size(); i++) { + std::cout << "----------------"; + if (i < headers.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + + // Print data rows + for (const auto& row : rows) { + for (size_t i = 0; i < row.size(); i++) { + std::cout << row[i]; + if (i < row.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + } + std::cout << std::endl; +} + +/// Execute a SELECT query and display fulltable results in table format +void Query(SQLHDBC hDbc) { + SQLHSTMT hStmt = SQL_NULL_HSTMT; + SQLRETURN ret = SQL_SUCCESS; + + try { + // Allocate statement handle + ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLAllocHandle(SQL_HANDLE_STMT)"); + return; + } + + // Execute query + const std::string sqlQuery = "select * from fulltable"; + std::cout << "Execute query: " << sqlQuery << std::endl; + + ret = SQLExecDirect(hStmt, reinterpret_cast(const_cast(sqlQuery.c_str())), + SQL_NTS); + if (!SQL_SUCCEEDED(ret)) { + if (ret != SQL_NO_DATA) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLExecDirect(SELECT)"); + } + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + // Get column count + SQLSMALLINT colCount = 0; + ret = SQLNumResultCols(hStmt, &colCount); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLNumResultCols"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::cout << "Column count = " << colCount << std::endl; + + // Return immediately if no columns + if (colCount <= 0) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + // Get column names and type information + std::vector columnNames; + std::vector columnTypes(colCount); + std::vector columnSizes(colCount); + std::vector decimalDigits(colCount); + std::vector nullable(colCount); + + // Get basic column information + for (SQLSMALLINT i = 1; i <= colCount; i++) { + SQLSMALLINT nameLength = 0; + ret = SQLDescribeCol(hStmt, i, NULL, 0, &nameLength, NULL, NULL, NULL, NULL); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get length)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::vector colNameBuffer(nameLength + 1); + SQLSMALLINT actualNameLength = 0; + + ret = SQLDescribeCol(hStmt, i, colNameBuffer.data(), nameLength + 1, &actualNameLength, NULL, + NULL, NULL, NULL); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get name)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::string fullName(reinterpret_cast(colNameBuffer.data())); + + size_t pos = fullName.find_last_of('.'); + if (pos != std::string::npos) { + columnNames.push_back(fullName.substr(pos + 1)); + } else { + columnNames.push_back(fullName); + } + + ret = SQLDescribeCol(hStmt, i, NULL, 0, NULL, &columnTypes[i - 1], &columnSizes[i - 1], + &decimalDigits[i - 1], &nullable[i - 1]); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get type info)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + } + + std::vector> tableRows; + + int rowCount = 0; + // Get data front every row + while (true) { + ret = SQLFetch(hStmt); + if (ret == SQL_NO_DATA) { + break; + } + + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLFetch"); + break; + } + + std::vector row; + + for (SQLSMALLINT i = 1; i <= colCount; i++) { + SQLLEN indicator = 0; + std::string valueStr; + + SQLSMALLINT cType; + size_t bufferSize; + bool isCharacterType = false; + const int maxBufferSize = 32768; + + switch (columnTypes[i - 1]) { + case SQL_CHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + cType = SQL_C_CHAR; + if (columnSizes[i - 1] > 0) { + bufferSize = min(maxBufferSize, static_cast(columnSizes[i - 1]) * 4 + 1); + } else { + bufferSize = maxBufferSize; + } + isCharacterType = true; + break; + + case SQL_DECIMAL: + case SQL_NUMERIC: + cType = SQL_C_CHAR; + if (columnSizes[i - 1] > 0) { + bufferSize = min(maxBufferSize, static_cast(columnSizes[i - 1]) * 4 + 1); + } else { + bufferSize = maxBufferSize; + } + isCharacterType = true; + break; + + case SQL_INTEGER: + case SQL_SMALLINT: + case SQL_TINYINT: + case SQL_BIGINT: + cType = SQL_C_SBIGINT; + bufferSize = sizeof(SQLBIGINT); + break; + + case SQL_REAL: + case SQL_FLOAT: + case SQL_DOUBLE: + cType = SQL_C_DOUBLE; + bufferSize = sizeof(double); + break; + + case SQL_BIT: + cType = SQL_C_BIT; + bufferSize = sizeof(SQLCHAR); + break; + + case SQL_DATE: + case SQL_TYPE_DATE: + cType = SQL_C_DATE; + bufferSize = sizeof(SQL_DATE_STRUCT); + break; + + case SQL_TIME: + case SQL_TYPE_TIME: + cType = SQL_C_TIME; + bufferSize = sizeof(SQL_TIME_STRUCT); + break; + + case SQL_TIMESTAMP: + case SQL_TYPE_TIMESTAMP: + cType = SQL_C_TIMESTAMP; + bufferSize = sizeof(SQL_TIMESTAMP_STRUCT); + break; + + default: + cType = SQL_C_CHAR; + bufferSize = 256; + isCharacterType = true; + break; + } + + std::vector buffer(bufferSize); + + ret = SQLGetData(hStmt, i, cType, buffer.data(), bufferSize, &indicator); + + if (indicator == SQL_NULL_DATA) { + valueStr = "NULL"; + } else if (ret != SQL_SUCCESS) { + valueStr = "ERR_CONV"; + } else { + if (cType == SQL_C_CHAR) { + valueStr = reinterpret_cast(buffer.data()); + } else if (cType == SQL_C_SBIGINT) { + SQLBIGINT intVal = *reinterpret_cast(buffer.data()); + valueStr = std::to_string(intVal); + } else if (cType == SQL_C_DOUBLE) { + double doubleVal = *reinterpret_cast(buffer.data()); + valueStr = std::to_string(doubleVal); + } else if (cType == SQL_C_BIT) { + valueStr = (*buffer.data() != 0) ? "TRUE" : "FALSE"; + } else if (cType == SQL_C_DATE) { + SQL_DATE_STRUCT* date = reinterpret_cast(buffer.data()); + char dateStr[20]; + snprintf(dateStr, sizeof(dateStr), "%04d-%02d-%02d", date->year, date->month, + date->day); + valueStr = dateStr; + } else if (cType == SQL_C_TIME) { + SQL_TIME_STRUCT* time = reinterpret_cast(buffer.data()); + char timeStr[15]; + snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", time->hour, time->minute, + time->second); + valueStr = timeStr; + } else if (cType == SQL_C_TIMESTAMP) { + SQL_TIMESTAMP_STRUCT* ts = reinterpret_cast(buffer.data()); + char tsStr[30]; + snprintf(tsStr, sizeof(tsStr), "%04d-%02d-%02d %02d:%02d:%02d.%06d", ts->year, + ts->month, ts->day, ts->hour, ts->minute, ts->second, ts->fraction / 1000); + valueStr = tsStr; + } else { + valueStr = "UNKNOWN_TYPE"; + } + + if (isCharacterType && ret == SQL_SUCCESS_WITH_INFO) { + SQLLEN actualSize = 0; + SQLGetDiagField(SQL_HANDLE_STMT, hStmt, 0, SQL_DIAG_COLUMN_SIZE, &actualSize, + SQL_IS_INTEGER, NULL); + + if (indicator > 0 && static_cast(indicator) > bufferSize - 1) { + valueStr += "..."; + } + } + } + + row.push_back(valueStr); + } + + tableRows.push_back(row); + } + + if (!tableRows.empty()) { + PrintSimpleTable(columnNames, tableRows); + } + + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } catch (const std::exception& ex) { + std::cerr << "Exception: " << ex.what() << std::endl; + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } catch (...) { + std::cerr << "Unknown exception occurred" << std::endl; + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } +} + +/// Execute a non-query SQL statement (e.g. CREATE DATABASE, CREATE TABLE, INSERT) +void Execute(SQLHDBC hDbc, const std::string& command) { + SQLHSTMT hStmt = SQL_NULL_HSTMT; + SQLRETURN ret; + + try { + // Allocate statement handle + ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLAllocHandle(SQL_HANDLE_STMT)"); + + // Execute command + ret = SQLExecDirect(hStmt, (SQLCHAR*)command.c_str(), SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLExecDirect"); + } + + // Free statement handle + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } catch (...) { + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } +} + +int main() { + SQLHENV hEnv = SQL_NULL_HENV; + SQLHDBC hDbc = SQL_NULL_HDBC; + SQLRETURN ret; + + try { + std::cout << "Start" << std::endl; + + // 1. Initialize ODBC environment + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLAllocHandle(SQL_HANDLE_ENV)"); + + ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLSetEnvAttr"); + + // 2. Establish connection + ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLAllocHandle(SQL_HANDLE_DBC)"); + + // Connection string + std::string dsn = "Apache IoTDB DSN"; + std::string user = "root"; + std::string password = "root"; + std::string server = "127.0.0.1"; + std::string database = "test"; + + std::string connectionString = "DSN=" + dsn + ";Server=" + server + ";UID=" + user + + ";PWD=" + password + ";Database=" + database + ";loglevel=4"; + std::cout << "Using connection string: " << connectionString << std::endl; + + SQLCHAR outConnStr[1024]; + SQLSMALLINT outConnStrLen; + + ret = SQLDriverConnect(hDbc, NULL, (SQLCHAR*)connectionString.c_str(), SQL_NTS, outConnStr, + sizeof(outConnStr), &outConnStrLen, SQL_DRIVER_COMPLETE); + + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Login failed" << std::endl; + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLDriverConnect"); + return 1; + } + + // Get driver name + SQLCHAR driverName[256]; + SQLSMALLINT nameLength; + ret = SQLGetInfo(hDbc, SQL_DRIVER_NAME, driverName, sizeof(driverName), &nameLength); + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLGetInfo"); + + std::cout << "Successfully opened connection. database name = " << driverName << std::endl; + + // 3. Execute operations + Execute(hDbc, "CREATE DATABASE IF NOT EXISTS test"); + Execute(hDbc, "use test"); + std::cout << "use test Execute complete. Begin to setup fulltable." << std::endl; + + // Create fulltable and insert test data + Execute(hDbc, + "CREATE TABLE IF NOT EXISTS fullTable (time TIMESTAMP TIME, bool_col BOOLEAN FIELD, " + "int32_col INT32 FIELD, int64_col INT64 FIELD, float_col FLOAT FIELD, double_col " + "DOUBLE FIELD, text_col TEXT FIELD, string_col STRING FIELD, blob_col BLOB FIELD, " + "timestamp_col TIMESTAMP FIELD, date_col DATE FIELD) WITH (TTL=315360000000)"); + const char* insertStatements[] = { + "INSERT INTO fulltable VALUES (1735689600000, true, 100, 10000000000, 36.5, 128.689, " + "'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689600000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689660000, false, 101, 10000000001, 36.6, 128.789, " + "'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689660000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689720000, true, 102, 10000000002, 36.7, 128.889, " + "'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689720000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689780000, false, 103, 10000000003, 36.8, 128.989, " + "'Device high temperature alert', 'DeviceA-Room1', '0x506C616E7444617462', 1735689780000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689840000, true, 104, 10000000004, 36.9, 129.089, " + "'Device status restored', 'DeviceA-Room1', '0x506C616E7444617461', 1735689840000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689900000, false, 105, 10000000005, 37.0, 129.189, " + "'Device running normally', 'DeviceB-Room2', '0x506C616E7444617463', 1735689900000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735689960000, true, 106, 10000000006, 37.1, 129.289, " + "'Device running normally', 'DeviceB-Room2', '0x506C616E7444617463', 1735689960000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690020000, false, 107, 10000000007, 37.2, 129.389, " + "'Device low humidity alert', 'DeviceB-Room2', '0x506C616E7444617464', 1735690020000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690080000, true, 108, 10000000008, 37.3, 129.489, " + "'Device status restored', 'DeviceB-Room2', '0x506C616E7444617463', 1735690080000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690140000, false, 109, 10000000009, 37.4, 129.589, " + "'Device running normally', 'DeviceC-Room3', '0x506C616E7444617465', 1735690140000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690200000, true, 110, 10000000010, 37.5, 129.689, " + "'Device running normally', 'DeviceC-Room3', '0x506C616E7444617465', 1735690200000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690260000, false, 111, 10000000011, 37.6, 129.789, " + "'Device unstable voltage alert', 'DeviceC-Room3', '0x506C616E7444617466', 1735690260000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690320000, true, 112, 10000000012, 37.7, 129.889, " + "'Device status restored', 'DeviceC-Room3', '0x506C616E7444617465', 1735690320000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690380000, false, 113, 10000000013, 37.8, 129.989, " + "'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690380000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690440000, true, 114, 10000000014, 37.9, 130.089, " + "'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690440000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690500000, false, 115, 10000000015, 38.0, 130.189, " + "'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690500000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690560000, true, 116, 10000000016, 38.1, 130.289, " + "'Device signal interrupted alert', 'DeviceD-Room4', '0x506C616E7444617468', " + "1735690560000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690620000, false, 117, 10000000017, 38.2, 130.389, " + "'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690620000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690680000, true, 118, 10000000018, 38.3, 130.489, " + "'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690680000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690740000, false, 119, 10000000019, 38.4, 130.589, " + "'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690740000, " + "'2026-01-04')", + "INSERT INTO fulltable VALUES (1735690790000, false, 119, 10000000019, 38.4, 130.589, " + "'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690740000, " + "'2026-01-04')"}; + for (const char* sql : insertStatements) { + Execute(hDbc, sql); + } + std::cout << "fulltable setup complete. Begin to query." << std::endl; + Query(hDbc); + std::cout << "Query ok" << std::endl; + + // 4. Clean up resources + SQLDisconnect(hDbc); + SQLFreeHandle(SQL_HANDLE_DBC, hDbc); + SQLFreeHandle(SQL_HANDLE_ENV, hEnv); + + return 0; + } catch (...) { + // Exception cleanup + if (hDbc != SQL_NULL_HDBC) { + SQLDisconnect(hDbc); + SQLFreeHandle(SQL_HANDLE_DBC, hDbc); + } + if (hEnv != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, hEnv); + } + + std::cerr << "Unexpected error!" << std::endl; + return 1; + } +} diff --git a/iotdb-client/odbc/examples/cpp-example/TreeTest.cpp b/iotdb-client/odbc/examples/cpp-example/TreeTest.cpp new file mode 100644 index 0000000000000..644d1daca48d5 --- /dev/null +++ b/iotdb-client/odbc/examples/cpp-example/TreeTest.cpp @@ -0,0 +1,525 @@ +/** + * 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 WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef SQL_DIAG_COLUMN_SIZE +#define SQL_DIAG_COLUMN_SIZE 33L +#endif + +void CheckOdbcError(SQLRETURN retCode, SQLSMALLINT handleType, SQLHANDLE handle, + const char* functionName) { + if (retCode == SQL_SUCCESS || retCode == SQL_SUCCESS_WITH_INFO) { + return; + } + + SQLCHAR sqlState[6]; + SQLCHAR message[SQL_MAX_MESSAGE_LENGTH]; + SQLINTEGER nativeError; + SQLSMALLINT textLength; + SQLRETURN errRet; + errRet = SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError, message, sizeof(message), + &textLength); + + std::cerr << "ODBC Error in " << functionName << ":\n"; + std::cerr << " SQL State: " << sqlState << "\n"; + std::cerr << " Native Error: " << nativeError << "\n"; + std::cerr << " Message: " << message << "\n"; + std::cerr << " SQLGetDiagRec Return: " << errRet << "\n"; + + if (retCode == SQL_ERROR || retCode == SQL_INVALID_HANDLE) { + exit(1); + } +} + +void PrintSimpleTable(const std::vector& headers, + const std::vector>& rows) { + for (size_t i = 0; i < headers.size(); i++) { + std::cout << headers[i]; + if (i < headers.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + + for (size_t i = 0; i < headers.size(); i++) { + std::cout << "----------------"; + if (i < headers.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + + for (const auto& row : rows) { + for (size_t i = 0; i < row.size(); i++) { + std::cout << row[i]; + if (i < row.size() - 1) + std::cout << "\t"; + } + std::cout << std::endl; + } + std::cout << std::endl; +} + +/// Execute a SELECT query and display results of root.full.fulldevice in table format +void Query(SQLHDBC hDbc) { + SQLHSTMT hStmt = SQL_NULL_HSTMT; + SQLRETURN ret = SQL_SUCCESS; + + try { + ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLAllocHandle(SQL_HANDLE_STMT)"); + return; + } + + const std::string sqlQuery = + "SELECT * FROM root.full.fulldevice WHERE time >= 1735689600000 AND time <= 1735690790000"; + std::cout << "Execute query: " << sqlQuery << std::endl; + + ret = SQLExecDirect(hStmt, reinterpret_cast(const_cast(sqlQuery.c_str())), + SQL_NTS); + if (!SQL_SUCCEEDED(ret)) { + if (ret != SQL_NO_DATA) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLExecDirect(SELECT)"); + } + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + SQLSMALLINT colCount = 0; + ret = SQLNumResultCols(hStmt, &colCount); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLNumResultCols"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::cout << "Column count = " << colCount << std::endl; + + if (colCount <= 0) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::vector columnNames; + std::vector columnTypes(colCount); + std::vector columnSizes(colCount); + std::vector decimalDigits(colCount); + std::vector nullable(colCount); + + // Get basic column information + for (SQLSMALLINT i = 1; i <= colCount; i++) { + SQLSMALLINT nameLength = 0; + ret = SQLDescribeCol(hStmt, i, NULL, 0, &nameLength, NULL, NULL, NULL, NULL); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get length)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::vector colNameBuffer(nameLength + 1); + SQLSMALLINT actualNameLength = 0; + + ret = SQLDescribeCol(hStmt, i, colNameBuffer.data(), nameLength + 1, &actualNameLength, NULL, + NULL, NULL, NULL); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get name)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + + std::string fullName(reinterpret_cast(colNameBuffer.data())); + + size_t pos = fullName.find_last_of('.'); + if (pos != std::string::npos) { + columnNames.push_back(fullName.substr(pos + 1)); + } else { + columnNames.push_back(fullName); + } + + ret = SQLDescribeCol(hStmt, i, NULL, 0, NULL, &columnTypes[i - 1], &columnSizes[i - 1], + &decimalDigits[i - 1], &nullable[i - 1]); + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLDescribeCol (get type info)"); + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + return; + } + } + + std::vector> tableRows; + + int rowCount = 0; + // Get data front every row + while (true) { + ret = SQLFetch(hStmt); + if (ret == SQL_NO_DATA) { + break; + } + + if (!SQL_SUCCEEDED(ret)) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLFetch"); + break; + } + + std::vector row; + + for (SQLSMALLINT i = 1; i <= colCount; i++) { + SQLLEN indicator = 0; + std::string valueStr; + + SQLSMALLINT cType; + size_t bufferSize; + bool isCharacterType = false; + const int maxBufferSize = 32768; + + switch (columnTypes[i - 1]) { + case SQL_CHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + cType = SQL_C_CHAR; + if (columnSizes[i - 1] > 0) { + bufferSize = min(maxBufferSize, static_cast(columnSizes[i - 1]) * 4 + 1); + } else { + bufferSize = maxBufferSize; + } + isCharacterType = true; + break; + + case SQL_DECIMAL: + case SQL_NUMERIC: + cType = SQL_C_CHAR; + if (columnSizes[i - 1] > 0) { + bufferSize = min(maxBufferSize, static_cast(columnSizes[i - 1]) * 4 + 1); + } else { + bufferSize = maxBufferSize; + } + isCharacterType = true; + break; + + case SQL_INTEGER: + case SQL_SMALLINT: + case SQL_TINYINT: + case SQL_BIGINT: + cType = SQL_C_SBIGINT; + bufferSize = sizeof(SQLBIGINT); + break; + + case SQL_REAL: + case SQL_FLOAT: + case SQL_DOUBLE: + cType = SQL_C_DOUBLE; + bufferSize = sizeof(double); + break; + + case SQL_BIT: + cType = SQL_C_BIT; + bufferSize = sizeof(SQLCHAR); + break; + + case SQL_DATE: + case SQL_TYPE_DATE: + cType = SQL_C_DATE; + bufferSize = sizeof(SQL_DATE_STRUCT); + break; + + case SQL_TIME: + case SQL_TYPE_TIME: + cType = SQL_C_TIME; + bufferSize = sizeof(SQL_TIME_STRUCT); + break; + + case SQL_TIMESTAMP: + case SQL_TYPE_TIMESTAMP: + cType = SQL_C_TIMESTAMP; + bufferSize = sizeof(SQL_TIMESTAMP_STRUCT); + break; + + default: + cType = SQL_C_CHAR; + bufferSize = 256; + isCharacterType = true; + break; + } + + std::vector buffer(bufferSize); + + ret = SQLGetData(hStmt, i, cType, buffer.data(), bufferSize, &indicator); + + if (indicator == SQL_NULL_DATA) { + valueStr = "NULL"; + } else if (ret != SQL_SUCCESS) { + valueStr = "ERR_CONV"; + } else { + if (cType == SQL_C_CHAR) { + valueStr = reinterpret_cast(buffer.data()); + } else if (cType == SQL_C_SBIGINT) { + SQLBIGINT intVal = *reinterpret_cast(buffer.data()); + valueStr = std::to_string(intVal); + } else if (cType == SQL_C_DOUBLE) { + double doubleVal = *reinterpret_cast(buffer.data()); + valueStr = std::to_string(doubleVal); + } else if (cType == SQL_C_BIT) { + valueStr = (*buffer.data() != 0) ? "TRUE" : "FALSE"; + } else if (cType == SQL_C_DATE) { + SQL_DATE_STRUCT* date = reinterpret_cast(buffer.data()); + char dateStr[20]; + snprintf(dateStr, sizeof(dateStr), "%04d-%02d-%02d", date->year, date->month, + date->day); + valueStr = dateStr; + } else if (cType == SQL_C_TIME) { + SQL_TIME_STRUCT* time = reinterpret_cast(buffer.data()); + char timeStr[15]; + snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", time->hour, time->minute, + time->second); + valueStr = timeStr; + } else if (cType == SQL_C_TIMESTAMP) { + SQL_TIMESTAMP_STRUCT* ts = reinterpret_cast(buffer.data()); + char tsStr[30]; + snprintf(tsStr, sizeof(tsStr), "%04d-%02d-%02d %02d:%02d:%02d.%06d", ts->year, + ts->month, ts->day, ts->hour, ts->minute, ts->second, ts->fraction / 1000); + valueStr = tsStr; + } else { + valueStr = "UNKNOWN_TYPE"; + } + + if (isCharacterType && ret == SQL_SUCCESS_WITH_INFO) { + SQLLEN actualSize = 0; + SQLGetDiagField(SQL_HANDLE_STMT, hStmt, 0, SQL_DIAG_COLUMN_SIZE, &actualSize, + SQL_IS_INTEGER, NULL); + + if (indicator > 0 && static_cast(indicator) > bufferSize - 1) { + valueStr += "..."; + } + } + } + + row.push_back(valueStr); + } + + tableRows.push_back(row); + } + + if (!tableRows.empty()) { + PrintSimpleTable(columnNames, tableRows); + } + + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } catch (const std::exception& ex) { + std::cerr << "Exception: " << ex.what() << std::endl; + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } catch (...) { + std::cerr << "Unknown exception occurred" << std::endl; + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } +} + +/// Execute a non-query SQL statement (e.g. INSERT; tree model INSERT auto-creates timeseries) +void Execute(SQLHDBC hDbc, const std::string& command) { + SQLHSTMT hStmt = SQL_NULL_HSTMT; + SQLRETURN ret; + + try { + ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLAllocHandle(SQL_HANDLE_STMT)"); + + ret = SQLExecDirect(hStmt, (SQLCHAR*)command.c_str(), SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + CheckOdbcError(ret, SQL_HANDLE_STMT, hStmt, "SQLExecDirect"); + } + + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } catch (...) { + if (hStmt != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } + throw; + } +} + +int main() { + SQLHENV hEnv = SQL_NULL_HENV; + SQLHDBC hDbc = SQL_NULL_HDBC; + SQLRETURN ret; + + try { + std::cout << "Start" << std::endl; + + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLAllocHandle(SQL_HANDLE_ENV)"); + + ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLSetEnvAttr"); + + ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc); + CheckOdbcError(ret, SQL_HANDLE_ENV, hEnv, "SQLAllocHandle(SQL_HANDLE_DBC)"); + + std::string dsn = "Apache IoTDB DSN"; + std::string user = "root"; + std::string password = "root"; + std::string server = "127.0.0.1"; + + std::string connectionString = "DSN=" + dsn + ";Server=" + server + ";UID=" + user + + ";PWD=" + password + ";loglevel=4;istablemodel=0"; + std::cout << "Using connection string: " << connectionString << std::endl; + + SQLCHAR outConnStr[1024]; + SQLSMALLINT outConnStrLen; + + ret = SQLDriverConnect(hDbc, NULL, (SQLCHAR*)connectionString.c_str(), SQL_NTS, outConnStr, + sizeof(outConnStr), &outConnStrLen, SQL_DRIVER_COMPLETE); + + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Login failed" << std::endl; + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLDriverConnect"); + return 1; + } + + SQLCHAR driverName[256]; + SQLSMALLINT nameLength; + ret = SQLGetInfo(hDbc, SQL_DRIVER_NAME, driverName, sizeof(driverName), &nameLength); + CheckOdbcError(ret, SQL_HANDLE_DBC, hDbc, "SQLGetInfo"); + + std::cout << "Successfully opened connection. database name = " << driverName << std::endl; + + const char* insertStatements[] = { + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689600000, true, " + "100, 10000000000, 36.5, 128.689, 'Device running normally', 'DeviceA-Room1', " + "1735689600000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689660000, false, " + "101, 10000000001, 36.6, 128.789, 'Device running normally', 'DeviceA-Room1', " + "1735689660000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689720000, true, " + "102, 10000000002, 36.7, 128.889, 'Device running normally', 'DeviceA-Room1', " + "1735689720000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689780000, false, " + "103, 10000000003, 36.8, 128.989, 'Device high temperature alert', 'DeviceA-Room1', " + "1735689780000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689840000, true, " + "104, 10000000004, 36.9, 129.089, 'Device status restored', 'DeviceA-Room1', " + "1735689840000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689900000, false, " + "105, 10000000005, 37.0, 129.189, 'Device running normally', 'DeviceB-Room2', " + "1735689900000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689960000, true, " + "106, 10000000006, 37.1, 129.289, 'Device running normally', 'DeviceB-Room2', " + "1735689960000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690020000, false, " + "107, 10000000007, 37.2, 129.389, 'Device low humidity alert', 'DeviceB-Room2', " + "1735690020000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690080000, true, " + "108, 10000000008, 37.3, 129.489, 'Device status restored', 'DeviceB-Room2', " + "1735690080000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690140000, false, " + "109, 10000000009, 37.4, 129.589, 'Device running normally', 'DeviceC-Room3', " + "1735690140000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690200000, true, " + "110, 10000000010, 37.5, 129.689, 'Device running normally', 'DeviceC-Room3', " + "1735690200000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690260000, false, " + "111, 10000000011, 37.6, 129.789, 'Device unstable voltage alert', 'DeviceC-Room3', " + "1735690260000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690320000, true, " + "112, 10000000012, 37.7, 129.889, 'Device status restored', 'DeviceC-Room3', " + "1735690320000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690380000, false, " + "113, 10000000013, 37.8, 129.989, 'Device running normally', 'DeviceD-Room4', " + "1735690380000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690440000, true, " + "114, 10000000014, 37.9, 130.089, 'Device running normally', 'DeviceD-Room4', " + "1735690440000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690500000, false, " + "115, 10000000015, 38.0, 130.189, 'Device running normally', 'DeviceD-Room4', " + "1735690500000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690560000, true, " + "116, 10000000016, 38.1, 130.289, 'Device signal interrupted alert', 'DeviceD-Room4', " + "1735690560000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690620000, false, " + "117, 10000000017, 38.2, 130.389, 'Device running normally', 'DeviceE-Room5', " + "1735690620000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690680000, true, " + "118, 10000000018, 38.3, 130.489, 'Device running normally', 'DeviceE-Room5', " + "1735690680000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690740000, false, " + "119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', " + "1735690740000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, " + "double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690790000, false, " + "119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', " + "1735690740000, '2026-01-04')"}; + for (const char* sql : insertStatements) { + Execute(hDbc, sql); + } + std::cout << "[DEBUG] Inserted 20 rows. Begin to query." << std::endl; + Query(hDbc); + std::cout << "Query ok" << std::endl; + + SQLDisconnect(hDbc); + SQLFreeHandle(SQL_HANDLE_DBC, hDbc); + SQLFreeHandle(SQL_HANDLE_ENV, hEnv); + + return 0; + } catch (...) { + if (hDbc != SQL_NULL_HDBC) { + SQLDisconnect(hDbc); + SQLFreeHandle(SQL_HANDLE_DBC, hDbc); + } + if (hEnv != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, hEnv); + } + + std::cerr << "Unexpected error!" << std::endl; + return 1; + } +} diff --git a/iotdb-client/odbc/examples/python-example/README.md b/iotdb-client/odbc/examples/python-example/README.md new file mode 100644 index 0000000000000..0a90b6fea96ec --- /dev/null +++ b/iotdb-client/odbc/examples/python-example/README.md @@ -0,0 +1,84 @@ + +# Python ODBC Example + +Python example programs that connect to the Apache IoTDB ODBC driver using pyodbc. For reference, see `examples/cpp-example` and `examples/BasicTest`. + +## Example Files + +| File | Description | +|------|-------------| +| **TableTest.py** | Table Model example. Uses table-style SQL such as `CREATE TABLE`, `INSERT INTO table`, suitable for IoTDB Table Model. | +| **TreeTest.py** | Tree Model example. Uses timeseries-style SQL such as `CREATE DATABASE`, `CREATE TIMESERIES`, `INSERT INTO root.xxx.xxx`, suitable for IoTDB native Tree Model. | + +### Table Model vs Tree Model + +- **Table Model**: Connection string defaults to or explicitly sets `istablemodel=1`. Uses `use database`, `CREATE TABLE`, `INSERT INTO tablename`; data is stored in tables. +- **Tree Model**: Connection string requires `istablemodel=0`. Uses full paths like `root.xxx.xxx`, creates timeseries via `CREATE TIMESERIES`; data is organized by device/measurement. + +## Install Dependencies + +```bash +pip install -r requirements.txt +``` + +## Prerequisites + +1. **ODBC Driver**: Apache IoTDB ODBC driver installed and configured, with DSN name `Apache IoTDB DSN` +2. **IoTDB Service**: IoTDB is running, default `127.0.0.1:6667` + +## Run + +### Table Model Example + +```bash +python TableTest.py +``` + +### Tree Model Example + +```bash +python TreeTest.py +``` + +## Function Descriptions + +- **execute**: Execute non-query SQL (e.g. `CREATE`, `INSERT`, `DELETE`) +- **query**: Execute a SELECT query and display results in table format +- **TableTest**: Demonstrates creation, insertion, and query of fulltable +- **TreeTest**: Demonstrates creation, insertion, and query of root.full.fulldevice + +## Connection Parameters + +| Parameter | Description | Example | +|-----------|-------------|---------| +| DSN | Data Source Name | `Apache IoTDB DSN` | +| Server | IoTDB server address | `127.0.0.1` | +| UID | Username | `root` | +| PWD | Password | `root` | +| Database | Database name (Table Model) | `test` | +| loglevel | Log level 0–4 | `4` | +| istablemodel | 0=Tree Model, 1=Table Model | `0` or `1` | + +## FAQ + +1. **Connection failed**: Check whether IoTDB is running, the DSN is configured correctly, and the Server/port are correct. +2. **Garbled characters**: If SQL statements contain non-ASCII characters, make sure the source file is saved with code page encoding. Alternatively, use English test data to avoid encoding issues. diff --git a/iotdb-client/odbc/examples/python-example/TableTest.py b/iotdb-client/odbc/examples/python-example/TableTest.py new file mode 100644 index 0000000000000..519f11a540365 --- /dev/null +++ b/iotdb-client/odbc/examples/python-example/TableTest.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# 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. +# -*- coding: utf-8 -*- +""" +Apache IoTDB ODBC Python example +Use pyodbc to connect to the IoTDB ODBC driver and perform operations such as query and insert. +For reference, see examples/cpp-example/test.cpp and examples/BasicTest/BasicTest/Program.cs +""" + +import pyodbc + + +def execute(conn: pyodbc.Connection, command: str) -> None: + """Execute a non-query SQL statement (e.g. USE, CREATE, INSERT, DELETE)""" + try: + with conn.cursor() as cursor: + cursor.execute(command) + # INSERT/UPDATE/DELETE require commit; session commands such as USE do not. + cmd_upper = command.strip().upper() + if cmd_upper.startswith(("INSERT", "UPDATE", "DELETE")): + conn.commit() + print(f"Execute command: {command}") + except pyodbc.Error as ex: + print(f"CommandText error: {ex}") + + +def query(conn: pyodbc.Connection, sql: str) -> None: + """Execute a SELECT query and display results in table format""" + try: + with conn.cursor() as cursor: + cursor.execute(sql) + col_count = len(cursor.description) + print(f"fCount = {col_count}") + + if col_count <= 0: + return + + # Get column names (if the name contains '.', take the last segment, consistent with C++/C# samples). + columns = [] + for i in range(col_count): + col_name = cursor.description[i][0] or f"Column{i}" + if "." in str(col_name): + col_name = str(col_name).split(".")[-1] + columns.append(str(col_name)) + + # Fetch data rows + rows = cursor.fetchall() + + # Simple table output + col_widths = [max(len(str(col)), 4) for col in columns] + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j < len(col_widths): + col_widths[j] = max(col_widths[j], len(str(val) if val is not None else "NULL")) + + # Print header + header = " | ".join(str(c).ljust(col_widths[i]) for i, c in enumerate(columns)) + print(header) + print("-" * len(header)) + + # Print data rows + for row in rows: + values = [] + for i, val in enumerate(row): + if val is None: + cell = "NULL" + else: + cell = str(val) + values.append(cell.ljust(col_widths[i]) if i < len(col_widths) else cell) + print(" | ".join(values)) + + print() + + except pyodbc.Error as ex: + print(f"Query error: {ex}") + + +def main() -> None: + dsn = "Apache IoTDB DSN" + user = "root" + password = "root" + server = "127.0.0.1" + database = "test" + connection_string = ( + f"DSN={dsn};Server={server};UID={user};PWD={password};" + f"Database={database};loglevel=4" + ) + + print("Start") + + try: + conn = pyodbc.connect(connection_string) + except pyodbc.Error as ex: + print(f"Login failed: {ex}") + return + + try: + driver_name = conn.getinfo(6) # SQL_DRIVER_NAME + print(f"Successfully opened connection. driver = {driver_name}") + except Exception: + print("Successfully opened connection.") + + try: + execute(conn, "CREATE DATABASE IF NOT EXISTS test") + execute(conn, "use test") + print("use test Execute complete. Begin to setup fulltable.") + + # Create the fulltable table and insert test data + execute( + conn, + "CREATE TABLE IF NOT EXISTS fullTable (time TIMESTAMP TIME, bool_col BOOLEAN FIELD, " + "int32_col INT32 FIELD, int64_col INT64 FIELD, float_col FLOAT FIELD, " + "double_col DOUBLE FIELD, text_col TEXT FIELD, string_col STRING FIELD, " + "blob_col BLOB FIELD, timestamp_col TIMESTAMP FIELD, date_col DATE FIELD) " + "WITH (TTL=315360000000)", + ) + insert_statements = [ + "INSERT INTO fulltable VALUES (1735689600000, true, 100, 10000000000, 36.5, 128.689, 'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689600000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689660000, false, 101, 10000000001, 36.6, 128.789, 'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689660000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689720000, true, 102, 10000000002, 36.7, 128.889, 'Device running normally', 'DeviceA-Room1', '0x506C616E7444617461', 1735689720000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689780000, false, 103, 10000000003, 36.8, 128.989, 'Device high temperature alert', 'DeviceA-Room1', '0x506C616E7444617462', 1735689780000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689840000, true, 104, 10000000004, 36.9, 129.089, 'Device status restored', 'DeviceA-Room1', '0x506C616E7444617461', 1735689840000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689900000, false, 105, 10000000005, 37.0, 129.189, 'Device running normally', 'DeviceB-Room2', '0x506C616E7444617463', 1735689900000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735689960000, true, 106, 10000000006, 37.1, 129.289, 'Device running normally', 'DeviceB-Room2', '0x506C616E7444617463', 1735689960000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690020000, false, 107, 10000000007, 37.2, 129.389, 'Device low humidity alert', 'DeviceB-Room2', '0x506C616E7444617464', 1735690020000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690080000, true, 108, 10000000008, 37.3, 129.489, 'Device status restored', 'DeviceB-Room2', '0x506C616E7444617463', 1735690080000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690140000, false, 109, 10000000009, 37.4, 129.589, 'Device running normally', 'DeviceC-Room3', '0x506C616E7444617465', 1735690140000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690200000, true, 110, 10000000010, 37.5, 129.689, 'Device running normally', 'DeviceC-Room3', '0x506C616E7444617465', 1735690200000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690260000, false, 111, 10000000011, 37.6, 129.789, 'Device unstable voltage alert', 'DeviceC-Room3', '0x506C616E7444617466', 1735690260000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690320000, true, 112, 10000000012, 37.7, 129.889, 'Device status restored', 'DeviceC-Room3', '0x506C616E7444617465', 1735690320000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690380000, false, 113, 10000000013, 37.8, 129.989, 'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690380000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690440000, true, 114, 10000000014, 37.9, 130.089, 'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690440000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690500000, false, 115, 10000000015, 38.0, 130.189, 'Device running normally', 'DeviceD-Room4', '0x506C616E7444617467', 1735690500000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690560000, true, 116, 10000000016, 38.1, 130.289, 'Device signal interrupted alert', 'DeviceD-Room4', '0x506C616E7444617468', 1735690560000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690620000, false, 117, 10000000017, 38.2, 130.389, 'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690620000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690680000, true, 118, 10000000018, 38.3, 130.489, 'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690680000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690740000, false, 119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690740000, '2026-01-04')", + "INSERT INTO fulltable VALUES (1735690790000, false, 119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', '0x506C616E7444617469', 1735690740000, '2026-01-04')", + ] + for insert_sql in insert_statements: + execute(conn, insert_sql) + print("fulltable setup complete. Begin to query.") + query(conn, "select * from fulltable") + print("Query ok") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/iotdb-client/odbc/examples/python-example/TreeTest.py b/iotdb-client/odbc/examples/python-example/TreeTest.py new file mode 100644 index 0000000000000..e1da4303ae7a0 --- /dev/null +++ b/iotdb-client/odbc/examples/python-example/TreeTest.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# 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. +# -*- coding: utf-8 -*- +""" +Apache IoTDB ODBC Python example - Tree Model +Uses pyodbc to connect to the IoTDB ODBC driver with istablemodel=0 for tree model. +Reference: examples/BasicTest/TreeTest/TreeTest.cs and examples/cpp-example/TreeTest.cpp. +""" + +import pyodbc + + +def execute(conn: pyodbc.Connection, command: str) -> None: + """Execute a non-query SQL statement (e.g. INSERT; tree model INSERT auto-creates timeseries)""" + try: + with conn.cursor() as cursor: + cursor.execute(command) + cmd_upper = command.strip().upper() + if cmd_upper.startswith(("INSERT", "UPDATE", "DELETE")): + conn.commit() + print(f"Execute command: {command}") + except pyodbc.Error as ex: + print(f"CommandText error: {ex}") + + +def query(conn: pyodbc.Connection, sql: str) -> None: + """Execute a SELECT query and display results of root.full.fulldevice in table format""" + try: + with conn.cursor() as cursor: + cursor.execute(sql) + col_count = len(cursor.description) + print(f"fCount = {col_count}") + + if col_count <= 0: + return + + columns = [] + for i in range(col_count): + col_name = cursor.description[i][0] or f"Column{i}" + if "." in str(col_name): + col_name = str(col_name).split(".")[-1] + columns.append(str(col_name)) + + rows = cursor.fetchall() + + col_widths = [max(len(str(col)), 4) for col in columns] + for row in rows: + for j, val in enumerate(row): + if j < len(col_widths): + col_widths[j] = max(col_widths[j], len(str(val) if val is not None else "NULL")) + + header = " | ".join(str(c).ljust(col_widths[i]) for i, c in enumerate(columns)) + print(header) + print("-" * len(header)) + + for row in rows: + values = [] + for i, val in enumerate(row): + if val is None: + cell = "NULL" + else: + cell = str(val) + values.append(cell.ljust(col_widths[i]) if i < len(col_widths) else cell) + print(" | ".join(values)) + + print() + + except pyodbc.Error as ex: + print(f"Query error: {ex}") + + +def main() -> None: + dsn = "Apache IoTDB DSN" + user = "root" + password = "root" + server = "127.0.0.1" + connection_string = ( + f"DSN={dsn};Server={server};UID={user};PWD={password};" + f"loglevel=4;istablemodel=0" + ) + + print("Start") + + try: + conn = pyodbc.connect(connection_string) + except pyodbc.Error as ex: + print(f"Login failed: {ex}") + return + + try: + driver_name = conn.getinfo(6) # SQL_DRIVER_NAME + print(f"Successfully opened connection. driver = {driver_name}") + except Exception: + print("Successfully opened connection.") + + try: + insert_statements = [ + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689600001, true, 100, 10000000000, 36.5, 128.689, 'Device running normally', 'DeviceA-Room1', 1735689600000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689660000, false, 101, 10000000001, 36.6, 128.789, 'Device running normally', 'DeviceA-Room1', 1735689660000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689720000, true, 102, 10000000002, 36.7, 128.889, 'Device running normally', 'DeviceA-Room1', 1735689720000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689780000, false, 103, 10000000003, 36.8, 128.989, 'Device high temperature alert', 'DeviceA-Room1', 1735689780000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689840000, true, 104, 10000000004, 36.9, 129.089, 'Device status restored', 'DeviceA-Room1', 1735689840000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689900000, false, 105, 10000000005, 37.0, 129.189, 'Device running normally', 'DeviceB-Room2', 1735689900000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735689960000, true, 106, 10000000006, 37.1, 129.289, 'Device running normally', 'DeviceB-Room2', 1735689960000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690020000, false, 107, 10000000007, 37.2, 129.389, 'Device low humidity alert', 'DeviceB-Room2', 1735690020000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690080000, true, 108, 10000000008, 37.3, 129.489, 'Device status restored', 'DeviceB-Room2', 1735690080000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690140000, false, 109, 10000000009, 37.4, 129.589, 'Device running normally', 'DeviceC-Room3', 1735690140000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690200000, true, 110, 10000000010, 37.5, 129.689, 'Device running normally', 'DeviceC-Room3', 1735690200000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690260000, false, 111, 10000000011, 37.6, 129.789, 'Device unstable voltage alert', 'DeviceC-Room3', 1735690260000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690320000, true, 112, 10000000012, 37.7, 129.889, 'Device status restored', 'DeviceC-Room3', 1735690320000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690380000, false, 113, 10000000013, 37.8, 129.989, 'Device running normally', 'DeviceD-Room4', 1735690380000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690440000, true, 114, 10000000014, 37.9, 130.089, 'Device running normally', 'DeviceD-Room4', 1735690440000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690500000, false, 115, 10000000015, 38.0, 130.189, 'Device running normally', 'DeviceD-Room4', 1735690500000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690560000, true, 116, 10000000016, 38.1, 130.289, 'Device signal interrupted alert', 'DeviceD-Room4', 1735690560000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690620000, false, 117, 10000000017, 38.2, 130.389, 'Device running normally', 'DeviceE-Room5', 1735690620000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690680000, true, 118, 10000000018, 38.3, 130.489, 'Device running normally', 'DeviceE-Room5', 1735690680000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690740000, false, 119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', 1735690740000, '2026-01-04')", + "INSERT INTO root.full.fulldevice(timestamp, bool_col, int32_col, int64_col, float_col, double_col, text_col, string_col, timestamp_col, date_col) VALUES (1735690790000, false, 119, 10000000019, 38.4, 130.589, 'Device running normally', 'DeviceE-Room5', 1735690740000, '2026-01-04')", + ] + for insert_sql in insert_statements: + execute(conn, insert_sql) + print(f"[DEBUG] Inserted {len(insert_statements)} rows. Begin to query.") + + query_sql = "SELECT * FROM root.full.fulldevice WHERE time >= 1735689600000 AND time <= 1735690790000" + query(conn, query_sql) + print("Query ok") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/iotdb-client/odbc/examples/python-example/requirements.txt b/iotdb-client/odbc/examples/python-example/requirements.txt new file mode 100644 index 0000000000000..35a46b8500847 --- /dev/null +++ b/iotdb-client/odbc/examples/python-example/requirements.txt @@ -0,0 +1,17 @@ +# 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. +pyodbc>=4.0.0 diff --git a/iotdb-client/odbc/pom.xml b/iotdb-client/odbc/pom.xml new file mode 100644 index 0000000000000..7e3f42ac0fbd2 --- /dev/null +++ b/iotdb-client/odbc/pom.xml @@ -0,0 +1,158 @@ + + + + 4.0.0 + + org.apache.iotdb + iotdb-client + 2.0.11-SNAPSHOT + + odbc + pom + IoTDB: Client: ODBC + + ${project.build.directory}/build + Release + ${os.classifier} + ON + + + + + org.codehaus.mojo + exec-maven-plugin + + + configure-odbc + generate-resources + + exec + + + cmake + + -S + ${project.basedir} + -B + ${odbc.build.dir} + -DCMAKE_BUILD_TYPE=${odbc.build.type} + -DODBC_PACKAGE_VERSION=${project.version} + -DODBC_PACKAGE_CLASSIFIER=${odbc.package.classifier} + -DODBC_BUILD_TESTS=${odbc.build.tests} + + + + + build-odbc + compile + + exec + + + cmake + + --build + ${odbc.build.dir} + --config + ${odbc.build.type} + + + + + test-odbc + test + + exec + + + ${skipTests} + ctest + + --test-dir + ${odbc.build.dir} + -C + ${odbc.build.type} + --output-on-failure + + + + + package-odbc + package + + exec + + + cpack + ${odbc.build.dir} + + -C + ${odbc.build.type} + + + + + + + + + + + spotless-cpp + + [17,) + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + + + src/**/*.h + src/**/*.hpp + src/**/*.c + src/**/*.cc + src/**/*.cpp + test/**/*.h + test/**/*.hpp + test/**/*.c + test/**/*.cc + test/**/*.cpp + examples/**/*.c + examples/**/*.cc + examples/**/*.cpp + + + ${clang.format.version} + + + + + + + + + + diff --git a/iotdb-client/odbc/scripts/windows/install.bat b/iotdb-client/odbc/scripts/windows/install.bat new file mode 100644 index 0000000000000..abb62643cb596 --- /dev/null +++ b/iotdb-client/odbc/scripts/windows/install.bat @@ -0,0 +1,28 @@ +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 +setlocal +if "%~1"=="" ( + echo Usage: install.bat "C:\Program Files\Apache IoTDB ODBC Driver" + exit /b 1 +) +if not exist "%~1" mkdir "%~1" +if errorlevel 1 exit /b 1 +copy /Y "%~dp0bin\apache_iotdb_odbc.dll" "%~1\apache_iotdb_odbc.dll" >nul +if errorlevel 1 exit /b 1 +"%~dp0bin\install_driver.exe" install "%~f1\apache_iotdb_odbc.dll" +exit /b %errorlevel% diff --git a/iotdb-client/odbc/scripts/windows/uninstall.bat b/iotdb-client/odbc/scripts/windows/uninstall.bat new file mode 100644 index 0000000000000..720f06d60711d --- /dev/null +++ b/iotdb-client/odbc/scripts/windows/uninstall.bat @@ -0,0 +1,19 @@ +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 +"%~dp0bin\install_driver.exe" uninstall +exit /b %errorlevel% diff --git a/iotdb-client/odbc/src/Catalog.cpp b/iotdb-client/odbc/src/Catalog.cpp new file mode 100644 index 0000000000000..86cd60da99491 --- /dev/null +++ b/iotdb-client/odbc/src/Catalog.cpp @@ -0,0 +1,346 @@ +/* + * 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 "driver.h" +#include +#include +#include + +namespace { +using Row = std::vector; +using Rows = std::vector; +const std::vector tableNames = {"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "TABLE_TYPE", + "REMARKS"}; +const std::vector columnNames = { + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "DATA_TYPE", + "TYPE_NAME", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "NUM_PREC_RADIX", + "NULLABLE", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", + "CHAR_OCTET_LENGTH", "ORDINAL_POSITION", "IS_NULLABLE"}; +const std::vector columnTypes = { + "STRING", "STRING", "STRING", "STRING", "INT16", "STRING", "INT32", "INT32", "INT16", + "INT16", "INT16", "STRING", "STRING", "INT16", "INT16", "INT32", "INT32", "STRING"}; + +std::string text(SQLCHAR* value, SQLSMALLINT length) { + return !value ? "" + : std::string(reinterpret_cast(value), + length == SQL_NTS ? std::strlen(reinterpret_cast(value)) + : static_cast(length)); +} + +bool validLength(SQLCHAR* value, SQLSMALLINT length) { + return !value || length >= 0 || length == SQL_NTS; +} + +size_t lastPathSeparator(const std::string& path) { + bool quoted = false; + size_t separator = std::string::npos; + for (size_t i = 0; i < path.size(); ++i) { + if (path[i] == '`') { + if (quoted && i + 1 < path.size() && path[i + 1] == '`') + ++i; + else + quoted = !quoted; + } else if (path[i] == '.' && !quoted) + separator = i; + } + return separator; +} + +bool matches(const std::string& value, SQLCHAR* pattern, SQLSMALLINT length, bool identifier) { + if (!pattern) + return true; + std::string requested = text(pattern, length); + if (identifier) { + auto lower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + return s; + }; + return lower(value) == lower(requested); + } + // Match ODBC patterns locally: never interpolate catalog arguments into SQL. + // Decode UTF-8 so '_' matches one Unicode character rather than one byte. + std::wstring_convert, char32_t> codec; + auto input = codec.from_bytes(value), filter = codec.from_bytes(requested); + std::vector previous(input.size() + 1), next(input.size() + 1); + previous[0] = true; + for (size_t i = 0; i < filter.size(); ++i) { + char32_t token = filter[i]; + bool escaped = token == U'\\' && i + 1 < filter.size(); + if (escaped) + token = filter[++i]; + std::fill(next.begin(), next.end(), false); + if (token == U'%' && !escaped) { + next[0] = previous[0]; + for (size_t j = 1; j <= input.size(); ++j) + next[j] = previous[j] || next[j - 1]; + } else { + for (size_t j = 1; j <= input.size(); ++j) + next[j] = previous[j - 1] && ((token == U'_' && !escaped) || token == input[j - 1]); + } + previous.swap(next); + } + return previous.back(); +} + +SQLRETURN query(StatementHandle* stmt, const std::string& sql, Rows& rows) { + SQLRETURN rc = IoTDB_ExecDirect(stmt, sql); + if (!SQL_SUCCEEDED(rc)) + return rc; + if (!stmt->resultSetPtr) + return SQL_ERROR; + rows = stmt->resultSetPtr->data; + if (!stmt->getConnection()->useRestful) { + auto* result = static_cast(stmt->resultSetPtr.get()); + // Convert every source batch before exposing the fixed catalog schema. + while (result->getNumRows() == stmt->getConnection()->batchSize && result->loadDataNextBatch()) + rows.insert(rows.end(), result->data.begin(), result->data.end()); + } + return SQL_SUCCESS; +} + +SQLRETURN result(StatementHandle* stmt, const std::vector& names, + const std::vector& types, Rows rows) { + stmt->ClearResultSet(); + stmt->AllocateSessionResultSet(); + stmt->resultSetPtr->columnNames = names; + stmt->resultSetPtr->columnTypes = types; + stmt->resultSetPtr->data = std::move(rows); + stmt->resultSetPtr->numRows = static_cast(stmt->resultSetPtr->data.size()); + stmt->resultSetPtr->numColumns = static_cast(names.size()); + stmt->resultSetPtr->isMetaData = true; + stmt->curRow = -1; + stmt->rowsReturned = 0; + stmt->isQuery = true; + stmt->isStream = false; + stmt->lastGetDataRow = -1; + stmt->getDataOffsets.clear(); + SQLSMALLINT count; + return SQLNumResultCols(stmt, &count); +} + +bool includesTables(std::string types) { + if (types.empty() || types == "%") + return true; + std::stringstream stream(types); + std::string type; + while (std::getline(stream, type, ',')) { + const size_t first = type.find_first_not_of(" '\t\r\n"); + const size_t last = type.find_last_not_of(" '\t\r\n"); + if (first != std::string::npos && type.substr(first, last - first + 1) == "TABLE") + return true; + } + return false; +} + +Row column(const std::string& catalog, const std::string& table, const std::string& name, + const std::string& type, bool time, const ODBCField& remark, int position) { + SQLSMALLINT sqlType = SQL_WVARCHAR; + int size = 2147483647, bytes = 2147483647; + ODBCField scale = ODBCField::null(), radix = ODBCField::null(), sub = ODBCField::null(); + ODBCField octets = ODBCField::null(); + if (type == "BOOLEAN") { + sqlType = SQL_BIT; + size = bytes = 1; + } else if (type == "INT32") { + sqlType = SQL_INTEGER; + size = 10; + bytes = 4; + scale = ODBCField(0); + radix = ODBCField(10); + } else if (type == "INT64" || type == "TIMESTAMP") { + // Session query metadata exposes timestamps as epoch integers. + sqlType = SQL_BIGINT; + size = 19; + bytes = 8; + scale = ODBCField(0); + radix = ODBCField(10); + } else if (type == "FLOAT" || type == "DOUBLE") { + sqlType = type == "FLOAT" ? SQL_REAL : SQL_DOUBLE; + size = type == "FLOAT" ? 7 : 15; + bytes = type == "FLOAT" ? 4 : 8; + radix = ODBCField(2); + } else if (type == "DATE") { + sqlType = SQL_TYPE_DATE; + size = 10; + bytes = sizeof(SQL_DATE_STRUCT); + sub = ODBCField(SQL_CODE_DATE); + } else { + sqlType = type == "BLOB" ? SQL_LONGVARBINARY : type == "TEXT" ? SQL_WLONGVARCHAR : SQL_WVARCHAR; + octets = ODBCField(bytes); + } + return {ODBCField(catalog), + ODBCField::null(), + ODBCField(table), + ODBCField(name), + ODBCField(sqlType), + ODBCField(type), + ODBCField(size), + ODBCField(bytes), + scale, + radix, + ODBCField(time ? SQL_NO_NULLS : SQL_NULLABLE), + remark, + ODBCField::null(), + ODBCField(type == "DATE" ? SQL_DATETIME : sqlType), + sub, + octets, + ODBCField(position), + ODBCField(time ? "NO" : "YES")}; +} +} // namespace + +SQLRETURN SQL_API SQLTables(SQLHSTMT handle, SQLCHAR* catalog, SQLSMALLINT catalogLength, + SQLCHAR* schema, SQLSMALLINT schemaLength, SQLCHAR* table, + SQLSMALLINT tableLength, SQLCHAR* types, SQLSMALLINT typesLength) { + if (!handle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(handle); + stmt->clearDiagnostics(); + if (!validLength(catalog, catalogLength) || !validLength(schema, schemaLength) || + !validLength(table, tableLength) || !validLength(types, typesLength)) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + try { + const std::string cat = text(catalog, catalogLength), sch = text(schema, schemaLength), + tbl = text(table, tableLength), typ = text(types, typesLength); + const bool emptyCatalog = catalog && cat.empty(), emptySchema = schema && sch.empty(), + emptyTable = table && tbl.empty(); + const std::vector tableTypes(5, "STRING"); + Rows rows, output; + if (cat == SQL_ALL_CATALOGS && emptySchema && emptyTable) { + auto rc = query(stmt, "SHOW DATABASES", rows); + if (!SQL_SUCCEEDED(rc)) + return rc; + for (const auto& row : rows) + output.push_back({row.at(0), ODBCField::null(), ODBCField::null(), ODBCField::null(), + ODBCField::null()}); + } else if (sch == SQL_ALL_SCHEMAS && emptyCatalog && emptyTable) { + // IoTDB has catalogs but no schemas; return the standard empty schema. + } else if (typ == SQL_ALL_TABLE_TYPES && emptyCatalog && emptySchema && emptyTable) { + output.push_back({ODBCField::null(), ODBCField::null(), ODBCField::null(), ODBCField("TABLE"), + ODBCField::null()}); + } else if (matches("", schema, schemaLength, stmt->metadataId) && includesTables(typ)) { + bool relational = stmt->getConnection()->isTableModel; + auto rc = + query(stmt, + relational ? "SELECT database, table_name, comment FROM information_schema.tables" + : "SHOW DEVICES WITH DATABASE", + rows); + if (!SQL_SUCCEEDED(rc)) + return rc; + for (const auto& row : rows) { + std::string database = row.at(relational ? 0 : 1).toString(); + std::string name = row.at(relational ? 1 : 0).toString(); + if (!relational && name.compare(0, database.size() + 1, database + ".") == 0) + name.erase(0, database.size() + 1); + if (matches(database, catalog, catalogLength, stmt->metadataId) && + matches(name, table, tableLength, stmt->metadataId)) + output.push_back({ODBCField(database), ODBCField::null(), ODBCField(name), + ODBCField("TABLE"), relational ? row.at(2) : ODBCField::null()}); + } + } + std::sort(output.begin(), output.end(), [](const Row& a, const Row& b) { + return std::make_tuple(a[3].toString(), a[0].toString(), a[2].toString()) < + std::make_tuple(b[3].toString(), b[0].toString(), b[2].toString()); + }); + return result(stmt, tableNames, tableTypes, std::move(output)); + } catch (const std::exception& error) { + stmt->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLColumns(SQLHSTMT handle, SQLCHAR* catalog, SQLSMALLINT catalogLength, + SQLCHAR* schema, SQLSMALLINT schemaLength, SQLCHAR* table, + SQLSMALLINT tableLength, SQLCHAR* name, SQLSMALLINT nameLength) { + if (!handle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(handle); + stmt->clearDiagnostics(); + if (!validLength(catalog, catalogLength) || !validLength(schema, schemaLength) || + !validLength(table, tableLength) || !validLength(name, nameLength)) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + try { + Rows rows, output; + if (!matches("", schema, schemaLength, stmt->metadataId)) + return result(stmt, columnNames, columnTypes, {}); + bool relational = stmt->getConnection()->isTableModel; + auto rc = query(stmt, + relational ? "SELECT database, table_name, column_name, datatype, category, " + "comment FROM information_schema.columns" + : "SHOW TIMESERIES", + rows); + if (!SQL_SUCCEEDED(rc)) + return rc; + auto source = stmt->resultSetPtr; + int timeseries = relational ? -1 : source->findColumnIndex("Timeseries"); + int databaseIndex = relational ? 0 : source->findColumnIndex("Database"); + int typeIndex = relational ? 3 : source->findColumnIndex("DataType"); + if (databaseIndex < 0 || typeIndex < 0 || (!relational && timeseries < 0)) + throw std::runtime_error("Missing catalog source columns"); + std::map, int> positions; + for (const auto& row : rows) { + std::string database = row.at(databaseIndex).toString(), device, field; + bool time = false; + if (relational) { + device = row.at(1).toString(); + field = row.at(2).toString(); + time = row.at(4).toString() == "TIME"; + } else { + std::string path = row.at(timeseries).toString(); + size_t split = lastPathSeparator(path); + device = path.substr(0, split); + field = path.substr(split + 1); + if (device.compare(0, database.size() + 1, database + ".") == 0) + device.erase(0, database.size() + 1); + } + auto key = std::make_pair(database, device); + int& position = positions[key]; + if (!relational && position == 0) { + position = 1; + output.push_back(column(database, device, "Time", "INT64", true, ODBCField::null(), 1)); + } + output.push_back(column(database, device, field, row.at(typeIndex).toString(), time, + relational ? row.at(5) : ODBCField::null(), ++position)); + } + output.erase( + std::remove_if(output.begin(), output.end(), + [&](const Row& row) { + return !matches(row[0].toString(), catalog, catalogLength, + stmt->metadataId) || + !matches(row[2].toString(), table, tableLength, stmt->metadataId) || + !matches(row[3].toString(), name, nameLength, stmt->metadataId); + }), + output.end()); + std::sort(output.begin(), output.end(), [](const Row& a, const Row& b) { + SQLINTEGER pa = 0, pb = 0; + a[16].toSLong(&pa); + b[16].toSLong(&pb); + return std::make_tuple(a[0].toString(), a[2].toString(), pa) < + std::make_tuple(b[0].toString(), b[2].toString(), pb); + }); + return result(stmt, columnNames, columnTypes, std::move(output)); + } catch (const std::exception& error) { + stmt->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } +} diff --git a/iotdb-client/odbc/src/ConnectionHandle.cpp b/iotdb-client/odbc/src/ConnectionHandle.cpp new file mode 100644 index 0000000000000..39b43abfa1cd7 --- /dev/null +++ b/iotdb-client/odbc/src/ConnectionHandle.cpp @@ -0,0 +1,291 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#include "ConnectionHandle.h" +#include "StatementHandle.h" +#include "Log.h" +#include +#include +#include + +ConnectionHandle::ConnectionHandle(EnvironmentHandle* environment) + : ODBCHandle(SQL_HANDLE_DBC), serverHostName("127.0.0.1"), serverPort("6667"), userName("root"), + password("root"), autoCommit(true), timeoutLogin(0), timeoutConnection(0), + environmentHandle(environment), database(""), logLevel(LOG_LEVEL_ERROR), isTableModel(true), + useRestful(false), sessionTimeoutMs(LLONG_MAX), batchSize(1000), sessionPtr(nullptr), + tableSessionPtr(nullptr) {} + +void ConnectionHandle::CloseSession() { + CloseChildren(); + if (sessionPtr) { + sessionPtr->close(); + sessionPtr = nullptr; + } + if (tableSessionPtr) { + tableSessionPtr->close(); + tableSessionPtr = nullptr; + } +} + +void ConnectionHandle::CloseChildren() { + // Release result sets while their Session and parent connection still exist. + // Statement destruction also removes its four implicit descriptors. + while (!statements.empty()) + delete statements.back(); + while (!descriptors.empty()) + delete descriptors.back(); +} + +ConnectionHandle::~ConnectionHandle() noexcept { + try { + CloseSession(); + } catch (...) { /* Never throw across SQLFreeHandle. */ + } +} + +void SetConnectionHandle(ConnectionHandle* cnct, std::string key, std::string value) { + std::stringstream logStream; + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return std::tolower(c); }); // Convert key to lowercase + + if (key == "ssl" || key == "restful") { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return std::tolower(c); }); + bool enabled = value == "1" || value == "true" || value == "yes" || value == "on"; + if (key == "ssl") { + cnct->invalidSslValue = + !enabled && value != "0" && value != "false" && value != "no" && value != "off"; + cnct->sslConfig.useSsl = enabled; + } else { + cnct->invalidRestfulValue = + !enabled && value != "0" && value != "false" && value != "no" && value != "off"; + cnct->useRestful = enabled; + } + } else if (key == "sslca") + cnct->sslConfig.trustCertFilePath = value; + else if (key == "sslcert") + cnct->sslConfig.clientCertificateFilePath = value; + else if (key == "sslkey") + cnct->sslConfig.clientPrivateKeyFilePath = value; + else if (key == "dsn" || key == "driver") { + } else if (key == "uid") { + logStream << "Set uid:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + cnct->userName = value; + } else if (key == "pwd") { + logStream << "Set pwd. (hidden)"; // Hidden for security reasons + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + cnct->password = value; + } else if (key == "server") { + logStream << "Set server:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + cnct->serverHostName = value; + } else if (key == "port") { + logStream << "Set port:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + cnct->serverPort = value; + } else if (key == "database") { + logStream << "Set database:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + cnct->database = value; + } else if (key == "loglevel") { + logStream << "Set loglevel:" << value; + logMessage(logStream.str()); + try { + size_t consumed = 0; + const int parsed = std::stoi(value, &consumed); + if (consumed != value.size() || parsed < LOG_LEVEL_ERROR || parsed > LOG_LEVEL_TRACE) + throw std::invalid_argument("LOGLEVEL must be between 0 and 4"); + cnct->logLevel = parsed; + } catch (const std::exception&) { + throw std::invalid_argument("LOGLEVEL must be between 0 and 4"); + } + } else if (key == "istablemodel" || key == "tablemodel") { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return std::tolower(c); }); // Convert value to lowercase + if (value == "0" || value == "false" || value == "no" || value == "off") { + cnct->isTableModel = false; + logStream << "Set isTableModel: false"; + } else if (value == "1" || value == "true" || value == "yes" || value == "on") { + cnct->isTableModel = true; + logStream << "Set isTableModel: true"; + } else { + logStream << "Invalid isTableModel value: " << value << ", defaulting to true"; + cnct->isTableModel = true; + } + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + } else if (key == "sessiontimeoutms") { + logStream << "Set sessionTimeoutMs:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + size_t consumed = 0; + cnct->sessionTimeoutMs = std::stoll(value, &consumed); + if (consumed != value.size() || cnct->sessionTimeoutMs < 0) + throw std::invalid_argument("SESSIONTIMEOUTMS must be a non-negative integer"); + if (cnct->sessionTimeoutMs == 0) + cnct->sessionTimeoutMs = LLONG_MAX; + } else if (key == "batchsize") { + logStream << "Set batchSize:" << value; + logMessage(cnct, logStream.str(), LOG_LEVEL_INFO); + size_t consumed = 0; + const long long parsed = std::stoll(value, &consumed); + if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits::max()) + throw std::invalid_argument("BATCHSIZE is out of range"); + cnct->batchSize = static_cast(parsed); + if (cnct->batchSize == 0) + cnct->batchSize = 1000; + } else { + logStream << "Currently unsupported connection parameter: key=" << key; + logMessage(cnct, logStream.str(), LOG_LEVEL_WARN); + } +} + +bool ConnectionHandle::LoadDsnFromOdbcIni(const std::string& dsnName) { + logMessage(this, "LoadDsnFromOdbcIni: Loading DSN '" + dsnName + "' from ODBC.INI", + LOG_LEVEL_INFO); + + if (dsnName.empty()) { + logMessage(this, "LoadDsnFromOdbcIni: DSN name is empty", LOG_LEVEL_WARN); + return false; + } + dataSourceName = dsnName; + + auto readKey = [&](const char* key, const char* defaultVal) -> std::string { + char buf[1024] = {}; + SQLGetPrivateProfileString(dsnName.c_str(), key, defaultVal, buf, sizeof(buf), "odbc.ini"); + return buf; + }; + + std::string val; + for (const char* key : {"SSL", "SSLCA", "SSLCERT", "SSLKEY", "RESTFUL"}) { + val = readKey(key, ""); + if (!val.empty()) + SetConnectionHandle(this, key, val); + } + + val = readKey("SERVER", ""); + if (!val.empty()) { + serverHostName = val; + logMessage(this, "LoadDsnFromOdbcIni: SERVER=" + val, LOG_LEVEL_DEBUG); + } + + val = readKey("PORT", ""); + if (!val.empty()) { + serverPort = val; + logMessage(this, "LoadDsnFromOdbcIni: PORT=" + val, LOG_LEVEL_DEBUG); + } + + val = readKey("UID", ""); + if (!val.empty()) { + userName = val; + logMessage(this, "LoadDsnFromOdbcIni: UID=" + val, LOG_LEVEL_DEBUG); + } + + val = readKey("PWD", ""); + if (!val.empty()) { + password = val; + logMessage(this, "LoadDsnFromOdbcIni: PWD=(hidden)", LOG_LEVEL_DEBUG); + } + + val = readKey("DATABASE", ""); + if (!val.empty()) { + database = val; + logMessage(this, "LoadDsnFromOdbcIni: DATABASE=" + val, LOG_LEVEL_DEBUG); + } + + val = readKey("ISTABLEMODEL", ""); + if (!val.empty()) { + SetConnectionHandle(this, "istablemodel", val); + } + + val = readKey("LOGLEVEL", ""); + if (!val.empty()) { + SetConnectionHandle(this, "loglevel", val); + } + + val = readKey("SESSIONTIMEOUTMS", ""); + if (!val.empty()) { + SetConnectionHandle(this, "sessiontimeoutms", val); + } + + val = readKey("BATCHSIZE", ""); + if (!val.empty()) { + SetConnectionHandle(this, "batchsize", val); + } + + std::ostringstream oss; + oss << "LoadDsnFromOdbcIni: Loaded - Server=" << serverHostName << ", Port=" << serverPort + << ", UID=" << userName << ", Database=" << database + << ", TableModel=" << (isTableModel ? "true" : "false") << ", LogLevel=" << logLevel + << ", SessionTimeout=" << sessionTimeoutMs << ", BatchSize=" << batchSize; + logMessage(this, oss.str(), LOG_LEVEL_INFO); + + return true; +} + +void ConnectionHandle::ValidateTransport() const { + if (invalidSslValue) + throw std::invalid_argument("SSL must be a boolean value"); + if (invalidRestfulValue) + throw std::invalid_argument("RESTFUL must be a boolean value"); + size_t consumed = 0; + long port = 0; + try { + port = std::stol(serverPort, &consumed); + } catch (const std::exception&) { + throw std::invalid_argument("PORT must be an integer between 1 and 65535"); + } + if (consumed != serverPort.size() || port < 1 || port > 65535) + throw std::invalid_argument("PORT must be an integer between 1 and 65535"); + bool hasCert = !sslConfig.clientCertificateFilePath.empty(); + bool hasKey = !sslConfig.clientPrivateKeyFilePath.empty(); + bool hasTlsOptions = + sslConfig.useSsl || hasCert || hasKey || !sslConfig.trustCertFilePath.empty(); + if (useRestful && hasTlsOptions) + throw std::invalid_argument("Session TLS options cannot be used with RESTFUL"); + if (hasTlsOptions && !sslConfig.useSsl) + throw std::invalid_argument("SSL=1 is required when certificate options are specified"); + if (hasCert != hasKey) + throw std::invalid_argument("SSLCERT and SSLKEY must be supplied together"); + sslConfig.validate(); +} + +void ConnectionHandle::OpenSession() { + ValidateTransport(); + int port = std::stoi(serverPort); + if (isTableModel) { + if (!tableSessionPtr) { + TableSessionBuilder builder; + builder.host(serverHostName) + ->rpcPort(port) + ->username(userName) + ->password(password) + ->database(database); + builder.sslConfig = sslConfig; + tableSessionPtr = builder.build(); + } + } else if (!sessionPtr) { + SessionBuilder builder; + builder.host(serverHostName)->rpcPort(port)->username(userName)->password(password); + builder.sslConfig = sslConfig; + sessionPtr = builder.build(); + } +} diff --git a/iotdb-client/odbc/src/ConnectionHandle.h b/iotdb-client/odbc/src/ConnectionHandle.h new file mode 100644 index 0000000000000..315d3cb4e1c0c --- /dev/null +++ b/iotdb-client/odbc/src/ConnectionHandle.h @@ -0,0 +1,80 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#ifndef CONNECTIONHANDLE_H +#define CONNECTIONHANDLE_H + +#include "Pch.h" +#include "ODBCHandle.h" +#include "EnvironmentHandle.h" +#include +#include +#include + +class StatementHandle; +class DescriptorHandle; + +class ConnectionHandle : public ODBCHandle { +public: + ConnectionHandle(EnvironmentHandle* environment); + ~ConnectionHandle() noexcept; + + EnvironmentHandle* getEnvironment() const { + return environmentHandle; + } + + std::string serverHostName; + std::string serverPort; + std::string userName; + std::string password; + bool autoCommit; + SQLUINTEGER timeoutLogin; + SQLUINTEGER timeoutConnection; + std::string database; + std::string dataSourceName; + std::vector statements; + std::vector descriptors; + int logLevel; + bool isTableModel; + bool useRestful; + SslConfig sslConfig; + bool invalidSslValue = false; + bool invalidRestfulValue = false; + void ValidateTransport() const; + void OpenSession(); + SQLBIGINT sessionTimeoutMs; + SQLINTEGER batchSize; + std::shared_ptr sessionPtr; + std::shared_ptr tableSessionPtr; + + void CloseSession(); + void CloseChildren(); + + bool LoadDsnFromOdbcIni(const std::string& dsnName); + +private: + EnvironmentHandle* environmentHandle; +}; + +void SetConnectionHandle(ConnectionHandle* cnct, std::string key, std::string value); + +#endif //CONNECTIONHANDLE_H diff --git a/iotdb-client/odbc/src/ConnectionString.h b/iotdb-client/odbc/src/ConnectionString.h new file mode 100644 index 0000000000000..e7a733530fddd --- /dev/null +++ b/iotdb-client/odbc/src/ConnectionString.h @@ -0,0 +1,77 @@ +/** + * 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 + +inline std::vector> +ParseConnectionString(const std::string& text) { + std::vector> values; + size_t pos = 0; + while (pos < text.size()) { + while (pos < text.size() && + (text[pos] == ';' || std::isspace(static_cast(text[pos])))) + ++pos; + if (pos == text.size()) + break; + auto equal = text.find('=', pos); + if (equal == std::string::npos || text.find(';', pos) < equal) + throw std::invalid_argument("Invalid connection attribute"); + std::string key = text.substr(pos, equal - pos); + while (!key.empty() && std::isspace(static_cast(key.back()))) + key.pop_back(); + if (key.empty()) + throw std::invalid_argument("Empty connection attribute"); + pos = equal + 1; + std::string value; + if (pos < text.size() && text[pos] == '{') { + ++pos; + bool closed = false; + while (pos < text.size()) { + char c = text[pos++]; + if (c == '}') { + if (pos < text.size() && text[pos] == '}') + ++pos; + else { + closed = true; + break; + } + } + value += c; + } + if (!closed) + throw std::invalid_argument("Unclosed connection attribute"); + while (pos < text.size() && std::isspace(static_cast(text[pos]))) + ++pos; + if (pos < text.size() && text[pos] != ';') + throw std::invalid_argument("Invalid braced connection attribute"); + } else { + auto end = text.find(';', pos); + if (end == std::string::npos) + end = text.size(); + value = text.substr(pos, end - pos); + pos = end; + } + values.emplace_back(std::move(key), std::move(value)); + } + return values; +} diff --git a/iotdb-client/odbc/src/CopyField.cpp b/iotdb-client/odbc/src/CopyField.cpp new file mode 100644 index 0000000000000..3705d83f71ad5 --- /dev/null +++ b/iotdb-client/odbc/src/CopyField.cpp @@ -0,0 +1,504 @@ +/** + * 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 +#ifdef _WIN32 +#include +#endif + +#include "ODBCField.h" +#include "Pch.h" +#include "driver.h" +#include "Log.h" +#include "ConnectionHandle.h" +#include "StatementHandle.h" +#include "DiagnosticManager.h" + +// UTF-8 to UTF-16/WCHAR conversion function +std::wstring utf8ToWide(const std::string& utf8Str) { +// Windows implementation (using MultiByteToWideChar) +#ifdef _WIN32 + if (utf8Str.empty()) + return L""; + + int size = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0); + + if (size == 0) { + throw std::runtime_error("UTF-8 to WideChar conversion failed"); + } + + std::wstring result(size, 0); + MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &result[0], size); + + result.resize(size - 1); // Remove extra null terminator + return result; + +// Linux/macOS implementation (using standard library) +#else + std::wstring_convert> converter; + return converter.from_bytes(utf8Str); +#endif +} + +static bool BuildCharStringForField(const ODBCField& value, std::string& out) { + SQLLEN len = 0; + if (!value.toChar(nullptr, 0, &len)) { + return false; + } + if (len == SQL_NULL_DATA) { + out.clear(); + return true; + } + std::vector buffer(static_cast(len) + 1); + if (!value.toChar(buffer.data(), static_cast(buffer.size()), &len)) { + return false; + } + out.assign(reinterpret_cast(buffer.data()), static_cast(len)); + return true; +} + +static bool BuildWCharStringForField(const ODBCField& value, std::basic_string& out) { + SQLLEN lenBytes = 0; + if (!value.toWChar(nullptr, 0, &lenBytes)) { + return false; + } + if (lenBytes == SQL_NULL_DATA) { + out.clear(); + return true; + } + size_t wcharCount = static_cast(lenBytes / sizeof(SQLWCHAR)); + std::vector buffer(wcharCount + 1); + if (!value.toWChar(buffer.data(), static_cast((wcharCount + 1) * sizeof(SQLWCHAR)), + &lenBytes)) { + return false; + } + out.assign(buffer.data(), buffer.data() + wcharCount); + return true; +} + +static bool BuildBinaryForField(const ODBCField& value, std::vector& out) { + SQLLEN len = 0; + if (!value.toBinary(nullptr, 0, &len)) { + return false; + } + if (len == SQL_NULL_DATA) { + out.clear(); + return true; + } + out.resize(static_cast(len)); + if (!value.toBinary(out.data(), len, &len)) { + return false; + } + return true; +} + +SQLRETURN CopyFieldToTargetVariable(StatementHandle* stmt, const ODBCField& value, + SQLSMALLINT targetType, SQLPOINTER targetValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr, + SQLUSMALLINT col) { + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + if (stmt->getDataOffsets.size() <= col) { + const int numCols = stmt->resultSetPtr ? stmt->resultSetPtr->getNumColumns() : 0; + stmt->getDataOffsets.assign(static_cast(numCols + 1), 0); + } + + SQLLEN& offset = stmt->getDataOffsets[col]; + + SQLLEN totalBytes = 0; + bool maxLengthApplied = false; + + std::string charValue; + std::basic_string wcharValue; + std::vector binaryValue; + + if (targetType == SQL_C_CHAR) { + if (!BuildCharStringForField(value, charValue)) { + stmt->addDiagnostic("22005", "Error converting to SQL_C_CHAR"); + return SQL_ERROR; + } + totalBytes = static_cast(charValue.size()); + } else if (targetType == SQL_C_WCHAR) { + if (!BuildWCharStringForField(value, wcharValue)) { + stmt->addDiagnostic("22005", "Error converting to SQL_C_WCHAR"); + return SQL_ERROR; + } + totalBytes = static_cast(wcharValue.size() * sizeof(SQLWCHAR)); + } else if (targetType == SQL_C_BINARY) { + if (!BuildBinaryForField(value, binaryValue)) { + stmt->addDiagnostic("22005", "Error converting to SQL_C_BINARY"); + return SQL_ERROR; + } + totalBytes = static_cast(binaryValue.size()); + } else { + stmt->addDiagnostic("HY003", "Invalid application buffer type"); + return SQL_ERROR; + } + + if (stmt->maxLength > 0 && totalBytes > static_cast(stmt->maxLength)) { + totalBytes = static_cast(stmt->maxLength); + maxLengthApplied = true; + } + + SQLLEN remaining = totalBytes - offset; + if (totalBytes == 0 && offset == 0) { + if (strLen_or_IndPtr) + *strLen_or_IndPtr = 0; + if (targetValuePtr) { + if (targetType == SQL_C_CHAR && bufferLength > 0) + *static_cast(targetValuePtr) = 0; + if (targetType == SQL_C_WCHAR && bufferLength >= static_cast(sizeof(SQLWCHAR))) + *static_cast(targetValuePtr) = 0; + offset = 1; + } + return SQL_SUCCESS; + } + if (remaining <= 0) { + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = 0; + } + return SQL_NO_DATA; + } + + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = remaining; + } + + SQLLEN copyLen = 0; + if (targetType == SQL_C_CHAR) { + SQLLEN maxPayload = (bufferLength > 1) ? (bufferLength - 1) : 0; + copyLen = (remaining < maxPayload) ? remaining : maxPayload; + if (targetValuePtr && bufferLength > 0) { + if (copyLen > 0) { + std::memcpy(targetValuePtr, charValue.data() + offset, static_cast(copyLen)); + } + static_cast(targetValuePtr)[copyLen] = '\0'; + } + } else if (targetType == SQL_C_WCHAR) { + SQLLEN maxPayload = (bufferLength >= static_cast(sizeof(SQLWCHAR))) + ? (bufferLength - static_cast(sizeof(SQLWCHAR))) + : 0; + copyLen = (remaining < maxPayload) ? remaining : maxPayload; + copyLen -= (copyLen % static_cast(sizeof(SQLWCHAR))); + if (targetValuePtr && bufferLength >= static_cast(sizeof(SQLWCHAR))) { + if (copyLen > 0) { + std::memcpy(targetValuePtr, reinterpret_cast(wcharValue.data()) + offset, + static_cast(copyLen)); + } + const SQLLEN wcharIndex = copyLen / static_cast(sizeof(SQLWCHAR)); + static_cast(targetValuePtr)[wcharIndex] = L'\0'; + } + } else if (targetType == SQL_C_BINARY) { + SQLLEN maxPayload = (bufferLength > 0) ? bufferLength : 0; + copyLen = (remaining < maxPayload) ? remaining : maxPayload; + if (targetValuePtr && copyLen > 0) { + std::memcpy(targetValuePtr, binaryValue.data() + offset, static_cast(copyLen)); + } + } + + if (copyLen > 0) { + offset += copyLen; + } + + if (remaining > copyLen) { + stmt->addDiagnostic("01004", "String data, right truncated"); + return SQL_SUCCESS_WITH_INFO; + } + + if (maxLengthApplied) { + return SQL_SUCCESS; + } + + return SQL_SUCCESS; +} + +SQLRETURN CopyFieldToTarget(StatementHandle* stmt, const ODBCField& value, SQLSMALLINT targetType, + SQLPOINTER targetValuePtr, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr, bool reportTruncation, SQLUSMALLINT col) { + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "CopyFieldToTarget: Copying field to target type: " + CDataTypeName(targetType), + LOG_LEVEL_TRACE); + logMessage(cnct, + "CopyFieldToTarget: Field Type: " + TSDataTypeName(value.getDataType()) + + ", Field value: " + value.toString(), + LOG_LEVEL_TRACE); + + auto reportTruncationIfNeeded = [&](SQLLEN fullLenBytes) -> SQLRETURN { + if (!reportTruncation) { + return SQL_SUCCESS; + } + if (bufferLength > 0 && fullLenBytes != SQL_NULL_DATA && bufferLength <= fullLenBytes) { + stmt->addDiagnostic("01004", "String data, right truncated"); + logMessage(cnct, "CopyFieldToTarget: Data truncated for column " + std::to_string(col), + LOG_LEVEL_INFO); + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; + }; + + switch (targetType) { + case SQL_C_CHAR: { + if (!value.toChar(static_cast(targetValuePtr), bufferLength, strLen_or_IndPtr)) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_CHAR", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_CHAR"); + return SQL_ERROR; + } + SQLLEN len = strLen_or_IndPtr ? *strLen_or_IndPtr : 0; + return reportTruncationIfNeeded(len); + } + case SQL_C_WCHAR: { + if (!value.toWChar(static_cast(targetValuePtr), bufferLength, strLen_or_IndPtr)) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_WCHAR", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_WCHAR"); + return SQL_ERROR; + } + SQLLEN len = strLen_or_IndPtr ? *strLen_or_IndPtr : 0; + return reportTruncationIfNeeded(len); + } + case SQL_C_SHORT: + case SQL_C_SSHORT: { + if (!value.toSShort(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_SSHORT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_SSHORT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLSMALLINT); + } + break; + } + case SQL_C_USHORT: { + if (!value.toUShort(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_USHORT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_USHORT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLUSMALLINT); + } + break; + } + case SQL_C_LONG: + case SQL_C_SLONG: { + if (!value.toSLong(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_SLONG", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_SLONG"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLINTEGER); + } + break; + } + case SQL_C_ULONG: { + if (!value.toULong(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_ULONG", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_ULONG"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLUINTEGER); + } + break; + } + case SQL_C_FLOAT: { + if (!value.toFloat(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_FLOAT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_FLOAT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLREAL); + } + break; + } + case SQL_C_DOUBLE: { + if (!value.toDouble(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_DOUBLE", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_DOUBLE"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLDOUBLE); + } + break; + } + case SQL_C_BIT: { + if (!value.toBit(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_BIT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_BIT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLCHAR); + } + break; + } + case SQL_C_TINYINT: + case SQL_C_STINYINT: { + if (!value.toSTinyInt(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_STINYINT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_STINYINT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLSCHAR); + } + break; + } + case SQL_C_UTINYINT: { + if (!value.toUTinyInt(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_UTINYINT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_UTINYINT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLCHAR); + } + break; + } + case SQL_BIGINT: + case SQL_C_SBIGINT: { + if (!value.toSBigInt(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_SBIGINT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_SBIGINT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLBIGINT); + } + break; + } + case SQL_C_UBIGINT: { + if (!value.toUBigInt(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_UBIGINT", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_UBIGINT"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLUBIGINT); + } + break; + } + case SQL_C_BINARY: { + if (!value.toBinary(static_cast(targetValuePtr), bufferLength, strLen_or_IndPtr)) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_BINARY", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_BINARY"); + return SQL_ERROR; + } + SQLLEN len = strLen_or_IndPtr ? *strLen_or_IndPtr : 0; + return reportTruncationIfNeeded(len); + } + case SQL_C_TYPE_DATE: { + if (!value.toDate(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_TYPE_DATE", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_TYPE_DATE"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQL_DATE_STRUCT); + } + break; + } + case SQL_C_TYPE_TIME: { + if (!value.toTime(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_TYPE_TIME", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_TYPE_TIME"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQL_TIME_STRUCT); + } + break; + } + case SQL_C_TYPE_TIMESTAMP: { + if (!value.toTimestamp(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_TYPE_TIMESTAMP", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_TYPE_TIMESTAMP"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQL_TIMESTAMP_STRUCT); + } + break; + } + case SQL_C_NUMERIC: { + const auto type = value.getDataType(); + if (type != TSDataType::INT32 && type != TSDataType::INT64 && type != TSDataType::TIMESTAMP) { + stmt->addDiagnostic("07006", + "Conversion to SQL_C_NUMERIC is supported only for integer types"); + return SQL_ERROR; + } + SQLSMALLINT precision = 38, scale = 0; + if (col) { + const auto* record = static_cast(stmt->appRowDesc)->findRecord(col); + if (record) { + if (record->precision) + precision = record->precision; + scale = record->scale; + } + } + if (precision < 1 || precision > 38 || scale < -38 || scale > 38) { + stmt->addDiagnostic("HY104", "Invalid precision or scale for SQL_C_NUMERIC"); + return SQL_ERROR; + } + bool truncated = false; + if (!value.toNumeric(static_cast(targetValuePtr), precision, scale, + &truncated)) { + stmt->addDiagnostic("22003", "Numeric value out of range"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQL_NUMERIC_STRUCT); + } + if (truncated) { + stmt->addDiagnostic("01S07", "Fractional truncation"); + return SQL_SUCCESS_WITH_INFO; + } + break; + } + case SQL_C_GUID: { + if (!value.toGUID(static_cast(targetValuePtr))) { + logMessage(cnct, "CopyFieldToTarget: Error converting to SQL_C_GUID", LOG_LEVEL_ERROR); + stmt->addDiagnostic("22005", "Error converting to SQL_C_GUID"); + return SQL_ERROR; + } + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = sizeof(SQLGUID); + } + break; + } + default: + logMessage(cnct, + "CopyFieldToTarget: Invalid application buffer type: " + CDataTypeName(targetType), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY003", "Invalid application buffer type"); + return SQL_ERROR; + } + + return SQL_SUCCESS; +} diff --git a/iotdb-client/odbc/src/CopyField.h b/iotdb-client/odbc/src/CopyField.h new file mode 100644 index 0000000000000..bc10630b7b3c0 --- /dev/null +++ b/iotdb-client/odbc/src/CopyField.h @@ -0,0 +1,39 @@ +/** + * 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. + */ +#ifndef COPYFIELD_H +#define COPYFIELD_H + +#include +#include "Pch.h" + +class StatementHandle; +class ODBCField; + +std::wstring utf8ToWide(const std::string& utf8Str); + +SQLRETURN CopyFieldToTargetVariable(StatementHandle* stmt, const ODBCField& value, + SQLSMALLINT targetType, SQLPOINTER targetValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr, + SQLUSMALLINT col); + +SQLRETURN CopyFieldToTarget(StatementHandle* stmt, const ODBCField& value, SQLSMALLINT targetType, + SQLPOINTER targetValuePtr, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr, bool reportTruncation, SQLUSMALLINT col); + +#endif // COPYFIELD_H diff --git a/iotdb-client/odbc/src/DescriptorHandle.h b/iotdb-client/odbc/src/DescriptorHandle.h new file mode 100644 index 0000000000000..c65819bc1d351 --- /dev/null +++ b/iotdb-client/odbc/src/DescriptorHandle.h @@ -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. + */ +#ifndef DESCRIPTORHANDLE_H +#define DESCRIPTORHANDLE_H + +#include "ODBCHandle.h" +#include "ConnectionHandle.h" + +class ConnectionHandle; +class StatementHandle; + +enum class DescriptorRole { + EXPLICIT, + APPLICATION_ROW, + IMPLEMENTATION_ROW, + APPLICATION_PARAM, + IMPLEMENTATION_PARAM +}; + +struct DescriptorRecord { + void setConciseType(SQLSMALLINT value) { + conciseType = type = value; + datetimeIntervalCode = 0; + if (value == SQL_TYPE_DATE || value == SQL_TYPE_TIME || value == SQL_TYPE_TIMESTAMP) { + type = SQL_DATETIME; + datetimeIntervalCode = value - SQL_TYPE_DATE + SQL_CODE_DATE; + } + } + + void updateConciseType() { + conciseType = type; + if (type == SQL_DATETIME && datetimeIntervalCode >= SQL_CODE_DATE && + datetimeIntervalCode <= SQL_CODE_TIMESTAMP) + conciseType = SQL_TYPE_DATE + datetimeIntervalCode - SQL_CODE_DATE; + } + + SQLSMALLINT conciseType = SQL_C_DEFAULT; + SQLSMALLINT type = SQL_C_DEFAULT; + SQLSMALLINT datetimeIntervalCode = 0; + SQLLEN octetLength = 0; + SQLULEN length = 0; + SQLSMALLINT precision = 0; + SQLSMALLINT scale = 0; + SQLSMALLINT nullable = SQL_NULLABLE_UNKNOWN; + SQLSMALLINT parameterType = SQL_PARAM_INPUT; + SQLSMALLINT unnamed = SQL_UNNAMED; + SQLINTEGER numPrecRadix = 0; + SQLPOINTER dataPtr = nullptr; + SQLLEN* indicatorPtr = nullptr; + SQLLEN* octetLengthPtr = nullptr; + std::string name; +}; + +class DescriptorHandle : public ODBCHandle { +public: + DescriptorHandle(ConnectionHandle* connection, DescriptorRole role, + StatementHandle* owner = nullptr) + : ODBCHandle(SQL_HANDLE_DESC), connection(connection), owner(owner), role(role), + implicit(role != DescriptorRole::EXPLICIT) { + if (connection) + connection->descriptors.push_back(this); + } + + ~DescriptorHandle() { + if (connection) { + auto& descriptors = connection->descriptors; + descriptors.erase(std::remove(descriptors.begin(), descriptors.end(), this), + descriptors.end()); + } + } + + DescriptorRecord& record(SQLSMALLINT number) { + if (records.size() < static_cast(number)) + records.resize(static_cast(number)); + return records[static_cast(number - 1)]; + } + + const DescriptorRecord* findRecord(SQLSMALLINT number) const { + return number > 0 && static_cast(number) <= records.size() + ? &records[static_cast(number - 1)] + : nullptr; + } + + DescriptorRecord* findRecord(SQLSMALLINT number) { + return number > 0 && static_cast(number) <= records.size() + ? &records[static_cast(number - 1)] + : nullptr; + } + + ConnectionHandle* connection; + StatementHandle* owner; + DescriptorRole role; + bool implicit; + SQLULEN arraySize = 1; + SQLUSMALLINT* arrayStatusPtr = nullptr; + SQLLEN* bindOffsetPtr = nullptr; + SQLULEN bindType = SQL_BIND_BY_COLUMN; + SQLSMALLINT count = 0; + SQLULEN* rowsProcessedPtr = nullptr; + std::vector records; +}; + +#endif // DESCRIPTORHANDLE_H diff --git a/iotdb-client/odbc/src/DiagnosticManager.cpp b/iotdb-client/odbc/src/DiagnosticManager.cpp new file mode 100644 index 0000000000000..1106c3f74f1f2 --- /dev/null +++ b/iotdb-client/odbc/src/DiagnosticManager.cpp @@ -0,0 +1,63 @@ +/** + * 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 "DiagnosticManager.h" + +#include + +DiagnosticRecord::DiagnosticRecord(std::string state, const std::string& msg, int error) + : sqlState(std::move(state)), message(msg), nativeError(error) {} + +DiagnosticManager::DiagnosticManager() = default; + +DiagnosticManager::~DiagnosticManager() = default; + +void DiagnosticManager::addDiagnostic(const std::string& sqlState, const std::string& message, + int nativeError) { + std::lock_guard lock(mutex); + diagnosticRecords.emplace_back(sqlState, message, nativeError); +} + +bool DiagnosticManager::getDiagnostic(const int index, std::string& sqlState, std::string& message, + int& nativeError) { + std::lock_guard lock(mutex); + if (index <= 0 || index > static_cast(diagnosticRecords.size())) { + return false; + } + const DiagnosticRecord& record = diagnosticRecords[index - 1]; + sqlState = record.sqlState; + message = record.message; + nativeError = record.nativeError; + return true; +} + +void DiagnosticManager::clearDiagnostics() { + std::lock_guard lock(mutex); + diagnosticRecords.clear(); +} + +size_t DiagnosticManager::diagnosticCount() { + std::lock_guard lock(mutex); + return diagnosticRecords.size(); +} + +void DiagnosticManager::consumeFirstDiagnostic() { + std::lock_guard lock(mutex); + if (!diagnosticRecords.empty()) + diagnosticRecords.pop_front(); +} diff --git a/iotdb-client/odbc/src/DiagnosticManager.h b/iotdb-client/odbc/src/DiagnosticManager.h new file mode 100644 index 0000000000000..62d48ee933d05 --- /dev/null +++ b/iotdb-client/odbc/src/DiagnosticManager.h @@ -0,0 +1,58 @@ +/** + * 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. + */ +#ifndef DIAGNOSTIC_MANAGER_H +#define DIAGNOSTIC_MANAGER_H + +#include "Pch.h" + +// Class representing a single diagnostic record +class DiagnosticRecord { +public: + std::string sqlState; + std::string message; + int nativeError; + + DiagnosticRecord(std::string state, const std::string& msg, int error = 0); +}; + +// Class managing a queue of diagnostic records +class DiagnosticManager { + + std::deque diagnosticRecords; + std::mutex mutex; + +public: + DiagnosticManager(); + ~DiagnosticManager(); + + // Adds a new diagnostic record + void addDiagnostic(const std::string& sqlState, const std::string& message, int nativeError = 0); + + // Retrieves a diagnostic record by index (1-based) + bool getDiagnostic(int index, std::string& sqlState, std::string& message, int& nativeError); + + // Clears all diagnostic records + void clearDiagnostics(); + void consumeFirstDiagnostic(); + + // Returns the count of diagnostic records + size_t diagnosticCount(); +}; + +#endif // DIAGNOSTIC_MANAGER_H diff --git a/iotdb-client/odbc/src/DriverConfig.h.in b/iotdb-client/odbc/src/DriverConfig.h.in new file mode 100644 index 0000000000000..8ea7e4d7eb330 --- /dev/null +++ b/iotdb-client/odbc/src/DriverConfig.h.in @@ -0,0 +1,25 @@ +/** + * 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 + +// Driver version information +#define DRIVER_VERSION "@DRIVER_VERSION@" + +#define DEBUG_LOG_OUTPUT_PATH "@DEBUG_LOG_OUTPUT_PATH@" +#cmakedefine01 ODBC_ENABLE_LOGGING diff --git a/iotdb-client/odbc/src/EnvironmentHandle.cpp b/iotdb-client/odbc/src/EnvironmentHandle.cpp new file mode 100644 index 0000000000000..6baabe8cf7d75 --- /dev/null +++ b/iotdb-client/odbc/src/EnvironmentHandle.cpp @@ -0,0 +1,25 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#include "EnvironmentHandle.h" + +// EnvironmentHandle implementation - currently no additional implementation needed diff --git a/iotdb-client/odbc/src/EnvironmentHandle.h b/iotdb-client/odbc/src/EnvironmentHandle.h new file mode 100644 index 0000000000000..2a3f9e81e7dc6 --- /dev/null +++ b/iotdb-client/odbc/src/EnvironmentHandle.h @@ -0,0 +1,37 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#ifndef ENVIRONMENTHANDLE_H +#define ENVIRONMENTHANDLE_H + +#include "Pch.h" +#include "ODBCHandle.h" + +class EnvironmentHandle : public ODBCHandle { +public: + EnvironmentHandle() : ODBCHandle(SQL_HANDLE_ENV), odbcVersion(0) {} + + SQLINTEGER odbcVersion; + // Add more fields if needed +}; + +#endif //ENVIRONMENTHANDLE_H diff --git a/iotdb-client/odbc/src/Log.cpp b/iotdb-client/odbc/src/Log.cpp new file mode 100644 index 0000000000000..6dfc0cefccce9 --- /dev/null +++ b/iotdb-client/odbc/src/Log.cpp @@ -0,0 +1,360 @@ +/** + * 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 "Pch.h" +#include "Log.h" + +// Additional standard library includes not in Pch.h +#include +#include +#include +#include + +#include +#include + +// Project includes +#include "DriverConfig.h" + +static std::ofstream logFile; +static std::once_flag logFileOnce; +static std::mutex logMutex; + +static void EnsureLogFileOpen() { +#ifdef WIN32 + HMODULE hMod = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&logMessageInternal), &hMod)) { + logFile.open(DEBUG_LOG_OUTPUT_PATH, std::ios::app); + return; + } + char dllPath[MAX_PATH] = {}; + if (GetModuleFileNameA(hMod, dllPath, MAX_PATH) == 0) { + logFile.open(DEBUG_LOG_OUTPUT_PATH, std::ios::app); + return; + } + std::string dir(dllPath); + const size_t lastSlash = dir.find_last_of("\\/"); + if (lastSlash != std::string::npos) + dir.resize(lastSlash); + else + dir = "."; + const std::string iniPath = dir + "\\apache_iotdb_odbc_driver.ini"; + char buf[MAX_PATH] = {}; + GetPrivateProfileStringA("logging", "LogFile", "", buf, sizeof(buf), iniPath.c_str()); + std::string path = buf; + if (path.empty()) { + GetPrivateProfileStringA("logging", "LogDir", "", buf, sizeof(buf), iniPath.c_str()); + if (buf[0] != '\0') + path = std::string(buf) + "\\apache_iotdb_odbc.log"; + } + if (path.empty()) + path = DEBUG_LOG_OUTPUT_PATH; + logFile.open(path, std::ios::app); +#else + logFile.open(DEBUG_LOG_OUTPUT_PATH, std::ios::app); +#endif +} + +void logMessageInternal(const std::string& message) { +#if !ODBC_ENABLE_LOGGING + (void)message; + return; +#else + std::call_once(logFileOnce, EnsureLogFileOpen); + std::lock_guard lock(logMutex); + if (!logFile) { + return; + } + + const std::time_t now = std::time(nullptr); + +#ifdef WIN32 + // Windows implementation + char buffer[26]; + if (ctime_s(buffer, sizeof(buffer), &now) == 0) { + std::string timeStr = buffer; + if (!timeStr.empty() && timeStr.back() == '\n') { + timeStr.pop_back(); + } + logFile << timeStr << ": " << message << std::endl; + } +#else + // Linux & macOS implementation + char buffer[64]; + std::tm localTime{}; + if (localtime_r(&now, &localTime) && + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &localTime)) { + logFile << buffer << ": " << message << std::endl; + } +#endif + else { + logFile << "Error retrieving time: " << message << std::endl; + } +#endif +} + +/* +Logging Levels: += 0 (ERROR): Critical failures requiring immediate intervention. Always logged. += 1 (WARN): Non-critical issues or unexpected conditions that merit attention. Logged in production. += 2 (INFO): General operational events (startup, milestones, state changes). += 3 (DEBUG): Diagnostic details for troubleshooting. Enabled during development/testing. += 4 (TRACE): Granular execution tracing (function calls, data flows). Highest verbosity for deep debugging. +if cnct == nullptr, the log will output. + */ +void logMessage(ConnectionHandle* cnct, const std::string& message, int level) { +#if !ODBC_ENABLE_LOGGING + (void)cnct; + (void)message; + (void)level; + return; +#else + if (!isLogLevelEnabled(cnct, level)) + return; + logMessageInternal(message); +#endif +} + +void logMessage(const std::string& message) { +#if ODBC_ENABLE_LOGGING + logMessageInternal("(NO LOG LEVEL) " + message); +#else + (void)message; +#endif +} + +void cleanupLogging() { +#if ODBC_ENABLE_LOGGING + std::lock_guard lock(logMutex); + if (logFile.is_open()) { + logFile.close(); + } +#endif +} + +bool isLogLevelEnabled(ConnectionHandle* cnct, int level) { +#if !ODBC_ENABLE_LOGGING + (void)cnct; + (void)level; + return false; +#else + if (cnct == nullptr) + return true; + return cnct->logLevel >= level; +#endif +} + +std::string CDataTypeName(SQLSMALLINT dataType) { + std::string name; + switch (dataType) { + case SQL_C_CHAR: + name = "SQL_C_CHAR"; + break; + case SQL_C_WCHAR: + name = "SQL_C_WCHAR"; + break; + case SQL_C_NUMERIC: + name = "SQL_C_NUMERIC"; + break; + case SQL_C_SSHORT: + name = "SQL_C_SSHORT"; + break; + case SQL_C_USHORT: + name = "SQL_C_USHORT"; + break; + case SQL_C_SLONG: + name = "SQL_C_SLONG"; + break; + case SQL_C_ULONG: + name = "SQL_C_ULONG"; + break; + case SQL_C_FLOAT: + name = "SQL_C_FLOAT"; + break; + case SQL_C_DOUBLE: + name = "SQL_C_DOUBLE"; + break; + case SQL_C_BIT: + name = "SQL_C_BIT"; + break; + case SQL_C_STINYINT: + name = "SQL_C_STINYINT"; + break; + case SQL_C_UTINYINT: + name = "SQL_C_UTINYINT"; + break; + case SQL_C_SBIGINT: + name = "SQL_C_SBIGINT"; + break; + case SQL_C_UBIGINT: + name = "SQL_C_UBIGINT"; + break; + case SQL_C_BINARY: + name = "SQL_C_BINARY"; + break; + case SQL_C_DATE: + name = "SQL_C_DATE"; + break; + case SQL_C_TIME: + name = "SQL_C_TIME"; + break; + case SQL_C_TIMESTAMP: + name = "SQL_C_TIMESTAMP"; + break; + case SQL_C_TYPE_DATE: + name = "SQL_C_TYPE_DATE"; + break; + case SQL_C_TYPE_TIME: + name = "SQL_C_TYPE_TIME"; + break; + case SQL_C_TYPE_TIMESTAMP: + name = "SQL_C_TYPE_TIMESTAMP"; + break; + case SQL_C_GUID: + name = "SQL_C_GUID"; + break; + case SQL_C_INTERVAL_YEAR: + name = "SQL_C_INTERVAL_YEAR"; + break; + case SQL_C_INTERVAL_MONTH: + name = "SQL_C_INTERVAL_MONTH"; + break; + case SQL_C_INTERVAL_DAY: + name = "SQL_C_INTERVAL_DAY"; + break; + case SQL_C_INTERVAL_HOUR: + name = "SQL_C_INTERVAL_HOUR"; + break; + case SQL_C_INTERVAL_MINUTE: + name = "SQL_C_INTERVAL_MINUTE"; + break; + case SQL_C_INTERVAL_SECOND: + name = "SQL_C_INTERVAL_SECOND"; + break; + case SQL_C_INTERVAL_YEAR_TO_MONTH: + name = "SQL_C_INTERVAL_YEAR_TO_MONTH"; + break; + case SQL_C_INTERVAL_DAY_TO_HOUR: + name = "SQL_C_INTERVAL_DAY_TO_HOUR"; + break; + case SQL_C_INTERVAL_DAY_TO_MINUTE: + name = "SQL_C_INTERVAL_DAY_TO_MINUTE"; + break; + case SQL_C_INTERVAL_DAY_TO_SECOND: + name = "SQL_C_INTERVAL_DAY_TO_SECOND"; + break; + case SQL_C_INTERVAL_HOUR_TO_MINUTE: + name = "SQL_C_INTERVAL_HOUR_TO_MINUTE"; + break; + case SQL_C_INTERVAL_HOUR_TO_SECOND: + name = "SQL_C_INTERVAL_HOUR_TO_SECOND"; + break; + case SQL_C_DEFAULT: + name = "SQL_C_DEFAULT"; + break; + default: + name = "UNKNOWN_TYPE_CODE"; + break; + } + return name + "(" + std::to_string(dataType) + ")"; +} + +std::string TSDataTypeName(TSDataType::TSDataType dataType) { + std::string name; + switch (dataType) { + case TSDataType::BOOLEAN: + name = "BOOLEAN"; + break; + case TSDataType::INT32: + name = "INT32"; + break; + case TSDataType::INT64: + name = "INT64"; + break; + case TSDataType::TIMESTAMP: + name = "TIMESTAMP"; + break; + case TSDataType::FLOAT: + name = "FLOAT"; + break; + case TSDataType::DOUBLE: + name = "DOUBLE"; + break; + case TSDataType::TEXT: + name = "TEXT"; + break; + case TSDataType::STRING: + name = "STRING"; + break; + case TSDataType::DATE: + name = "DATE"; + break; + case TSDataType::BLOB: + name = "BLOB"; + break; + default: + name = "UNKNOWN_TYPE"; + break; + } + return "TSDataType::" + name + "(" + std::to_string(static_cast(dataType)) + ")"; +} + +std::string sqlCharToString(SQLCHAR* str) { + return str ? reinterpret_cast(str) : "NULL"; +} + +std::string handleToString(SQLHANDLE handle) { + std::stringstream ss; + ss << std::hex << handle; + return ss.str(); +} + +std::string valueToString(SQLPOINTER value) { + if (value == nullptr) { + return "NULL"; + } + + std::ostringstream stream; + + // Check if Value is a standard constant or an integer + if (reinterpret_cast(value) == SQL_AUTOCOMMIT_ON) { + return "SQL_AUTOCOMMIT_ON"; + } + if (reinterpret_cast(value) == SQL_AUTOCOMMIT_OFF) { + return "SQL_AUTOCOMMIT_OFF"; + } + + // Handle specific types of values based on context + try { + // If it points to an integer + if (reinterpret_cast(value) <= std::numeric_limits::max()) { + stream << "Integer: " << reinterpret_cast(value); + } else { + // If it points to a string (make sure it's null-terminated) + const auto strValue = static_cast(value); + stream << "String: \"" << std::string(strValue) << "\""; + } + } catch (...) { + // Fallback for unknown Value types + stream << "Unknown: " << value; + } + + return stream.str(); +} diff --git a/iotdb-client/odbc/src/Log.h b/iotdb-client/odbc/src/Log.h new file mode 100644 index 0000000000000..81d0c73342c40 --- /dev/null +++ b/iotdb-client/odbc/src/Log.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. + */ +#ifndef ODBC_LOG_H +#define ODBC_LOG_H + +#include "Pch.h" +#include "ConnectionHandle.h" // For ConnectionHandle definition + +// Logging levels +#define LOG_LEVEL_ERROR 0 +#define LOG_LEVEL_WARN 1 +#define LOG_LEVEL_INFO 2 +#define LOG_LEVEL_DEBUG 3 +#define LOG_LEVEL_TRACE 4 + +// Basic log message (always logged if enabled) +void logMessageInternal(const std::string& message); +void logMessage(const std::string& message); + +// Level-based log message with connection context +void logMessage(ConnectionHandle* cnct, const std::string& message, int level); + +// Cleanup logging resources +void cleanupLogging(); + +bool isLogLevelEnabled(ConnectionHandle* cnct, int level); + +std::string CDataTypeName(SQLSMALLINT dataType); +std::string TSDataTypeName(TSDataType::TSDataType dataType); + +std::string sqlCharToString(SQLCHAR* str); +std::string handleToString(SQLHANDLE handle); +std::string valueToString(SQLPOINTER value); + +#endif // ODBC_LOG_H diff --git a/iotdb-client/odbc/src/ODBCField.cpp b/iotdb-client/odbc/src/ODBCField.cpp new file mode 100644 index 0000000000000..7211e7fef18b7 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCField.cpp @@ -0,0 +1,1065 @@ +/** + * 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 "ODBCField.h" +#include "Pch.h" +#include +#include +#include +#include +#include "Log.h" +#include +#include + +ODBCField::ODBCField() { + field_.dataType = TSDataType::UNKNOWN; + // Constructor implementation +} + +ODBCField::ODBCField(bool value) { + setValue(value); +} + +ODBCField::ODBCField(int value) { + setValue(value); +} + +ODBCField::ODBCField(const IoTDBDate& value) { + setValue(value); +} + +ODBCField::ODBCField(int64_t value) { + setValue(value); +} + +ODBCField::ODBCField(float value) { + setValue(value); +} + +ODBCField::ODBCField(double value) { + setValue(value); +} + +ODBCField::ODBCField(const std::string& value) { + setValue(value); +} + +ODBCField::ODBCField(const Field& field) : field_(field) { + // Constructor with field implementation +} + +ODBCField::~ODBCField() { + // Destructor implementation +} + +ODBCField ODBCField::null() { + return ODBCField(); +} + +void ODBCField::setField(const Field& field) { + field_ = field; +} + +TSDataType::TSDataType ODBCField::getDataType() const { + return field_.dataType; +} + +bool ODBCField::isNull() const { + // Check if field is null based on dataType and optional values + switch (field_.dataType) { + case TSDataType::BOOLEAN: + return !field_.boolV.is_initialized(); + case TSDataType::INT32: + return !field_.intV.is_initialized(); + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + return !field_.longV.is_initialized(); + case TSDataType::FLOAT: + return !field_.floatV.is_initialized(); + case TSDataType::DOUBLE: + return !field_.doubleV.is_initialized(); + case TSDataType::TEXT: + case TSDataType::STRING: + return !field_.stringV.is_initialized(); + case TSDataType::DATE: + return !field_.dateV.is_initialized(); + case TSDataType::BLOB: + // BLOB is represented as string in this implementation + return !field_.stringV.is_initialized(); + default: + return true; // Unknown type, treat as null + } +} + +bool ODBCField::toChar(SQLCHAR* target, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) const { + // Convert to SQL_C_CHAR (null-terminated string) + if (isNull()) { + if (strLen_or_IndPtr) + *strLen_or_IndPtr = SQL_NULL_DATA; + return true; + } + + std::string strValue; + switch (field_.dataType) { + case TSDataType::BOOLEAN: + strValue = field_.boolV.value() ? "1" : "0"; + break; + case TSDataType::INT32: + strValue = std::to_string(field_.intV.value()); + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + strValue = std::to_string(field_.longV.value()); + break; + case TSDataType::FLOAT: + strValue = std::to_string(field_.floatV.value()); + break; + case TSDataType::DOUBLE: + strValue = std::to_string(field_.doubleV.value()); + break; + case TSDataType::TEXT: + case TSDataType::STRING: + case TSDataType::BLOB: + strValue = field_.stringV.value(); + break; + case TSDataType::DATE: + strValue = field_.dateV.value().toIsoExtendedString(); + break; + default: + return false; // Unsupported conversion + } + + SQLLEN requiredLength = strValue.length() + 1; // +1 for null terminator + + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = strValue.length(); // Length without null terminator + } + + if (target && bufferLength >= requiredLength) { + std::memcpy(target, strValue.c_str(), requiredLength); + } else if (target && bufferLength > 0) { + // Truncate if buffer is too small + std::memcpy(target, strValue.c_str(), bufferLength - 1); + target[bufferLength - 1] = '\0'; + } + + return true; +} + +bool ODBCField::toWChar(SQLWCHAR* target, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) const { + // Convert to SQL_C_WCHAR (wide character string) + if (isNull()) { + if (strLen_or_IndPtr) + *strLen_or_IndPtr = SQL_NULL_DATA; + return true; + } + + std::string strValue; + switch (field_.dataType) { + case TSDataType::BOOLEAN: + strValue = field_.boolV.value() ? "1" : "0"; + break; + case TSDataType::INT32: + strValue = std::to_string(field_.intV.value()); + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + strValue = std::to_string(field_.longV.value()); + break; + case TSDataType::FLOAT: + strValue = std::to_string(field_.floatV.value()); + break; + case TSDataType::DOUBLE: + strValue = std::to_string(field_.doubleV.value()); + break; + case TSDataType::TEXT: + case TSDataType::STRING: + case TSDataType::BLOB: + strValue = field_.stringV.value(); + break; + case TSDataType::DATE: + strValue = field_.dateV.value().toIsoExtendedString(); + break; + default: + return false; // Unsupported conversion + } + + // Convert to wide string + std::wstring_convert, char16_t> converter; + std::u16string wstrValue = converter.from_bytes(strValue); + SQLLEN requiredLength = (wstrValue.length() + 1) * sizeof(SQLWCHAR); // +1 for null terminator + + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = + wstrValue.length() * sizeof(SQLWCHAR); // Length in bytes without null terminator + } + + if (target && bufferLength >= requiredLength) { + std::memcpy(target, wstrValue.c_str(), requiredLength); + } else if (target && bufferLength >= sizeof(SQLWCHAR)) { + // Truncate if buffer is too small (at least room for null terminator) + SQLLEN maxChars = (bufferLength / sizeof(SQLWCHAR)) - 1; + std::memcpy(target, wstrValue.c_str(), maxChars * sizeof(SQLWCHAR)); + target[maxChars] = L'\0'; + } + + return true; +} + +bool ODBCField::toSTinyInt(SQLSCHAR* target) const { + // Convert to SQL_C_STINYINT (signed char, -128 to 127) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= -128 && field_.intV.value() <= 127) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + if (field_.longV.value() >= -128 && field_.longV.value() <= 127) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toUTinyInt(SQLCHAR* target) const { + // Convert to SQL_C_UTINYINT (unsigned char, 0 to 255) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= 0 && field_.intV.value() <= 255) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + if (field_.longV.value() >= 0 && field_.longV.value() <= 255) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toSShort(SQLSMALLINT* target) const { + // Convert to SQL_C_SSHORT (signed short int, -32,768 to 32,767) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= -32768 && field_.intV.value() <= 32767) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + if (field_.longV.value() >= -32768 && field_.longV.value() <= 32767) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toUShort(SQLUSMALLINT* target) const { + // Convert to SQL_C_USHORT (unsigned short int, 0 to 65,535) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= 0 && field_.intV.value() <= 65535) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + if (field_.longV.value() >= 0 && field_.longV.value() <= 65535) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toSLong(SQLINTEGER* target) const { + // Convert to SQL_C_SLONG (signed long int) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + *target = static_cast(field_.intV.value()); + return true; + case TSDataType::INT64: + // Check for overflow/underflow + if (field_.longV.value() >= INT_MIN && field_.longV.value() <= INT_MAX) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + case TSDataType::TIMESTAMP: + // Timestamp can be converted to long (seconds since epoch) + *target = static_cast(field_.longV.value() / 1000); // Convert ms to seconds + return true; + default: + break; + } + return false; +} + +bool ODBCField::toULong(SQLUINTEGER* target) const { + // Convert to SQL_C_ULONG (unsigned long int) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= 0) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + if (field_.longV.value() >= 0 && field_.longV.value() <= UINT_MAX) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + case TSDataType::TIMESTAMP: + // Timestamp can be converted to unsigned long (seconds since epoch) + if (field_.longV.value() >= 0) { + *target = static_cast(field_.longV.value() / 1000); // Convert ms to seconds + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toSBigInt(SQLBIGINT* target) const { + // Convert to SQL_C_SBIGINT (signed 64-bit integer) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + *target = static_cast(field_.intV.value()); + return true; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + *target = static_cast(field_.longV.value()); + return true; + default: + break; + } + return false; +} + +bool ODBCField::toUBigInt(SQLUBIGINT* target) const { + // Convert to SQL_C_UBIGINT (unsigned 64-bit integer) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + if (field_.intV.value() >= 0) { + *target = static_cast(field_.intV.value()); + return true; + } + break; + case TSDataType::INT64: + if (field_.longV.value() >= 0) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + case TSDataType::TIMESTAMP: + if (field_.longV.value() >= 0) { + *target = static_cast(field_.longV.value()); + return true; + } + break; + default: + break; + } + return false; +} + +bool ODBCField::toFloat(SQLREAL* target) const { + // Convert to SQL_C_FLOAT (float) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1.0f : 0.0f; + return true; + case TSDataType::INT32: + *target = static_cast(field_.intV.value()); + return true; + case TSDataType::INT64: + *target = static_cast(field_.longV.value()); + return true; + case TSDataType::FLOAT: + *target = static_cast(field_.floatV.value()); + return true; + case TSDataType::DOUBLE: + // Check for precision loss + { + double val = field_.doubleV.value(); + if (val >= -std::numeric_limits::max() && val <= std::numeric_limits::max()) { + *target = static_cast(val); + return true; + } + } + break; + case TSDataType::TIMESTAMP: + *target = static_cast(field_.longV.value()); + return true; + default: + break; + } + return false; +} + +bool ODBCField::toDouble(SQLDOUBLE* target) const { + // Convert to SQL_C_DOUBLE (double) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1.0 : 0.0; + return true; + case TSDataType::INT32: + *target = static_cast(field_.intV.value()); + return true; + case TSDataType::INT64: + *target = static_cast(field_.longV.value()); + return true; + case TSDataType::FLOAT: + *target = static_cast(field_.floatV.value()); + return true; + case TSDataType::DOUBLE: + *target = static_cast(field_.doubleV.value()); + return true; + case TSDataType::TIMESTAMP: + *target = static_cast(field_.longV.value()); + return true; + default: + break; + } + return false; +} + +bool ODBCField::toBit(SQLCHAR* target) const { + // Convert to SQL_C_BIT (unsigned char, 0 or 1) + if (!target || isNull()) + return false; + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + *target = field_.boolV.value() ? 1 : 0; + return true; + case TSDataType::INT32: + *target = (field_.intV.value() != 0) ? 1 : 0; + return true; + case TSDataType::INT64: + *target = (field_.longV.value() != 0) ? 1 : 0; + return true; + case TSDataType::FLOAT: + *target = (field_.floatV.value() != 0.0f) ? 1 : 0; + return true; + case TSDataType::DOUBLE: + *target = (field_.doubleV.value() != 0.0) ? 1 : 0; + return true; + case TSDataType::TEXT: + case TSDataType::STRING: { + std::string str = field_.stringV.value(); + // Convert string to boolean (common conventions) + if (str == "1" || str == "true" || str == "TRUE" || str == "yes" || str == "YES") { + *target = 1; + return true; + } else if (str == "0" || str == "false" || str == "FALSE" || str == "no" || str == "NO") { + *target = 0; + return true; + } + } break; + default: + break; + } + return false; +} + +bool ODBCField::toBinary(SQLCHAR* target, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) const { + // Convert to SQL_C_BINARY (binary data as unsigned char array) + if (isNull()) { + if (strLen_or_IndPtr) + *strLen_or_IndPtr = SQL_NULL_DATA; + return true; + } + // For BLOB data, decode the hex string (e.g. "0x506C...") into raw bytes + if (field_.dataType == TSDataType::BLOB && field_.stringV.is_initialized()) { + const std::string& raw = field_.stringV.value(); + // Strip optional "0x"/"0X" prefix + std::string hex = raw; + if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) { + hex = hex.substr(2); + } + + if (hex.size() % 2 != 0) { + return false; + } + + auto hexNibble = [](unsigned char c) -> int { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + return -1; + }; + + std::vector decoded; + decoded.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + int high = hexNibble(static_cast(hex[i])); + int low = hexNibble(static_cast(hex[i + 1])); + if (high < 0 || low < 0) { + return false; + } + decoded.push_back(static_cast((high << 4) | low)); + } + + SQLLEN dataLength = static_cast(decoded.size()); + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = dataLength; // total byte length + } + + if (!target || bufferLength <= 0) { + return true; + } + + SQLLEN copyLength = (bufferLength < dataLength) ? bufferLength : dataLength; + std::memcpy(target, decoded.data(), static_cast(copyLength)); + return true; + } + + // For other types, this conversion is not supported + return false; +} + +bool ODBCField::toTimestamp(SQL_TIMESTAMP_STRUCT* target) const { + // Convert to SQL_C_TYPE_TIMESTAMP + if (!target || isNull()) + return false; + + if (field_.dataType == TSDataType::DATE && field_.dateV.is_initialized()) { + target->year = field_.dateV.value().year(); + target->month = field_.dateV.value().month(); + target->day = field_.dateV.value().day(); + target->hour = 0; + target->minute = 0; + target->second = 0; + target->fraction = 0; + return true; + } + + if (field_.dataType == TSDataType::TIMESTAMP && field_.longV.is_initialized()) { + int64_t ms = field_.longV.value(); + // Floor division preserves the fractional part before the Unix epoch. + int64_t seconds = ms / 1000 - (ms % 1000 < 0); + int64_t days = seconds / 86400 - (seconds % 86400 < 0); + int64_t secondsInDay = seconds - days * 86400; + auto leap = [](int year) { return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); }; + int year = 1970; + // Windows gmtime rejects negative timestamps; use the Gregorian calendar + // directly so pre-1970 data has identical behavior on both platforms. + while (days < 0 && year > 1) + days += leap(--year) ? 366 : 365; + while (year <= 9999 && days >= (leap(year) ? 366 : 365)) { + days -= leap(year) ? 366 : 365; + ++year; + } + if (days < 0 || year > 9999) + return false; + int monthDays[] = {31, leap(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + int month = 0; + while (days >= monthDays[month]) + days -= monthDays[month++]; + target->year = static_cast(year); + target->month = static_cast(month + 1); + target->day = static_cast(days + 1); + target->hour = static_cast(secondsInDay / 3600); + target->minute = static_cast((secondsInDay / 60) % 60); + target->second = static_cast(secondsInDay % 60); + // fraction: 纳秒 (十亿分之一秒) = 毫秒余数 * 1000000 + int64_t ms_part = ms % 1000; + if (ms_part < 0) + ms_part += 1000; + target->fraction = static_cast(ms_part * 1000000); + return true; + } + + return false; +} + +bool ODBCField::toDate(SQL_DATE_STRUCT* target) const { + // Convert to SQL_C_TYPE_DATE + if (!target || isNull()) + return false; + + if (field_.dataType == TSDataType::DATE && field_.dateV.is_initialized()) { + IoTDBDate date = field_.dateV.value(); + target->year = date.year(); + target->month = date.month(); + target->day = date.day(); + return true; + } + + return false; +} + +bool ODBCField::toTime(SQL_TIME_STRUCT* target) const { + // Convert to SQL_C_TYPE_TIME + if (!target || isNull()) + return false; + + // For timestamp, extract time part (simplified implementation) + if (field_.dataType == TSDataType::TIMESTAMP && field_.longV.is_initialized()) { + // This is a simplified implementation - real implementation would need proper timestamp parsing + // For now, just return false for timestamp to time conversion + return false; + } + + return false; +} + +bool ODBCField::toNumeric(SQL_NUMERIC_STRUCT* target, SQLSMALLINT precision, SQLSMALLINT scale, + bool* truncated) const { + if (!target || isNull() || precision < 1 || precision > 38 || scale < -38 || scale > 38) + return false; + int64_t value; + if (field_.dataType == TSDataType::INT32) + value = field_.intV.value(); + else if (field_.dataType == TSDataType::INT64 || field_.dataType == TSDataType::TIMESTAMP) + value = field_.longV.value(); + else + return false; // Approximate-to-fixed conversion is not implemented. + + SQL_NUMERIC_STRUCT numeric{}; + numeric.precision = static_cast(precision); + numeric.scale = static_cast(scale); + numeric.sign = value < 0 ? 0 : 1; + // Unsigned subtraction also represents abs(INT64_MIN), without signed overflow. + uint64_t magnitude = + value < 0 ? uint64_t(0) - static_cast(value) : static_cast(value); + for (size_t i = 0; i < sizeof(magnitude); ++i) { + numeric.val[i] = static_cast(magnitude & 0xff); + magnitude >>= 8; + } + bool lostFraction = false; + for (int digit = 0; digit < std::abs(static_cast(scale)); ++digit) { + unsigned carry = 0; + if (scale > 0) { + for (auto& byte : numeric.val) { + unsigned product = static_cast(byte) * 10 + carry; + byte = static_cast(product & 0xff); + carry = product >> 8; + } + if (carry) + return false; + } else { + for (int i = SQL_MAX_NUMERIC_LEN - 1; i >= 0; --i) { + unsigned dividend = carry * 256 + numeric.val[i]; + numeric.val[i] = static_cast(dividend / 10); + carry = dividend % 10; + } + lostFraction = lostFraction || carry != 0; + } + } + // Count decimal digits without converting the 128-bit magnitude through a + // floating-point type. This keeps precision checks exact on MSVC as well. + SQL_NUMERIC_STRUCT remaining = numeric; + int digits = 0; + bool nonzero; + do { + unsigned carry = 0; + nonzero = false; + for (int i = SQL_MAX_NUMERIC_LEN - 1; i >= 0; --i) { + unsigned dividend = carry * 256 + remaining.val[i]; + remaining.val[i] = static_cast(dividend / 10); + carry = dividend % 10; + nonzero = nonzero || remaining.val[i] != 0; + } + ++digits; + } while (nonzero); + if (digits > precision) + return false; + *target = numeric; + if (truncated) + *truncated = lostFraction; + return true; +} +bool ODBCField::toGUID(SQLGUID* target) const { + // Convert to SQL_C_GUID + // GUID conversion is only supported for string representations of GUIDs + if (!target || isNull()) + return false; + + if (field_.dataType == TSDataType::TEXT || field_.dataType == TSDataType::STRING) { + const std::string& guidStr = field_.stringV.value(); + + // Basic GUID parsing - expects format like "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + // This is a simplified implementation + if (guidStr.length() == 36) { + // Parse the GUID string (this is a very basic implementation) + // Real implementation would need proper GUID parsing + try { + // For simplicity, we'll just return false for now + // A full implementation would parse the string and set the Data1-Data4 fields + return false; + } catch (...) { + return false; + } + } + } + + return false; +} + +// Set value methods - overloaded for different data types +void ODBCField::setValue(bool value) { + field_.dataType = TSDataType::BOOLEAN; + field_.boolV = value; + // Clear other optional values + field_.intV = {}; + field_.dateV = {}; + field_.longV = {}; + field_.floatV = {}; + field_.doubleV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(int value) { + field_.dataType = TSDataType::INT32; + field_.intV = value; + // Clear other optional values + field_.boolV = {}; + field_.dateV = {}; + field_.longV = {}; + field_.floatV = {}; + field_.doubleV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(const IoTDBDate& value) { + field_.dataType = TSDataType::DATE; + field_.dateV = value; + // Clear other optional values + field_.boolV = {}; + field_.intV = {}; + field_.longV = {}; + field_.floatV = {}; + field_.doubleV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(int64_t value) { + field_.dataType = TSDataType::INT64; + field_.longV = value; + // Clear other optional values + field_.boolV = {}; + field_.intV = {}; + field_.dateV = {}; + field_.floatV = {}; + field_.doubleV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(float value) { + field_.dataType = TSDataType::FLOAT; + field_.floatV = value; + // Clear other optional values + field_.boolV = {}; + field_.intV = {}; + field_.dateV = {}; + field_.longV = {}; + field_.doubleV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(double value) { + field_.dataType = TSDataType::DOUBLE; + field_.doubleV = value; + // Clear other optional values + field_.boolV = {}; + field_.intV = {}; + field_.dateV = {}; + field_.longV = {}; + field_.floatV = {}; + field_.stringV = {}; +} + +void ODBCField::setValue(const std::string& value) { + field_.dataType = TSDataType::STRING; + field_.stringV = value; + // Clear other optional values + field_.boolV = {}; + field_.intV = {}; + field_.dateV = {}; + field_.longV = {}; + field_.floatV = {}; + field_.doubleV = {}; +} + +void ODBCField::setDataType(TSDataType::TSDataType dataType) { + // get the only existing value in field_ + TSDataType::TSDataType existingDataType = TSDataType::UNKNOWN; + int existingDataTypeCount = 0; + if (field_.boolV.is_initialized()) { + existingDataType = TSDataType::BOOLEAN; + existingDataTypeCount++; + } else if (field_.intV.is_initialized()) { // INT32 + existingDataType = TSDataType::INT32; + existingDataTypeCount++; + } else if (field_.longV.is_initialized()) { // INT64, TIMESTAMP + existingDataType = TSDataType::INT64; + existingDataTypeCount++; + } else if (field_.floatV.is_initialized()) { // FLOAT + existingDataType = TSDataType::FLOAT; + existingDataTypeCount++; + } else if (field_.doubleV.is_initialized()) { // DOUBLE + existingDataType = TSDataType::DOUBLE; + existingDataTypeCount++; + } else if (field_.stringV.is_initialized()) { // TEXT, STRING, BLOB + existingDataType = TSDataType::STRING; + existingDataTypeCount++; + } else if (field_.dateV.is_initialized()) { // DATE + existingDataType = TSDataType::DATE; + existingDataTypeCount++; + } + if (existingDataTypeCount > 1) { + throw std::runtime_error("ODBCField::setDataType: Multiple data types exist"); + } + if (existingDataTypeCount == 0) { + field_.dataType = dataType; + return; + } + + // Down below is the case that existingDataTypeCount is 1 + if (existingDataType == dataType || field_.dataType == dataType) { + return; + } + + switch (dataType) { + case TSDataType::TIMESTAMP: + if (existingDataType == TSDataType::INT64) { + field_.dataType = TSDataType::TIMESTAMP; + } + break; + case TSDataType::TEXT: + if (existingDataType == TSDataType::STRING) { + field_.dataType = TSDataType::TEXT; + } + break; + case TSDataType::BLOB: + if (existingDataType == TSDataType::STRING) { + field_.dataType = TSDataType::BLOB; + } + break; + default: + throw std::runtime_error("ODBCField::setDataType: Unsupported data type. Existing data type: " + + std::to_string(existingDataType) + + ", New data type: " + std::to_string(dataType)); + } +} + +std::string ODBCField::toString() const { + // Convert field value to string representation for debugging + if (isNull()) { + return "NULL"; + } + + switch (field_.dataType) { + case TSDataType::BOOLEAN: + return field_.boolV.value() ? "true" : "false"; + case TSDataType::INT32: + return std::to_string(field_.intV.value()); + case TSDataType::INT64: + return std::to_string(field_.longV.value()); + case TSDataType::TIMESTAMP: + return "timestamp(" + std::to_string(field_.longV.value()) + ")"; + case TSDataType::FLOAT: + return std::to_string(field_.floatV.value()); + case TSDataType::DOUBLE: + return std::to_string(field_.doubleV.value()); + case TSDataType::TEXT: + case TSDataType::STRING: + return field_.stringV.value(); + case TSDataType::DATE: + return "date(" + field_.dateV.value().toIsoExtendedString() + ")"; + case TSDataType::BLOB: + return "blob(" + std::to_string((field_.stringV.value()).length()) + " bytes)"; + default: + return "unknown(" + std::to_string(static_cast(field_.dataType)) + ")"; + } +} + +bool ODBCField::canConvert(TSDataType::TSDataType tsDataType, SQLSMALLINT cDataType) { + static const std::unordered_map> + typeConversionMap = []() { + std::unordered_map> map; + // 初始化映射表 + std::unordered_set boolSet = { + SQL_C_CHAR, SQL_C_WCHAR, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_SSHORT, + SQL_C_USHORT, SQL_C_SLONG, SQL_C_ULONG, SQL_C_SBIGINT, SQL_C_UBIGINT, + SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BIT}; + map[TSDataType::BOOLEAN] = boolSet; + + std::unordered_set int32Set = { + SQL_C_CHAR, SQL_C_WCHAR, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_SSHORT, + SQL_C_USHORT, SQL_C_SLONG, SQL_C_ULONG, SQL_C_SBIGINT, SQL_C_UBIGINT, + SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BIT, SQL_C_NUMERIC}; + map[TSDataType::INT32] = int32Set; + + std::unordered_set int64Set = { + SQL_C_CHAR, SQL_C_WCHAR, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_SSHORT, + SQL_C_USHORT, SQL_C_SLONG, SQL_C_ULONG, SQL_C_SBIGINT, SQL_C_UBIGINT, + SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BIT, SQL_C_NUMERIC}; + map[TSDataType::INT64] = int64Set; + + std::unordered_set timestampSet = int64Set; + map[TSDataType::TIMESTAMP] = timestampSet; + + std::unordered_set floatSet = {SQL_C_CHAR, SQL_C_WCHAR, SQL_C_FLOAT, + SQL_C_DOUBLE, SQL_C_BIT}; + map[TSDataType::FLOAT] = floatSet; + + std::unordered_set doubleSet = floatSet; + map[TSDataType::DOUBLE] = doubleSet; + + std::unordered_set textSet = {SQL_C_CHAR, SQL_C_WCHAR, SQL_C_BIT}; + map[TSDataType::TEXT] = textSet; + map[TSDataType::STRING] = textSet; + + std::unordered_set blobSet = {SQL_C_BINARY}; + map[TSDataType::BLOB] = blobSet; + + std::unordered_set dateSet = {SQL_C_CHAR, SQL_C_WCHAR, SQL_C_TYPE_DATE, + SQL_C_TYPE_TIMESTAMP}; + map[TSDataType::DATE] = dateSet; + + return map; + }(); + + SQLSMALLINT effectiveCType = cDataType; + // The deprecated unsuffixed numeric C types are still used by MSDASQL. + if (effectiveCType == SQL_C_LONG) + effectiveCType = SQL_C_SLONG; + else if (effectiveCType == SQL_C_SHORT) + effectiveCType = SQL_C_SSHORT; + else if (effectiveCType == SQL_C_TINYINT) + effectiveCType = SQL_C_STINYINT; + if (effectiveCType == SQL_C_DEFAULT) { + effectiveCType = resolveDefaultCType(tsDataType); + } + + auto it = typeConversionMap.find(tsDataType); + if (it == typeConversionMap.end()) { + return false; + } + return it->second.find(effectiveCType) != it->second.end(); +} + +SQLSMALLINT ODBCField::resolveDefaultCType(TSDataType::TSDataType tsDataType) { + switch (tsDataType) { + case TSDataType::BOOLEAN: + return SQL_C_BIT; + case TSDataType::INT32: + return SQL_C_SLONG; + case TSDataType::INT64: + case TSDataType::TIMESTAMP: + return SQL_C_SBIGINT; + case TSDataType::FLOAT: + return SQL_C_FLOAT; + case TSDataType::DOUBLE: + return SQL_C_DOUBLE; + case TSDataType::TEXT: + case TSDataType::STRING: + return SQL_C_WCHAR; + case TSDataType::BLOB: + return SQL_C_BINARY; + case TSDataType::DATE: + return SQL_C_TYPE_DATE; + default: + return SQL_C_CHAR; + } +} diff --git a/iotdb-client/odbc/src/ODBCField.h b/iotdb-client/odbc/src/ODBCField.h new file mode 100644 index 0000000000000..5b180aa0098a0 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCField.h @@ -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. + */ +#ifndef ODBCFIELD_H +#define ODBCFIELD_H + +#include "Pch.h" +#include +#include +#include + +class ODBCField { +public: + // Construct a null field + ODBCField(); + // Constructors for different data types + ODBCField(bool value); + ODBCField(int value); + ODBCField(const IoTDBDate& value); + ODBCField(int64_t value); + ODBCField(float value); + ODBCField(double value); + ODBCField(const std::string& value); + ODBCField(const char* value) : ODBCField(std::string(value)) {} + // Construct with a Field + explicit ODBCField(const Field& field); + ~ODBCField(); + + // Get a null field, same as ODBCField() constructor + static ODBCField null(); + + // Set the underlying Field + void setField(const Field& field); + void setDataType(TSDataType::TSDataType dataType); + + // Get the data type + TSDataType::TSDataType getDataType() const; + + // Check if the field is null + bool isNull() const; + + // Convert to various ODBC C data types + // These methods return true if conversion is successful, false otherwise + + // Character string types + bool toChar(SQLCHAR* target, SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) const; // SQL_C_CHAR + bool toWChar(SQLWCHAR* target, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr) const; // SQL_C_WCHAR + + // Exact numeric types + bool toSTinyInt(SQLSCHAR* target) const; // SQL_C_STINYINT + bool toUTinyInt(SQLCHAR* target) const; // SQL_C_UTINYINT + bool toSShort(SQLSMALLINT* target) const; // SQL_C_SSHORT + bool toUShort(SQLUSMALLINT* target) const; // SQL_C_USHORT + bool toSLong(SQLINTEGER* target) const; // SQL_C_SLONG + bool toULong(SQLUINTEGER* target) const; // SQL_C_ULONG + bool toSBigInt(SQLBIGINT* target) const; // SQL_C_SBIGINT + bool toUBigInt(SQLUBIGINT* target) const; // SQL_C_UBIGINT + + // Approximate numeric types + bool toFloat(SQLREAL* target) const; // SQL_C_FLOAT + bool toDouble(SQLDOUBLE* target) const; // SQL_C_DOUBLE + + // Boolean type + bool toBit(SQLCHAR* target) const; // SQL_C_BIT + + // Character string representing binary data + bool toBinary(SQLCHAR* target, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr) const; // SQL_C_BINARY + + // Datetime types + bool toDate(SQL_DATE_STRUCT* target) const; // SQL_C_TYPE_DATE + bool toTime(SQL_TIME_STRUCT* target) const; // SQL_C_TYPE_TIME + bool toTimestamp(SQL_TIMESTAMP_STRUCT* target) const; // SQL_C_TYPE_TIMESTAMP + + // Numeric type + bool toNumeric(SQL_NUMERIC_STRUCT* target, SQLSMALLINT precision = 38, SQLSMALLINT scale = 0, + bool* truncated = nullptr) const; // SQL_C_NUMERIC + + // GUID type + bool toGUID(SQLGUID* target) const; // SQL_C_GUID + + // Set value methods - overloaded for different data types + void setValue(bool value); + void setValue(int value); + void setValue(const IoTDBDate& value); + void setValue(int64_t value); + void setValue(float value); + void setValue(double value); + void setValue(const std::string& value); + + // Get string representation for debugging + std::string toString() const; + + static bool canConvert(TSDataType::TSDataType tsDataType, SQLSMALLINT cDataType); + + /// Resolve SQL_C_DEFAULT to the default C type for the given IoTDB/TSDataType. + static SQLSMALLINT resolveDefaultCType(TSDataType::TSDataType tsDataType); + +private: + Field field_; +}; + +#endif // ODBCFIELD_H diff --git a/iotdb-client/odbc/src/ODBCHandle.cpp b/iotdb-client/odbc/src/ODBCHandle.cpp new file mode 100644 index 0000000000000..3144bdf3b0452 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCHandle.cpp @@ -0,0 +1,25 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#include "ODBCHandle.h" + +// ODBCHandle implementation - currently no additional implementation needed diff --git a/iotdb-client/odbc/src/ODBCHandle.h b/iotdb-client/odbc/src/ODBCHandle.h new file mode 100644 index 0000000000000..a3e53e686ebd0 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCHandle.h @@ -0,0 +1,81 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#ifndef ODBCHANDLE_H +#define ODBCHANDLE_H + +#include "Pch.h" +#include "DiagnosticManager.h" +#include +#include + +class ODBCHandle : public DiagnosticManager { + // Common fields for all handles +protected: + int handleType; // e.g., SQL_HANDLE_ENV, SQL_HANDLE_DBC, SQL_HANDLE_STMT + bool isFreed; + +public: + explicit ODBCHandle(int type) : handleType(type), isFreed(false) { + std::lock_guard lock(registryMutex()); + registry()[this] = type; + } + + ~ODBCHandle() { + std::lock_guard lock(registryMutex()); + registry().erase(this); + } + + ODBCHandle(const ODBCHandle&) = delete; + ODBCHandle& operator=(const ODBCHandle&) = delete; + + // SQLDisconnect drops child handles. A later cleanup call must reject their + // old addresses without dereferencing already released storage. + static bool isLiveHandle(SQLHANDLE handle, int type) { + std::lock_guard lock(registryMutex()); + auto entry = registry().find(handle); + return entry != registry().end() && entry->second == type; + } + + int getHandleType() const { + return handleType; + } + + void markAsFreed() { + isFreed = true; + } + bool isHandleFreed() const { + return isFreed; + } + +private: + static std::mutex& registryMutex() { + static std::mutex mutex; + return mutex; + } + static std::unordered_map& registry() { + static std::unordered_map handles; + return handles; + } +}; + +#endif //ODBCHANDLE_H diff --git a/iotdb-client/odbc/src/ODBCRestResultSet.cpp b/iotdb-client/odbc/src/ODBCRestResultSet.cpp new file mode 100644 index 0000000000000..51a631062cbb2 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCRestResultSet.cpp @@ -0,0 +1,142 @@ +/** + * 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 "ODBCRestResultSet.h" + +// Project includes +#include "Log.h" + +// Standard includes +#include +#include +#include +#include "StatementHandle.h" + +ODBCRestResultSet::ODBCRestResultSet(StatementHandle* stmtHandle) : ODBCResultSet(stmtHandle) { + // Initialize member variables +} + +ODBCRestResultSet::~ODBCRestResultSet() { + // Destructor - shared_ptr will handle cleanup automatically +} + +// Implement REST data loading +void ODBCRestResultSet::loadData(const nlohmann::json& jsonData) { + logMessage(stmt->getConnection(), "ODBCRestResultSet::loadData: Starting", LOG_LEVEL_TRACE); + ConnectionHandle* cnct = stmt->getConnection(); + + // Clear existing data + columnNames.clear(); + columnTypes.clear(); + data.clear(); + numRows = 0; + numColumns = 0; + + if (stmt->getConnection()->isTableModel) { + logMessage(cnct, "ODBCRestResultSet::loadData: Parsing JSON response for table model", + LOG_LEVEL_TRACE); + parseJsonResponseTable(jsonData); + } else { + logMessage(cnct, "ODBCRestResultSet::loadData: Parsing JSON response for tree model", + LOG_LEVEL_TRACE); + parseJsonResponseTree(jsonData); + } + + logMessage(cnct, "ODBCRestResultSet::loadData: Exiting", LOG_LEVEL_DEBUG); +} + +namespace { +ODBCField jsonField(const nlohmann::json& value, const std::string& type) { + if (value.is_null()) + return ODBCField::null(); + if (type == "DATE") { + if (value.is_number_integer()) + return ODBCField(parseIntToDate(value.get())); + const auto text = value.get(); + if (text.size() != 10 || text[4] != '-' || text[7] != '-') + throw std::runtime_error("Invalid REST DATE value"); + return ODBCField(IoTDBDate(std::stoi(text.substr(0, 4)), std::stoi(text.substr(5, 2)), + std::stoi(text.substr(8, 2)))); + } + if (type == "INT32") + return ODBCField(value.get()); + if (type == "FLOAT") + return ODBCField(value.get()); + ODBCField field; + if (value.is_string()) + field = ODBCField(value.get()); + else if (value.is_boolean()) + field = ODBCField(value.get()); + else if (value.is_number_integer()) + field = ODBCField(value.get()); + else if (value.is_number_float()) + field = ODBCField(value.get()); + else + throw std::runtime_error("Unsupported REST result value"); + if (type == "BLOB") + field.setDataType(TSDataType::BLOB); + else if (type == "TIMESTAMP") + field.setDataType(TSDataType::TIMESTAMP); + else if (type == "TEXT") + field.setDataType(TSDataType::TEXT); + return field; +} +} // namespace + +void ODBCRestResultSet::parseJsonResponseTree(const nlohmann::json& response) { + isMetaData = !response.contains("expressions") || response["expressions"].is_null() || + response["expressions"].empty(); + const char* names = isMetaData ? "column_names" : "expressions"; + if (response.contains(names) && response[names].is_array()) + columnNames = response[names].get>(); + if (response.contains("data_types") && response["data_types"].is_array()) + columnTypes = response["data_types"].get>(); + if (!isMetaData) { + columnNames.insert(columnNames.begin(), "Time"); + columnTypes.insert(columnTypes.begin(), "TIMESTAMP"); + } + numColumns = static_cast(columnNames.size()); + const auto& values = response.at("values"); + numRows = isMetaData ? (values.empty() ? 0 : static_cast(values.at(0).size())) + : static_cast(response.at("timestamps").size()); + for (int row = 0; row < numRows; ++row) { + std::vector fields; + for (int col = 0; col < numColumns; ++col) { + const auto& value = !isMetaData && col == 0 ? response.at("timestamps").at(row) + : values.at(col - (isMetaData ? 0 : 1)).at(row); + fields.push_back(jsonField(value, col < columnTypes.size() ? columnTypes[col] : "")); + } + data.push_back(std::move(fields)); + } +} + +void ODBCRestResultSet::parseJsonResponseTable(const nlohmann::json& response) { + isMetaData = false; + if (response.contains("column_names") && response["column_names"].is_array()) + columnNames = response["column_names"].get>(); + if (response.contains("data_types") && response["data_types"].is_array()) + columnTypes = response["data_types"].get>(); + numColumns = static_cast(columnNames.size()); + for (const auto& row : response.at("values")) { + std::vector fields; + for (size_t col = 0; col < row.size(); ++col) + fields.push_back(jsonField(row[col], col < columnTypes.size() ? columnTypes[col] : "")); + data.push_back(std::move(fields)); + } + numRows = static_cast(data.size()); +} diff --git a/iotdb-client/odbc/src/ODBCRestResultSet.h b/iotdb-client/odbc/src/ODBCRestResultSet.h new file mode 100644 index 0000000000000..2dd4b0e03bef6 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCRestResultSet.h @@ -0,0 +1,41 @@ +/** + * 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. + */ +#ifndef ODBCRESTRESULTSET_H +#define ODBCRESTRESULTSET_H + +#include "ODBCResultSet.h" + +class ODBCRestResultSet : public ODBCResultSet { +public: + ODBCRestResultSet(StatementHandle* stmtHandle); + ~ODBCRestResultSet(); + + // Implement REST data loading + virtual void loadData(const nlohmann::json& jsonData) override; + virtual void loadData(std::unique_ptr sessionDataSet) override { + throw std::runtime_error("ODBCRestResultSet: loadData with SessionDataSet is not supported"); + } + +private: + // Helper methods for parsing different model types + void parseJsonResponseTree(const nlohmann::json& jsonResponse); + void parseJsonResponseTable(const nlohmann::json& jsonResponse); +}; + +#endif // ODBCRESTRESULTSET_H diff --git a/iotdb-client/odbc/src/ODBCResultSet.cpp b/iotdb-client/odbc/src/ODBCResultSet.cpp new file mode 100644 index 0000000000000..82aed9ec864c4 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCResultSet.cpp @@ -0,0 +1,379 @@ +/** + * 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 "ODBCResultSet.h" + +// Project includes +#include "ODBCField.h" +#include "Log.h" +#include "TableSession.h" +#include "TableSessionBuilder.h" +#include "driver.h" +#include "SessionDataSet.h" + +// IoTDB Session API includes for streaming support +#include + +const std::unordered_map + ODBCResultSet::columnTypeToTSDataTypeMap = { + {"BOOLEAN", TSDataType::BOOLEAN}, {"INT16", TSDataType::INT32}, + {"INT32", TSDataType::INT32}, {"INT64", TSDataType::INT64}, + {"FLOAT", TSDataType::FLOAT}, {"DOUBLE", TSDataType::DOUBLE}, + {"STRING", TSDataType::STRING}, {"TEXT", TSDataType::TEXT}, + {"DATE", TSDataType::DATE}, {"BLOB", TSDataType::BLOB}, + {"TIMESTAMP", TSDataType::TIMESTAMP}, +}; + +ODBCResultSet::ODBCResultSet(StatementHandle* stmtHandle) + : stmt(stmtHandle), numRows(0), numColumns(0), isMetaData(false) { + // Initialize member variables - vectors are automatically initialized as empty +} + +bool ODBCResultSet::isRestful() const { + return stmt->getConnection()->useRestful; +} + +ODBCResultSet::~ODBCResultSet() { + // Destructor - shared_ptr will handle cleanup automatically +} + +// JSON parsing functionality moved to subclasses via loadData methods +// parseJsonResponseTree implementation moved to ODBCRestResultSet + +// JSON parsing functionality moved to subclasses via loadData methods +// parseJsonResponseTable implementation moved to ODBCRestResultSet + +// Get column name by column index (0-based) +// @param columnIndex Column index (0-based) +// @return Column name as string, empty string if invalid index +std::string ODBCResultSet::getColumnName(size_t columnIndex) const { + logMessage(stmt->getConnection(), "ODBCResultSet::getColumnName: Starting", LOG_LEVEL_TRACE); + if (columnIndex < columnNames.size()) { + return columnNames[columnIndex]; + } + logMessage(stmt->getConnection(), "ODBCResultSet::getColumnName: Exiting", LOG_LEVEL_TRACE); + return ""; +} + +// Get column type by column index (0-based) +// @param columnIndex Column index (0-based) +// @return Column type as string, empty string if invalid index +SQLSMALLINT ODBCResultSet::getDefaultCTypeForColumn(SQLUSMALLINT columnNumber) const { + if (columnNumber < 1 || columnNumber > getNumColumns()) { + return SQL_C_CHAR; + } + std::string columnType = getColumnType(columnNumber - 1); + if (columnType == "INT16") + return SQL_C_SSHORT; + auto it = columnTypeToTSDataTypeMap.find(columnType); + if (it == columnTypeToTSDataTypeMap.end()) { + return SQL_C_CHAR; + } + SQLSMALLINT defaultType = ODBCField::resolveDefaultCType(it->second); + logMessage(stmt->getConnection(), + "ODBCResultSet::getDefaultCTypeForColumn: Column " + std::to_string(columnNumber) + + ", TSDataType: " + columnType + ", default type: " + CDataTypeName(defaultType), + LOG_LEVEL_TRACE); + return defaultType; +} + +std::string ODBCResultSet::getColumnType(size_t columnIndex) const { + logMessage(stmt->getConnection(), "ODBCResultSet::getColumnType: Starting", LOG_LEVEL_TRACE); + if (columnIndex < columnTypes.size()) { + return columnTypes[columnIndex]; + } + logMessage(stmt->getConnection(), "ODBCResultSet::getColumnType: Exiting", LOG_LEVEL_TRACE); + return ""; +} + +// Get data value by row and column index (0-based) +// @param rowIndex Row index (0-based) +// @param columnIndex Column index (0-based) +// @return Data value as string, empty string if invalid indices +const ODBCField& ODBCResultSet::getValue(size_t rowIndex, size_t columnIndex) const { + logMessage(stmt->getConnection(), "ODBCResultSet::getValue: Starting", LOG_LEVEL_TRACE); + logMessage(stmt->getConnection(), + "ODBCResultSet::getValue: rowIndex = " + std::to_string(rowIndex) + + ", columnIndex = " + std::to_string(columnIndex), + LOG_LEVEL_TRACE); + static const ODBCField kNullField; + if (rowIndex < 0 || rowIndex >= data.size()) { + logMessage(stmt->getConnection(), + "ODBCResultSet::getValue: Invalid row index, rowIndex = " + + std::to_string(rowIndex) + ", returning empty field", + LOG_LEVEL_ERROR); + return kNullField; + } + if (columnIndex < 0 || columnIndex >= data[rowIndex].size()) { + logMessage( + stmt->getConnection(), + "ODBCResultSet::getValue: Invalid column index, rowIndex = " + std::to_string(rowIndex) + + ", columnIndex = " + std::to_string(columnIndex) + ", returning empty field", + LOG_LEVEL_ERROR); + return kNullField; + } + + return data[rowIndex][columnIndex]; +} + +// Find column index by column name +// @param columnName Column name to find +// @return Column index (0-based), or -1 if not found +int ODBCResultSet::findColumnIndex(const std::string& columnName) const { + logMessage(stmt->getConnection(), "ODBCResultSet::findColumnIndex: Starting", LOG_LEVEL_TRACE); + for (size_t i = 0; i < columnNames.size(); ++i) { + if (columnNames[i] == columnName) { + return static_cast(i); + } + } + logMessage(stmt->getConnection(), "ODBCResultSet::findColumnIndex: Exiting", LOG_LEVEL_TRACE); + return -1; +} + +// Set data value by row and column index +// @param rowIndex Row index (0-based) +// @param columnIndex Column index (0-based) +// @param value New value to set +// @return true if successful, false if invalid indices +bool ODBCResultSet::setValue(size_t rowIndex, size_t columnIndex, const ODBCField& value) { + logMessage(stmt->getConnection(), "ODBCResultSet::setValue: Starting", LOG_LEVEL_TRACE); + if (rowIndex < data.size() && columnIndex < data[rowIndex].size()) { + data[rowIndex][columnIndex] = value; + return true; + } + logMessage(stmt->getConnection(), "ODBCResultSet::setValue: Exiting", LOG_LEVEL_TRACE); + return false; +} + +// Set column name by column index +// @param columnIndex Column index (0-based) +// @param columnName New column name +// @return true if successful, false if invalid index +bool ODBCResultSet::setColumnName(size_t columnIndex, const std::string& columnName) { + logMessage(stmt->getConnection(), "ODBCResultSet::setColumnName: Starting", LOG_LEVEL_TRACE); + if (columnIndex < columnNames.size()) { + columnNames[columnIndex] = columnName; + return true; + } + logMessage(stmt->getConnection(), "ODBCResultSet::setColumnName: Exiting", LOG_LEVEL_TRACE); + return false; +} + +// Set column type by column index +// @param columnIndex Column index (0-based) +// @param columnType New column type +// @return true if successful, false if invalid index +bool ODBCResultSet::setColumnType(size_t columnIndex, const std::string& columnType) { + logMessage(stmt->getConnection(), "ODBCResultSet::setColumnType: Starting", LOG_LEVEL_TRACE); + if (columnIndex < columnTypes.size()) { + columnTypes[columnIndex] = columnType; + return true; + } + logMessage(stmt->getConnection(), "ODBCResultSet::setColumnType: Exiting", LOG_LEVEL_TRACE); + return false; +} + +// Get raw JSON value for SQLGetData type conversion +// @param rowIndex Row index (0-based) +// @param columnNumber Column number (1-based, as used in SQLGetData) +// @param value Output parameter for the JSON value +// @return true if successful, false otherwise + +bool ODBCResultSet::getRawJsonValue(size_t rowIndex, SQLUSMALLINT columnNumber, + nlohmann::json& value) { + // Default implementation for non-REST result sets + logMessage(stmt->getConnection(), + "ODBCResultSet::getRawJsonValue: Not supported for this result set type", + LOG_LEVEL_ERROR); + return false; +} + +int ODBCResultSet::getNumRows() const { + return numRows; +} + +int ODBCResultSet::getNumColumns() const { + return numColumns; +} + +bool ODBCResultSet::getIsMetaData() const { + return isMetaData; +} + +bool ODBCResultSet::isEmpty() const { + return data.empty(); +} + +// Clear all data and reset to initial state +void ODBCResultSet::clear() { + logMessage(stmt->getConnection(), "ODBCResultSet::clear: Starting", LOG_LEVEL_TRACE); + + // Reset primitive members + numRows = 0; + numColumns = 0; + isMetaData = false; + + // Clear vector contents + columnNames.clear(); + columnTypes.clear(); + data.clear(); + + logMessage(stmt->getConnection(), "ODBCResultSet::clear: Exiting", LOG_LEVEL_TRACE); +} + +void ODBCResultSet::outputTable(int maxRows) const { + (void)maxRows; + // Result values may contain credentials or application data. Log only shape. + logMessage(stmt->getConnection(), + "ODBCResultSet: " + std::to_string(numRows) + " rows, " + std::to_string(numColumns) + + " columns", + LOG_LEVEL_DEBUG); +} + +// Check if data type conversion is valid for a column +bool ODBCResultSet::isValidConversion(SQLUSMALLINT columnNumber, SQLSMALLINT targetType) { + std::string columnType = getColumnType(columnNumber - 1); + if (columnTypeToTSDataTypeMap.find(columnType) == columnTypeToTSDataTypeMap.end()) { + logMessage(stmt->getConnection(), + "ODBCResultSet::isValidConversion: Invalid column type: " + columnType, + LOG_LEVEL_ERROR); + return false; + } + TSDataType::TSDataType tsDataType = columnTypeToTSDataTypeMap.at(columnType); + bool canConvert = ODBCField::canConvert(tsDataType, targetType); + if (!canConvert) { + logMessage(stmt->getConnection(), + "ODBCResultSet::isValidConversion: Invalid data type conversion for column " + + std::to_string(columnNumber) + ", source type : " + columnType + + ", target type: " + CDataTypeName(targetType), + LOG_LEVEL_ERROR); + return false; + } + return true; +} + +// Bind a column to application variables for data retrieval +SQLRETURN ODBCResultSet::bindColumn(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValuePtr, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "ODBCResultSet::bindColumn: Entering", LOG_LEVEL_TRACE); + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "Parameters: " + << "columnNumber = " << columnNumber << ", targetType = " << targetType + << ", targetValue = " << targetValuePtr << ", bufferLength = " << bufferLength + << ", strLen_or_Ind = " << strLen_or_IndPtr; + + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (columnNumber == 0) { + // Bookmark column not supported + logMessage(cnct, "ODBCResultSet::bindColumn: Bookmark column (0) not supported", + LOG_LEVEL_WARN); + stmt->addDiagnostic("HYC00", "Bookmarks are not supported"); + return SQL_ERROR; + } + + if (bufferLength < 0) { + logMessage(cnct, + "ODBCResultSet::bindColumn: Invalid buffer length: " + std::to_string(bufferLength), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + if (columnNumber > getNumColumns()) { + logMessage(cnct, + "ODBCResultSet::bindColumn: Invalid column index: " + std::to_string(columnNumber) + + ", max columns: " + std::to_string(getNumColumns()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + + if (!targetValuePtr && !strLen_or_IndPtr) { + if (bindColInfo.size() > columnNumber) + bindColInfo[columnNumber] = BindColInfo(); + return SQL_SUCCESS; + } + + SQLSMALLINT actualType = + (targetType == SQL_C_DEFAULT) ? getDefaultCTypeForColumn(columnNumber) : targetType; + logMessage(cnct, "ODBCResultSet::bindColumn: Actual type: " + CDataTypeName(actualType), + LOG_LEVEL_TRACE); + + if (!isValidConversion(columnNumber, actualType)) { + logMessage(cnct, + "ODBCResultSet::bindColumn: Invalid data type conversion for column " + + std::to_string(columnNumber) + ", target type: " + std::to_string(targetType), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07006", "Restricted data type attribute violation"); + return SQL_ERROR; + } + + // Ensure bindColInfo vector is large enough + if (bindColInfo.size() <= columnNumber) { + bindColInfo.resize(columnNumber + 1); + } + + BindColInfo* binding = &bindColInfo[columnNumber]; + binding->targetType = actualType; + binding->targetValuePtr = targetValuePtr; + binding->bufferLength = bufferLength; + binding->strLen_or_IndPtr = strLen_or_IndPtr; + binding->isBound = true; + + if (targetValuePtr && actualType == SQL_C_CHAR && bufferLength > 0) { + // Initialize to an empty string + static_cast(targetValuePtr)[0] = '\0'; + if (strLen_or_IndPtr) { + *strLen_or_IndPtr = SQL_NTS; + } + } + logMessage(cnct, "ODBCResultSet::bindColumn: Exiting", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +// Unbind columns +void ODBCResultSet::unbindColumns() { + logMessage(stmt->getConnection(), "ODBCResultSet::unbindColumns: Entering", LOG_LEVEL_TRACE); + for (auto& binding : bindColInfo) { + binding.isBound = false; + binding.targetValuePtr = nullptr; + binding.bufferLength = 0; + binding.strLen_or_IndPtr = nullptr; + binding.isBound = false; + } + logMessage(stmt->getConnection(), "ODBCResultSet::unbindColumns: Exiting", LOG_LEVEL_TRACE); +} + +// Get binding information for a column +BindColInfo* ODBCResultSet::getBindColInfo(SQLUSMALLINT columnNumber) { + if (columnNumber >= bindColInfo.size() || columnNumber == 0) { + return nullptr; + } + return &bindColInfo[columnNumber]; +} + +// Check if a column is bound +bool ODBCResultSet::isColumnBound(SQLUSMALLINT columnNumber) { + BindColInfo* binding = getBindColInfo(columnNumber); + return binding && binding->isBound; +} diff --git a/iotdb-client/odbc/src/ODBCResultSet.h b/iotdb-client/odbc/src/ODBCResultSet.h new file mode 100644 index 0000000000000..abd115f4d8bb9 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCResultSet.h @@ -0,0 +1,169 @@ +/** + * 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. + */ +#ifndef ODBCRESULTSET_H +#define ODBCRESULTSET_H + +#include "Pch.h" +#include "ODBCField.h" + +class StatementHandle; // Forward declaration +class SessionDataSet; // Forward declaration + +struct BindColInfo { + SQLSMALLINT targetType; // C data type (SQL_C_CHAR, etc.) + SQLPOINTER targetValuePtr; // Data buffer pointer + SQLLEN bufferLength; // Buffer length + SQLLEN* strLen_or_IndPtr; // Length/indicator pointer + bool isBound; // Whether bound + BindColInfo() + : targetType(SQL_C_DEFAULT), targetValuePtr(nullptr), bufferLength(0), + strLen_or_IndPtr(nullptr), isBound(false) {} +}; + +class ODBCResultSet { +public: + ODBCResultSet(StatementHandle* stmtHandle); + virtual ~ODBCResultSet(); + + // Unified data loading interface - to be implemented by subclasses + virtual void loadData(const nlohmann::json& jsonData) = 0; + virtual void loadData(std::unique_ptr sessionDataSet) = 0; + // Query methods + // Get column name by column index (0-based) + // @param columnIndex Column index (0-based) + // @return Column name as string, empty string if invalid index + std::string getColumnName(size_t columnIndex) const; + + // Get column type by column index (0-based) + // @param columnIndex Column index (0-based) + // @return Column type as string, empty string if invalid index + std::string getColumnType(size_t columnIndex) const; + + // Get default C type for SQL_C_DEFAULT for a column (1-based). + SQLSMALLINT getDefaultCTypeForColumn(SQLUSMALLINT columnNumber) const; + + // Get data value by row and column index (0-based) + // @param rowIndex Row index (0-based) + // @param columnIndex Column index (0-based) + // @return Data value as string, empty string if invalid indices + const ODBCField& getValue(size_t rowIndex, size_t columnIndex) const; + + // Find column index by column name + // @param columnName Column name to find + // @return Column index (0-based), or -1 if not found + int findColumnIndex(const std::string& columnName) const; + + // Set data value by row and column index + // @param rowIndex Row index (0-based) + // @param columnIndex Column index (0-based) + // @param value New value to set + // @return true if successful, false if invalid indices + bool setValue(size_t rowIndex, size_t columnIndex, const ODBCField& value); + + // Set column name by column index + // @param columnIndex Column index (0-based) + // @param columnName New column name + // @return true if successful, false if invalid index + bool setColumnName(size_t columnIndex, const std::string& columnName); + + // Set column type by column index + // @param columnIndex Column index (0-based) + // @param columnType New column type + // @return true if successful, false if invalid index + bool setColumnType(size_t columnIndex, const std::string& columnType); + + // Remove rows that match the predicate function + // @param predicate Function that returns true for rows to be removed + // @return Number of rows removed + template size_t removeRowsIf(Predicate predicate) { + size_t originalSize = data.size(); + data.erase(std::remove_if(data.begin(), data.end(), predicate), data.end()); + size_t removedCount = originalSize - data.size(); + numRows = data.size(); + return removedCount; + } + + int getNumRows() const; + int getNumColumns() const; + bool getIsMetaData() const; + bool isEmpty() const; + + // Clear all data and reset to initial state + void clear(); + + // Check if data type conversion is valid for a column + // @param columnNumber Column number to check (1-based) + // @param targetType Target C data type + // @return true if conversion is valid, false otherwise + bool isValidConversion(SQLUSMALLINT columnNumber, SQLSMALLINT targetType); + + // Get raw JSON value for SQLGetData type conversion + // @param rowIndex Row index (0-based) + // @param columnNumber Column number (1-based, as used in SQLGetData) + // @param value Output parameter for the JSON value + // @return true if successful, false otherwise + virtual bool getRawJsonValue(size_t rowIndex, SQLUSMALLINT columnNumber, nlohmann::json& value); + + // Bind a column to application variables for data retrieval + // @param columnNumber Column number to bind (1-based) + // @param targetType C data type for the column data + // @param targetValuePtr Pointer to the buffer for column data + // @param bufferLength Length of the buffer + // @param strLen_or_IndPtr Pointer to length/indicator variable + // @return SQL_SUCCESS if successful, SQL_ERROR otherwise + SQLRETURN bindColumn(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr); + + // Unbind columns + void unbindColumns(); + + // Get binding information for a column + // @param columnNumber Column number (1-based) + // @return Pointer to BindColInfo, or nullptr if not bound + BindColInfo* getBindColInfo(SQLUSMALLINT columnNumber); + + // Check if a column is bound + // @param columnNumber Column number (1-based) + // @return true if bound, false otherwise + bool isColumnBound(SQLUSMALLINT columnNumber); + + bool isRestful() const; + + void outputTable(int maxRows = 20) const; + + // Member variables, consistent with corresponding members in StatementHandle + int numRows; + int numColumns; + bool + isMetaData; // Whether metadata, affects json content format. Only valid for tree model, meaningless for table model + + // Data storage using vector format + std::vector columnNames; // Column names + std::vector columnTypes; // Column types + std::vector> data; // Row data, each row is a vector of ODBCFields + + // Column binding information + std::vector bindColInfo; + +protected: + StatementHandle* stmt; // Statement handle reference + static const std::unordered_map columnTypeToTSDataTypeMap; +}; + +#endif //ODBCRESULTSET_H diff --git a/iotdb-client/odbc/src/ODBCSessionResultSet.cpp b/iotdb-client/odbc/src/ODBCSessionResultSet.cpp new file mode 100644 index 0000000000000..be2d401e03197 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCSessionResultSet.cpp @@ -0,0 +1,394 @@ +/** + * 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 "ODBCSessionResultSet.h" + +// Project includes +#include "Common.h" +#include "Log.h" +#include "TableSession.h" +#include "TableSessionBuilder.h" +#include "driver.h" +#include "SessionDataSet.h" +#include "StatementHandle.h" +#include + +ODBCSessionResultSet::ODBCSessionResultSet(StatementHandle* stmtHandle) + : ODBCResultSet(stmtHandle) { + // Initialize member variables - sessionDataSet is automatically initialized as nullptr +} + +ODBCSessionResultSet::~ODBCSessionResultSet() { + if (sessionDataSet) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::~ODBCSessionResultSet: Closing session data set " + "(closeOperationHandle()). WARNING: This is an abnormal operation, should be closed " + "in loadDataInternal()", + LOG_LEVEL_WARN); + try { + sessionDataSet->closeOperationHandle(); + } catch (...) { + // Destruction may follow a failed connection; do not terminate the host process. + } + } + // Destructor - unique_ptr will handle cleanup automatically +} + +// Implement Session data loading +void ODBCSessionResultSet::loadData(std::unique_ptr _sessionDataSet) { + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadData: Starting", LOG_LEVEL_TRACE); + + // Clear existing data + clear(); + TSDataTypeColumnTypes.clear(); + + // Store the SessionDataSet for batch processing + sessionDataSet = std::move(_sessionDataSet); + + bool res = loadDataMetaData(); + if (!res) { + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadData: Failed to load metadata", + LOG_LEVEL_ERROR); + return; + } + res = loadDataInternal(); + if (!res) { + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadData: Failed to load data", + LOG_LEVEL_ERROR); + return; + } + + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadData: Initialized with " + std::to_string(numColumns) + + " columns, loaded first batch", + LOG_LEVEL_DEBUG); + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadData: Exiting", LOG_LEVEL_TRACE); +} + +bool ODBCSessionResultSet::loadDataNextBatch() { + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadDataNextBatch: Starting", + LOG_LEVEL_TRACE); + bool res = loadDataInternal(); + if (!res) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataNextBatch: Failed to load data", LOG_LEVEL_ERROR); + return false; + } + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadDataNextBatch: Exiting", + LOG_LEVEL_TRACE); + return true; +} + +bool ODBCSessionResultSet::loadDataMetaData() { + if (!sessionDataSet) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadSessionData: No data set available", LOG_LEVEL_WARN); + return false; + } + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadDataMetaData: Starting", + LOG_LEVEL_TRACE); + + // Get column information + columnNames = sessionDataSet->getColumnNames(); + columnTypes = sessionDataSet->getColumnTypeList(); + + // Normalize each column name: + // 1) If it ends with '`', keep content between the last two '`' (inclusive) + // 2) Otherwise, strip prefix up to the last '.' + for (size_t i = 0; i < columnNames.size(); ++i) { + // Relational aliases are already final names and may contain dots. + if (stmt->getConnection()->isTableModel) + continue; + if (!columnNames[i].empty() && columnNames[i].back() == '`') { + size_t secondLastBacktick = columnNames[i].rfind('`', columnNames[i].size() - 2); + if (secondLastBacktick != std::string::npos) { + columnNames[i] = columnNames[i].substr(secondLastBacktick); + } + } else { + size_t lastDot = columnNames[i].rfind('.'); + if (lastDot != std::string::npos) { + columnNames[i] = columnNames[i].substr(lastDot + 1); + } + } + } + + for (const auto& columnType : columnTypes) { + if (columnTypeToTSDataTypeMap.find(columnType) == columnTypeToTSDataTypeMap.end()) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadSessionData: Unknown column type: " + columnType, + LOG_LEVEL_ERROR); + throw std::runtime_error("Unsupported Session result type: " + columnType); + } + TSDataTypeColumnTypes.push_back(columnTypeToTSDataTypeMap.at(columnType)); + } + numColumns = columnNames.size(); + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadSessionData: " + std::to_string(numColumns) + + " columns found", + LOG_LEVEL_DEBUG); + + logMessage(stmt->getConnection(), "ODBCSessionResultSet::loadDataMetaData: Exiting", + LOG_LEVEL_TRACE); + return true; +} + +bool ODBCSessionResultSet::loadDataInternal() { + if (!sessionDataSet) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadSessionData: No data set available", LOG_LEVEL_WARN); + return false; + } + + data.clear(); + numRows = 0; + + // Get data iterator for efficient access + auto dataIterator = sessionDataSet->getIterator(); + // Load up to BATCH_SIZE rows + int loadedRows = 0; + try { + while (loadedRows < stmt->getConnection()->batchSize) { + bool hasNextRow = dataIterator.next(); + if (!hasNextRow) { + // No more rows to load + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: No more rows to load, closing session " + "data set (closeOperationHandle())", + LOG_LEVEL_DEBUG); + sessionDataSet->closeOperationHandle(); + sessionDataSet.reset(); + break; + } + std::vector rowData(numColumns); + + // Process each column/field + for (size_t col = 0; col < numColumns; ++col) { + int dataIteratorIndex = col + 1; + ODBCField& field = rowData[col]; + + // Session result conversion is shared by table and tree model responses. + // in the session response between the two models. + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Loading column " + std::to_string(col), + LOG_LEVEL_TRACE); + // Try different data types + if (dataIterator.isNullByIndex(dataIteratorIndex)) { + field = ODBCField::null(); + continue; + } + // Try to retrieve actual data + TSDataType::TSDataType tsDataType = TSDataTypeColumnTypes[col]; + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Loading column " + std::to_string(col) + + " with type " + columnTypes[col], + LOG_LEVEL_DEBUG); + + switch (tsDataType) { + case TSDataType::BOOLEAN: { + Optional boolVal = dataIterator.getBooleanByIndex(dataIteratorIndex); + if (boolVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Boolean Value: " + + std::to_string(boolVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(boolVal.value()); + field.setDataType(TSDataType::BOOLEAN); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Boolean Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::INT32: { + Optional intVal = dataIterator.getIntByIndex(dataIteratorIndex); + if (intVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Int Value: " + + std::to_string(intVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(static_cast(intVal.value())); + field.setDataType(TSDataType::INT32); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Int Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::INT64: { + Optional longVal = dataIterator.getLongByIndex(dataIteratorIndex); + if (longVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Long Value: " + + std::to_string(longVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(longVal.value()); + field.setDataType(TSDataType::INT64); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Long Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::FLOAT: { + Optional floatVal = dataIterator.getFloatByIndex(dataIteratorIndex); + if (floatVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Float Value: " + + std::to_string(floatVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(floatVal.value()); + field.setDataType(TSDataType::FLOAT); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Float Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::DOUBLE: { + Optional doubleVal = dataIterator.getDoubleByIndex(dataIteratorIndex); + if (doubleVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Double Value: " + + std::to_string(doubleVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(doubleVal.value()); + field.setDataType(TSDataType::DOUBLE); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Double Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::STRING: { + Optional stringVal = dataIterator.getStringByIndex(dataIteratorIndex); + if (stringVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get String Value: " + + stringVal.value(), + LOG_LEVEL_DEBUG); + field.setValue(stringVal.value()); + field.setDataType(TSDataType::STRING); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get String Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::DATE: { + Optional dateVal = dataIterator.getDateByIndex(dataIteratorIndex); + if (dateVal) { + logMessage( + stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Date Value (Dont output for now)", + LOG_LEVEL_DEBUG); + field.setValue(dateVal.value()); + field.setDataType(TSDataType::DATE); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Date Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::BLOB: { + Optional blobVal = dataIterator.getStringByIndex(dataIteratorIndex); + if (blobVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Blob Value: " + blobVal.value(), + LOG_LEVEL_DEBUG); + field.setValue(blobVal.value()); + field.setDataType(TSDataType::BLOB); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Blob Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::TIMESTAMP: { + Optional timestampVal = dataIterator.getTimestampByIndex(dataIteratorIndex); + if (timestampVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Timestamp Value: " + + std::to_string(timestampVal.value()), + LOG_LEVEL_DEBUG); + field.setValue(timestampVal.value()); + field.setDataType(TSDataType::TIMESTAMP); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Timestamp Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + case TSDataType::TEXT: { + Optional textVal = dataIterator.getStringByIndex(dataIteratorIndex); + if (textVal) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Text Value: " + textVal.value(), + LOG_LEVEL_DEBUG); + field.setValue(textVal.value()); + field.setDataType(TSDataType::TEXT); + } else { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Get Text Value: null", + LOG_LEVEL_DEBUG); + } + break; + } + default: + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: UNKNOWN TYPE, setting to null", + LOG_LEVEL_ERROR); + field = ODBCField::null(); + break; + } + } + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: copying row into data", LOG_LEVEL_DEBUG); + data.push_back(rowData); + loadedRows++; + } + } catch (const std::exception& e) { + logMessage(stmt->getConnection(), + std::string("ODBCSessionResultSet::loadDataInternal: Exception: ") + e.what(), + LOG_LEVEL_ERROR); + throw; + } catch (...) { + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Unknown exception occurred", + LOG_LEVEL_ERROR); + throw std::runtime_error("Unable to read Session result batch"); + } + + numRows = data.size(); + + logMessage(stmt->getConnection(), + "ODBCSessionResultSet::loadDataInternal: Loaded " + std::to_string(numRows) + + " rows in this batch", + LOG_LEVEL_DEBUG); + + // Return true if we loaded some data, false if no more data + return numRows > 0; +} diff --git a/iotdb-client/odbc/src/ODBCSessionResultSet.h b/iotdb-client/odbc/src/ODBCSessionResultSet.h new file mode 100644 index 0000000000000..c69b53f2692a5 --- /dev/null +++ b/iotdb-client/odbc/src/ODBCSessionResultSet.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. + */ +#ifndef ODBCSESSIONRESULTSET_H +#define ODBCSESSIONRESULTSET_H + +#include "ODBCResultSet.h" +#include "SessionDataSet.h" +#include + +class ODBCSessionResultSet : public ODBCResultSet { +public: + ODBCSessionResultSet(StatementHandle* stmtHandle); + ~ODBCSessionResultSet(); + + // Implement Session data loading + virtual void loadData(std::unique_ptr sessionDataSet) override; + virtual void loadData(const nlohmann::json& jsonData) override { + throw std::runtime_error("ODBCSessionResultSet: loadData with json is not supported"); + } + bool loadDataNextBatch(); + + // Session-specific functionality + bool loadDataInternal(); + bool loadDataMetaData(); + +private: + // Streaming support for Session API + std::unique_ptr sessionDataSet; // SessionDataSet for large result sets + std::vector TSDataTypeColumnTypes; // Column types +}; + +#endif // ODBCSESSIONRESULTSET_H diff --git a/iotdb-client/odbc/src/Pch.h b/iotdb-client/odbc/src/Pch.h new file mode 100644 index 0000000000000..2de6806c8b9a8 --- /dev/null +++ b/iotdb-client/odbc/src/Pch.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. + */ +#ifndef PCH_H +#define PCH_H + +// Common preprocessor definitions +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude unnecessary Windows API +#endif + +// Platform-specific includes +#if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) +#define WIN32 +#endif + +#ifdef WIN32 +#include +#endif + +// ODBC includes +#include +#include + +// Third-party libraries +#include + +// Boost includes +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Custom boost::throw_exception implementation when BOOST_NO_EXCEPTIONS is defined + +// Standard library includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif // PCH_H diff --git a/iotdb-client/odbc/src/StatementHandle.cpp b/iotdb-client/odbc/src/StatementHandle.cpp new file mode 100644 index 0000000000000..b00278b008157 --- /dev/null +++ b/iotdb-client/odbc/src/StatementHandle.cpp @@ -0,0 +1,120 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#include "StatementHandle.h" +#include "Log.h" +#include "driver.h" + +SQLSMALLINT streamNextBatch(StatementHandle* stmt) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "streamNextBatch: Entering", LOG_LEVEL_TRACE); + + // Only SELECT statements can be streamed + if (stmt->isQuery == false || (stmt->resultSetPtr->isMetaData && !stmt->isStream)) { + logMessage(cnct, + "streamNextBatch: Statement is not a SELECT query, cannot stream, comes to an end. " + "returning SQL_NO_DATA", + LOG_LEVEL_INFO); + return SQL_NO_DATA; + } + + // if fewer than page size rows were returned, it means the end of the data + if (stmt->resultSetPtr->getNumRows() < cnct->batchSize) { + logMessage(cnct, + "streamNextBatch: Fewer rows than fetch page size returned, end of data reached. " + "returning SQL_NO_DATA", + LOG_LEVEL_INFO); + return SQL_NO_DATA; + } + + if (!cnct->useRestful) { + bool ret; + try { + ret = static_cast(stmt->resultSetPtr.get())->loadDataNextBatch(); + } catch (const std::exception& error) { + stmt->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } + stmt->streamCurRow += stmt->resultSetPtr->getNumRows(); + if (!ret) { + logMessage(cnct, "streamNextBatch: Failed to load next batch of data", LOG_LEVEL_ERROR); + return SQL_NO_DATA; + } + return SQL_SUCCESS; + } + + // REST responses that did not request pagination already contain all rows. + if (!stmt->isStream) + return SQL_NO_DATA; + + std::stringstream newStatementTextss; + if (stmt->resultSetPtr->getNumRows() > 0) { + stmt->streamCurRow += stmt->resultSetPtr->getNumRows(); + logMessage(cnct, + "streamNextBatch: Updated streamCurRow to " + std::to_string(stmt->streamCurRow), + LOG_LEVEL_DEBUG); + } + if (isLogLevelEnabled(cnct, LOG_LEVEL_DEBUG)) { + std::stringstream oss; + oss << "streamNextBatch: curRow = " << stmt->curRow; + logMessage(cnct, oss.str(), LOG_LEVEL_DEBUG); + } + + newStatementTextss << stmt->statementText << " OFFSET " << stmt->streamCurRow << " LIMIT 9000"; + + logMessage(cnct, "streamNextBatch: Executing the next query batch", LOG_LEVEL_DEBUG); + SQLRETURN ret = IoTDB_ExecDirect(stmt, newStatementTextss.str(), false); + if (ret == SQL_NO_DATA) { + logMessage(cnct, "streamNextBatch: Streaming ended. returning SQL_NO_DATA", LOG_LEVEL_ERROR); + return ret; + } + if (ret != SQL_SUCCESS) { + logMessage(cnct, + "streamNextBatch: Failed to execute next batch with code: " + std::to_string(ret), + LOG_LEVEL_ERROR); + return ret; + } else { + logMessage(cnct, "streamNextBatch: Successfully executed next batch", LOG_LEVEL_TRACE); + } + logMessage(cnct, "streamNextBatch: Exiting", LOG_LEVEL_TRACE); + return ret; +} + +void StatementHandle::AllocateSessionResultSet() { + logMessage(getConnection(), "StatementHandle::AllocateSessionResultSet: Entering", + LOG_LEVEL_TRACE); + resultSetPtr = std::make_shared(this); + resultSetPtr->bindColInfo = columnBindings; + logMessage(getConnection(), "StatementHandle::AllocateSessionResultSet: Exiting", + LOG_LEVEL_TRACE); +} +void StatementHandle::AllocateRestResultSet() { + logMessage(getConnection(), "StatementHandle::AllocateRestResultSet: Entering", LOG_LEVEL_TRACE); + resultSetPtr = std::make_shared(this); + resultSetPtr->bindColInfo = columnBindings; + logMessage(getConnection(), "StatementHandle::AllocateRestResultSet: Exiting", LOG_LEVEL_TRACE); +} +void StatementHandle::ClearResultSet() { + logMessage(getConnection(), "StatementHandle::ClearResultSet: Entering", LOG_LEVEL_TRACE); + resultSetPtr = nullptr; + logMessage(getConnection(), "StatementHandle::ClearResultSet: Exiting", LOG_LEVEL_TRACE); +} diff --git a/iotdb-client/odbc/src/StatementHandle.h b/iotdb-client/odbc/src/StatementHandle.h new file mode 100644 index 0000000000000..38e345c451755 --- /dev/null +++ b/iotdb-client/odbc/src/StatementHandle.h @@ -0,0 +1,196 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#ifndef STATEMENTHANDLE_H +#define STATEMENTHANDLE_H + +#include "Pch.h" +#include "ODBCHandle.h" +#include "ConnectionHandle.h" +#include "DescriptorHandle.h" +#include "ODBCResultSet.h" +#include "ODBCSessionResultSet.h" +#include "ODBCRestResultSet.h" +#include + +// IoTDB Session API includes + +// Forward declarations to avoid circular dependencies +struct BindColInfo; + +struct ParameterBinding { + SQLSMALLINT inputOutputType = SQL_PARAM_INPUT; + SQLSMALLINT valueType = SQL_C_DEFAULT; + SQLSMALLINT parameterType = SQL_UNKNOWN_TYPE; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLPOINTER valuePtr = nullptr; + SQLLEN bufferLength = 0; + SQLLEN* indicatorPtr = nullptr; + SQLLEN* octetLengthPtr = nullptr; + std::vector streamedData; + std::vector streamedNull; + bool bound = false; +}; + +class StatementHandle : public ODBCHandle { +public: + StatementHandle(ConnectionHandle* connection) + : ODBCHandle(SQL_HANDLE_STMT), appRowDesc(nullptr), impRowDesc(nullptr), + appParamDesc(nullptr), + impParamDesc(nullptr), // Added: Application and implementation parameter descriptors + curRow(-1), isStream(false), isQuery(false), streamCurRow(0), connectionHandle(connection), + resultSetPtr(nullptr), + // Added: Cursor-related properties + cursorType(SQL_CURSOR_FORWARD_ONLY), // Default to forward-only cursor + concurrency(SQL_CONCUR_READ_ONLY), // Default to read-only concurrency + scrollable(SQL_NONSCROLLABLE), // Default to non-scrollable + sensitivity(SQL_UNSPECIFIED), // Default to unspecified sensitivity + useBookmarks(SQL_UB_OFF), // Default to not using bookmarks + // Added: Pointer-related properties + bookmarkPtr(nullptr), // Bookmark pointer + rowsFetchedPtr(nullptr), // Already exists, but kept for reference + rowStatusPtr(nullptr), rowBindOffsetPtr(nullptr), // Row bind offset pointer + paramsProcessedPtr(nullptr), // Processed parameters pointer + paramStatusPtr(nullptr), // Parameter status pointer + // Added: Size and quantity related properties + rowArraySize(1), // Default row array size is 1 + paramSetSize(1), // Default parameter set size is 1 + maxRows(0), // Default no maximum row limit + maxLength(0), // Default no maximum length limit + rowBindType(SQL_BIND_BY_COLUMN), paramBindType(SQL_BIND_BY_COLUMN), + // Added: Other boolean/flag properties + noScan(SQL_NOSCAN_OFF), // Default to enable scanning + retrieveData(SQL_RD_ON), // Default to retrieve data + metadataId(SQL_FALSE), // Default metadata ID mode off + enableAutoIPD(SQL_FALSE), // Parameter descriptors are not populated automatically + // Added: SQL Server specific properties + paramFocus(0), // Default parameter focus is 0 + prepared(false), rowsReturned(0), lastGetDataRow(-1), lastGetDataCol(0) { + implicitAppRowDesc = new DescriptorHandle(connection, DescriptorRole::APPLICATION_ROW, this); + implicitImpRowDesc = new DescriptorHandle(connection, DescriptorRole::IMPLEMENTATION_ROW, this); + implicitAppParamDesc = + new DescriptorHandle(connection, DescriptorRole::APPLICATION_PARAM, this); + implicitImpParamDesc = + new DescriptorHandle(connection, DescriptorRole::IMPLEMENTATION_PARAM, this); + appRowDesc = implicitAppRowDesc; + impRowDesc = implicitImpRowDesc; + appParamDesc = implicitAppParamDesc; + impParamDesc = implicitImpParamDesc; + cursorName = "SQL_CUR" + std::to_string(reinterpret_cast(this)); + if (connection) + connection->statements.push_back(this); + } + + ~StatementHandle() { + if (connectionHandle) { + auto& statements = connectionHandle->statements; + statements.erase(std::remove(statements.begin(), statements.end(), this), statements.end()); + } + ClearResultSet(); + delete implicitAppRowDesc; + delete implicitImpRowDesc; + delete implicitAppParamDesc; + delete implicitImpParamDesc; + } + + ConnectionHandle* getConnection() const { + return connectionHandle; + } + + void AllocateSessionResultSet(); + void AllocateRestResultSet(); + void ClearResultSet(); + + std::string statementText; + std::string cursorName; + bool prepared; + SQLULEN rowsReturned; + std::vector columnBindings; + std::vector parameterBindings; + bool needsParameterData = false; + SQLULEN nextDataSet = 0; + SQLULEN activeDataSet = std::numeric_limits::max(); + size_t nextDataParameter = 0; + size_t activeDataParameter = std::numeric_limits::max(); + SQLHANDLE appRowDesc; // Application row descriptor (already exists) + SQLHANDLE impRowDesc; // Implementation row descriptor (already exists) + SQLHANDLE appParamDesc; // Added: Application parameter descriptor + SQLHANDLE impParamDesc; // Added: Implementation parameter descriptor + DescriptorHandle* implicitAppRowDesc; + DescriptorHandle* implicitImpRowDesc; + DescriptorHandle* implicitAppParamDesc; + DescriptorHandle* implicitImpParamDesc; + + std::shared_ptr resultSetPtr; + int curRow; + bool isStream; // Flag indicating whether the current statement is in streaming mode. + bool isQuery; // Flag indicating whether the current statement is a query statement. + int streamCurRow; // Available in streaming mode, indicates how many to follow after OFFSET. Initially 0. + // Add more fields if needed + + // Added: Cursor-related properties + SQLINTEGER cursorType; // Cursor type (SQL_CURSOR_FORWARD_ONLY, etc.) + SQLINTEGER concurrency; // Concurrency type (SQL_CONCUR_READ_ONLY, etc.) + SQLINTEGER scrollable; // Scrollable (SQL_SCROLLABLE/SQL_NONSCROLLABLE) + SQLINTEGER sensitivity; // Cursor sensitivity (SQL_SENSITIVE/SQL_INSENSITIVE, etc.) + SQLINTEGER useBookmarks; // Bookmark usage (SQL_UB_OFF/SQL_UB_VARIABLE, etc.) + + // Added: Pointer-related properties + SQLULEN* bookmarkPtr; // Bookmark pointer + SQLULEN* rowsFetchedPtr; + SQLUSMALLINT* rowStatusPtr; // Status for the single supported row in a rowset + SQLULEN* rowBindOffsetPtr; // Row bind offset pointer + SQLULEN* paramsProcessedPtr; // Processed parameters pointer + SQLUSMALLINT* paramStatusPtr; // Parameter status pointer + SQLULEN* paramBindOffsetPtr = nullptr; + + // Added: Size and quantity related properties + SQLULEN rowArraySize; // Row array size + SQLULEN paramSetSize; // Parameter set size + SQLULEN maxRows; // Maximum number of rows to return + SQLULEN maxLength; // Maximum data length + SQLULEN rowBindType; // Column binding or row binding method + SQLULEN paramBindType; + + // Added: Other boolean/flag properties + SQLINTEGER noScan; // Scan option (SQL_NOSCAN/SQL_UNNAMED) + SQLINTEGER retrieveData; // Data retrieval option (SQL_RD_ON/SQL_RD_OFF) + SQLINTEGER metadataId; // Metadata ID mode (SQL_TRUE/SQL_FALSE) + SQLINTEGER enableAutoIPD; // Auto IPD enable (SQL_TRUE/SQL_FALSE) + + // Added: SQL Server specific properties + SQLINTEGER paramFocus; // Parameter focus + + // SQLGetData tracking + std::vector getDataOffsets; + int lastGetDataRow; + SQLUSMALLINT lastGetDataCol; + +private: + ConnectionHandle* connectionHandle; +}; + +// Function declarations +SQLSMALLINT streamNextBatch(StatementHandle* stmt); + +#endif //STATEMENTHANDLE_H diff --git a/iotdb-client/odbc/src/StatementUtils.cpp b/iotdb-client/odbc/src/StatementUtils.cpp new file mode 100644 index 0000000000000..62a2fccf9fabd --- /dev/null +++ b/iotdb-client/odbc/src/StatementUtils.cpp @@ -0,0 +1,101 @@ +/** + * 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 "StatementUtils.h" + +#include +#include + +bool IsQueryStatement(const std::string& sql) { + size_t start = 0; + while ((start = sql.find_first_not_of(" \t\n\r", start)) != std::string::npos) { + if (sql.compare(start, 2, "--") == 0) { + start = sql.find_first_of("\n\r", start + 2); + if (start == std::string::npos) + return false; + } else if (sql.compare(start, 2, "/*") == 0) { + start = sql.find("*/", start + 2); + if (start == std::string::npos) + return false; + start += 2; + } else { + break; + } + } + if (start == std::string::npos) + return false; + + size_t end = start; + while (end < sql.size() && + (std::isalnum(static_cast(sql[end])) || sql[end] == '_')) + ++end; + std::string keyword = sql.substr(start, end == std::string::npos ? end : end - start); + std::transform(keyword.begin(), keyword.end(), keyword.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return keyword == "select" || keyword == "with" || keyword == "show" || keyword == "describe" || + keyword == "desc" || keyword == "list"; +} + +std::vector ParameterMarkerPositions(const std::string& sql) { + std::vector result; + char quote = '\0'; + bool lineComment = false; + bool blockComment = false; + for (size_t i = 0; i < sql.size(); ++i) { + const char current = sql[i]; + const char next = i + 1 < sql.size() ? sql[i + 1] : '\0'; + if (lineComment) { + if (current == '\n' || current == '\r') + lineComment = false; + continue; + } + if (blockComment) { + if (current == '*' && next == '/') { + blockComment = false; + ++i; + } + continue; + } + if (quote) { + if (current == quote) { + if (next == quote) + ++i; + else + quote = '\0'; + } + continue; + } + if (current == '-' && next == '-') { + lineComment = true; + ++i; + continue; + } + if (current == '/' && next == '*') { + blockComment = true; + ++i; + continue; + } + if (current == '\'' || current == '"' || current == '`') { + quote = current; + continue; + } + if (current == '?') + result.push_back(i); + } + return result; +} diff --git a/iotdb-client/odbc/src/StatementUtils.h b/iotdb-client/odbc/src/StatementUtils.h new file mode 100644 index 0000000000000..9fee583258a51 --- /dev/null +++ b/iotdb-client/odbc/src/StatementUtils.h @@ -0,0 +1,28 @@ +/** + * 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. + */ +#ifndef STATEMENT_UTILS_H +#define STATEMENT_UTILS_H + +#include +#include + +bool IsQueryStatement(const std::string& sql); +std::vector ParameterMarkerPositions(const std::string& sql); + +#endif // STATEMENT_UTILS_H diff --git a/iotdb-client/odbc/src/Unicode.cpp b/iotdb-client/odbc/src/Unicode.cpp new file mode 100644 index 0000000000000..4542aa78bc626 --- /dev/null +++ b/iotdb-client/odbc/src/Unicode.cpp @@ -0,0 +1,355 @@ +/* + * 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 "driver.h" +#include + +namespace { +std::string utf8(const SQLWCHAR* value, SQLINTEGER length) { + if (length < 0 && length != SQL_NTS) + throw std::invalid_argument("Invalid string or buffer length"); + if (!value) + return ""; + size_t count = length == SQL_NTS ? std::char_traits::length(value) : length; + std::u16string input(count, u'\0'); + if (count) + std::memcpy(&input[0], value, count * sizeof(SQLWCHAR)); + return std::wstring_convert, char16_t>().to_bytes(input); +} + +SQLCHAR* ansi(const SQLWCHAR* original, std::string& converted) { + return original ? reinterpret_cast(&converted[0]) : nullptr; +} + +template SQLRETURN invoke(SQLHANDLE handle, F call) { + if (!handle) + return SQL_INVALID_HANDLE; + try { + return call(); + } catch (const std::invalid_argument& error) { + auto* h = static_cast(handle); + h->clearDiagnostics(); + h->addDiagnostic("HY090", error.what()); + } catch (const std::exception& error) { + auto* h = static_cast(handle); + h->clearDiagnostics(); + h->addDiagnostic("22018", error.what()); + } + return SQL_ERROR; +} + +template +SQLRETURN output(SQLHANDLE handle, const std::string& value, SQLWCHAR* buffer, SQLINTEGER capacity, + Length* length, bool bytes, bool diagnostic = false) { + if (capacity < 0) + throw std::invalid_argument("Invalid string or buffer length"); + auto wide = std::wstring_convert, char16_t>().from_bytes(value); + const size_t units = bytes ? sizeof(SQLWCHAR) : 1; + if (length) + *length = static_cast( + std::min(wide.size() * units, static_cast(std::numeric_limits::max()))); + if (!buffer) + return SQL_SUCCESS; + size_t slots = static_cast(capacity) / units; + size_t copied = slots ? std::min(wide.size(), slots - 1) : 0; + if (copied) + std::memcpy(buffer, wide.data(), copied * sizeof(SQLWCHAR)); + if (slots) + buffer[copied] = 0; + if (copied < wide.size()) { + if (!diagnostic) + static_cast(handle)->addDiagnostic("01004", "String data, right truncated"); + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; +} + +bool stringInfo(SQLUSMALLINT info) { + switch (info) { + case SQL_DRIVER_NAME: + case SQL_DRIVER_ODBC_VER: + case SQL_DRIVER_VER: + case SQL_DATABASE_NAME: + case SQL_DESCRIBE_PARAMETER: + case SQL_DBMS_NAME: + case SQL_DBMS_VER: + case SQL_IDENTIFIER_QUOTE_CHAR: + case SQL_CATALOG_NAME: + case SQL_CATALOG_NAME_SEPARATOR: + case SQL_ORDER_BY_COLUMNS_IN_SELECT: + case SQL_COLUMN_ALIAS: + case SQL_CATALOG_TERM: + case SQL_SCHEMA_TERM: + case SQL_SEARCH_PATTERN_ESCAPE: + case SQL_SPECIAL_CHARACTERS: + case SQL_NEED_LONG_DATA_LEN: + case SQL_DATA_SOURCE_NAME: + case SQL_DATA_SOURCE_READ_ONLY: + case SQL_MAX_ROW_SIZE_INCLUDES_LONG: + case SQL_PROCEDURE_TERM: + case SQL_INTEGRITY: + case SQL_TABLE_TERM: + case SQL_USER_NAME: + case SQL_MULT_RESULT_SETS: + case SQL_SERVER_NAME: + case SQL_ACCESSIBLE_PROCEDURES: + case SQL_ACCESSIBLE_TABLES: + case SQL_PROCEDURES: + case SQL_LIKE_ESCAPE_CLAUSE: + case SQL_OUTER_JOINS: + return true; + default: + return false; + } +} +} // namespace + +SQLRETURN SQL_API SQLExecDirectW(SQLHSTMT h, SQLWCHAR* sql, SQLINTEGER length) { + return invoke(h, [&] { + auto value = utf8(sql, length); + return SQLExecDirect(h, ansi(sql, value), static_cast(value.size())); + }); +} +SQLRETURN SQL_API SQLPrepareW(SQLHSTMT h, SQLWCHAR* sql, SQLINTEGER length) { + return invoke(h, [&] { + auto value = utf8(sql, length); + return SQLPrepare(h, ansi(sql, value), static_cast(value.size())); + }); +} +SQLRETURN SQL_API SQLDriverConnectW(SQLHDBC h, SQLHWND window, SQLWCHAR* input, SQLSMALLINT size, + SQLWCHAR* buffer, SQLSMALLINT capacity, SQLSMALLINT* length, + SQLUSMALLINT completion) { + return invoke(h, [&] { + if (capacity < 0) + throw std::invalid_argument("Invalid string or buffer length"); + auto value = utf8(input, size); + SQLRETURN rc = + SQLDriverConnect(h, window, ansi(input, value), SQL_NTS, nullptr, 0, nullptr, completion); + if (!SQL_SUCCEEDED(rc)) + return rc; + SQLRETURN copied = output(h, value, buffer, capacity, length, false); + return copied == SQL_SUCCESS ? rc : copied; + }); +} +SQLRETURN SQL_API SQLConnectW(SQLHDBC h, SQLWCHAR* dsn, SQLSMALLINT ds, SQLWCHAR* user, + SQLSMALLINT us, SQLWCHAR* password, SQLSMALLINT ps) { + return invoke(h, [&] { + auto d = utf8(dsn, ds), u = utf8(user, us), p = utf8(password, ps); + return SQLConnect(h, ansi(dsn, d), SQL_NTS, ansi(user, u), SQL_NTS, ansi(password, p), SQL_NTS); + }); +} +SQLRETURN SQL_API SQLTablesW(SQLHSTMT h, SQLWCHAR* cat, SQLSMALLINT cs, SQLWCHAR* schema, + SQLSMALLINT ss, SQLWCHAR* table, SQLSMALLINT ts, SQLWCHAR* type, + SQLSMALLINT ys) { + return invoke(h, [&] { + auto c = utf8(cat, cs), s = utf8(schema, ss), t = utf8(table, ts), y = utf8(type, ys); + return SQLTables(h, ansi(cat, c), SQL_NTS, ansi(schema, s), SQL_NTS, ansi(table, t), SQL_NTS, + ansi(type, y), SQL_NTS); + }); +} +SQLRETURN SQL_API SQLColumnsW(SQLHSTMT h, SQLWCHAR* cat, SQLSMALLINT cs, SQLWCHAR* schema, + SQLSMALLINT ss, SQLWCHAR* table, SQLSMALLINT ts, SQLWCHAR* name, + SQLSMALLINT ns) { + return invoke(h, [&] { + auto c = utf8(cat, cs), s = utf8(schema, ss), t = utf8(table, ts), n = utf8(name, ns); + return SQLColumns(h, ansi(cat, c), SQL_NTS, ansi(schema, s), SQL_NTS, ansi(table, t), SQL_NTS, + ansi(name, n), SQL_NTS); + }); +} +SQLRETURN SQL_API SQLDescribeColW(SQLHSTMT h, SQLUSMALLINT col, SQLWCHAR* name, + SQLSMALLINT capacity, SQLSMALLINT* length, SQLSMALLINT* type, + SQLULEN* size, SQLSMALLINT* digits, SQLSMALLINT* nullable) { + return invoke(h, [&] { + std::vector value(32767); + SQLRETURN rc = + SQLDescribeCol(h, col, value.data(), 32767, nullptr, type, size, digits, nullable); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(value.data()), name, capacity, length, false); + }); +} +SQLRETURN SQL_API SQLColAttributeW(SQLHSTMT h, SQLUSMALLINT col, SQLUSMALLINT field, + SQLPOINTER buffer, SQLSMALLINT capacity, SQLSMALLINT* length, + SQLLEN* numeric) { + switch (field) { + case SQL_COLUMN_NAME: + case SQL_DESC_NAME: + case SQL_DESC_LABEL: + case SQL_DESC_TYPE_NAME: + case SQL_DESC_BASE_COLUMN_NAME: + case SQL_DESC_BASE_TABLE_NAME: + case SQL_DESC_CATALOG_NAME: + case SQL_DESC_SCHEMA_NAME: + case SQL_DESC_TABLE_NAME: + case SQL_DESC_LITERAL_PREFIX: + case SQL_DESC_LITERAL_SUFFIX: + case SQL_DESC_LOCAL_TYPE_NAME: + return invoke(h, [&] { + std::vector value(32767); + SQLRETURN rc = SQLColAttribute(h, col, field, value.data(), 32767, nullptr, numeric); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(value.data()), static_cast(buffer), + capacity, length, true); + }); + default: + return SQLColAttribute(h, col, field, buffer, capacity, length, numeric); + } +} +SQLRETURN SQL_API SQLGetInfoW(SQLHDBC h, SQLUSMALLINT info, SQLPOINTER buffer, SQLSMALLINT capacity, + SQLSMALLINT* length) { + if (!stringInfo(info)) + return SQLGetInfo(h, info, buffer, capacity, length); + return invoke(h, [&] { + std::vector value(32767); + SQLRETURN rc = SQLGetInfo(h, info, value.data(), 32767, nullptr); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(value.data()), static_cast(buffer), + capacity, length, true); + }); +} +SQLRETURN SQL_API SQLGetDiagRecW(SQLSMALLINT type, SQLHANDLE h, SQLSMALLINT record, SQLWCHAR* state, + SQLINTEGER* native, SQLWCHAR* message, SQLSMALLINT capacity, + SQLSMALLINT* length) { + if (!h || static_cast(h)->getHandleType() != type) + return SQL_INVALID_HANDLE; + if (record <= 0 || capacity < 0) + return SQL_ERROR; + // Diagnostic reads must not change the records they are reading. + try { + std::string s, m; + int n; + if (!static_cast(h)->getDiagnostic(record, s, m, n)) + return SQL_NO_DATA; + if (state) + output(h, s, state, 6, nullptr, false, true); + if (native) + *native = n; + return output(h, m, message, capacity, length, false, true); + } catch (...) { + return SQL_ERROR; + } +} +SQLRETURN SQL_API SQLGetDiagFieldW(SQLSMALLINT type, SQLHANDLE h, SQLSMALLINT record, + SQLSMALLINT field, SQLPOINTER buffer, SQLSMALLINT capacity, + SQLSMALLINT* length) { + switch (field) { + case SQL_DIAG_SQLSTATE: + case SQL_DIAG_MESSAGE_TEXT: + case SQL_DIAG_CLASS_ORIGIN: + case SQL_DIAG_SUBCLASS_ORIGIN: + case SQL_DIAG_CONNECTION_NAME: + case SQL_DIAG_SERVER_NAME: { + // Do not use invoke: reading diagnostics must preserve the diagnostic stack. + try { + std::vector value(32767); + SQLRETURN rc = SQLGetDiagField(type, h, record, field, value.data(), 32767, nullptr); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(value.data()), static_cast(buffer), + capacity, length, true, true); + } catch (...) { + return SQL_ERROR; + } + } + default: + return SQLGetDiagField(type, h, record, field, buffer, capacity, length); + } +} +SQLRETURN SQL_API SQLGetTypeInfoW(SQLHSTMT h, SQLSMALLINT type) { + return SQLGetTypeInfo(h, type); +} +SQLRETURN SQL_API SQLGetStmtAttrW(SQLHSTMT h, SQLINTEGER attr, SQLPOINTER value, SQLINTEGER size, + SQLINTEGER* length) { + return SQLGetStmtAttr(h, attr, value, size, length); +} +SQLRETURN SQL_API SQLSetStmtAttrW(SQLHSTMT h, SQLINTEGER attr, SQLPOINTER value, SQLINTEGER size) { + return SQLSetStmtAttr(h, attr, value, size); +} +SQLRETURN SQL_API SQLGetConnectAttrW(SQLHDBC h, SQLINTEGER attr, SQLPOINTER value, SQLINTEGER size, + SQLINTEGER* length) { + if (attr != SQL_ATTR_CURRENT_CATALOG) + return SQLGetConnectAttr(h, attr, value, size, length); + return invoke(h, [&] { + auto* connection = static_cast(h); + connection->clearDiagnostics(); + return output(h, connection->database, static_cast(value), size, length, true); + }); +} +SQLRETURN SQL_API SQLSetConnectAttrW(SQLHDBC h, SQLINTEGER attr, SQLPOINTER value, + SQLINTEGER size) { + if (attr != SQL_ATTR_CURRENT_CATALOG) + return SQLSetConnectAttr(h, attr, value, size); + return invoke(h, [&] { + if ((size < 0 && size != SQL_NTS) || (size >= 0 && size % sizeof(SQLWCHAR))) + throw std::invalid_argument("Invalid string or buffer length"); + auto converted = utf8(static_cast(value), size == SQL_NTS ? size : size / 2); + return SQLSetConnectAttr(h, attr, value ? &converted[0] : nullptr, + static_cast(converted.size())); + }); +} +SQLRETURN SQL_API SQLNativeSqlW(SQLHDBC h, SQLWCHAR* input, SQLINTEGER size, SQLWCHAR* buffer, + SQLINTEGER capacity, SQLINTEGER* length) { + return invoke(h, [&] { + auto converted = utf8(input, size); + SQLRETURN rc = SQLNativeSql(h, ansi(input, converted), + static_cast(converted.size()), nullptr, 0, nullptr); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, converted, buffer, capacity, length, false); + }); +} +SQLRETURN SQL_API SQLGetDescFieldW(SQLHDESC h, SQLSMALLINT record, SQLSMALLINT field, + SQLPOINTER value, SQLINTEGER size, SQLINTEGER* length) { + if (field != SQL_DESC_NAME) + return SQLGetDescField(h, record, field, value, size, length); + return invoke(h, [&] { + std::vector converted(32767); + SQLRETURN rc = SQLGetDescField(h, record, field, converted.data(), 32767, nullptr); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(converted.data()), static_cast(value), size, + length, true); + }); +} +SQLRETURN SQL_API SQLGetDescRecW(SQLHDESC h, SQLSMALLINT record, SQLWCHAR* name, + SQLSMALLINT capacity, SQLSMALLINT* length, SQLSMALLINT* type, + SQLSMALLINT* sub, SQLLEN* size, SQLSMALLINT* precision, + SQLSMALLINT* scale, SQLSMALLINT* nullable) { + return invoke(h, [&] { + std::vector converted(32767); + SQLRETURN rc = SQLGetDescRec(h, record, converted.data(), 32767, nullptr, type, sub, size, + precision, scale, nullable); + if (!SQL_SUCCEEDED(rc)) + return rc; + return output(h, reinterpret_cast(converted.data()), name, capacity, length, false); + }); +} +SQLRETURN SQL_API SQLSetDescFieldW(SQLHDESC h, SQLSMALLINT record, SQLSMALLINT field, + SQLPOINTER value, SQLINTEGER size) { + if (field != SQL_DESC_NAME) + return SQLSetDescField(h, record, field, value, size); + return invoke(h, [&] { + if ((size < 0 && size != SQL_NTS) || (size >= 0 && size % sizeof(SQLWCHAR))) + throw std::invalid_argument("Invalid string or buffer length"); + auto converted = utf8(static_cast(value), size == SQL_NTS ? size : size / 2); + return SQLSetDescField(h, record, field, value ? &converted[0] : nullptr, + static_cast(converted.size())); + }); +} diff --git a/iotdb-client/odbc/src/driver.cpp b/iotdb-client/odbc/src/driver.cpp new file mode 100644 index 0000000000000..60de638d0137a --- /dev/null +++ b/iotdb-client/odbc/src/driver.cpp @@ -0,0 +1,6149 @@ +/** + * 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 "ODBCField.h" +#include "Pch.h" +#include "driver.h" + +// Additional includes not in Pch.h +#include +#include +#include +#include // Need to support wide characters + +// ODBC includes +#include +#include +#include + +// Project includes +#include "DiagnosticManager.h" +#include "Log.h" +#include "rest_api_client.h" +#include "session_api_client.h" +#include "StatementHandle.h" +#include "StatementUtils.h" +#include "ConnectionHandle.h" +#include "ConnectionString.h" +#include "EnvironmentHandle.h" +#include "CopyField.h" + +static void PopulateImplementationRowDescriptor(StatementHandle* stmt); + +/** + * Copy std::string to ODBC string buffer + * @param value input string + * @param stringBuffer output buffer + * @param bufferLength buffer size (in bytes) + * @param stringLength returns actual string length (excluding null terminator) + */ +SQLRETURN setString(const std::string& value, SQLPOINTER stringBuffer, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLength) { + const SQLSMALLINT actualLength = static_cast( + std::min(value.size(), static_cast(std::numeric_limits::max()))); + + if (stringLength) { + *stringLength = actualLength; + } + + if (stringBuffer && bufferLength > 0) { + const size_t maxCopy = std::min(value.size(), static_cast(bufferLength - 1)); + + if (!value.empty()) { + std::memcpy(stringBuffer, value.data(), maxCopy); + } + static_cast(stringBuffer)[maxCopy] = '\0'; + if (maxCopy < value.size()) + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; +} + +/** + * Copy C-style string to ODBC string buffer + * @param value input string (must be null-terminated) + * @param stringBuffer output buffer + * @param bufferLength buffer size (in bytes) + * @param stringLength returns actual string length (excluding null terminator) + */ +SQLRETURN setString(const char* value, SQLPOINTER stringBuffer, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLength) { + if (!value) + value = ""; + + const size_t valueLen = std::strlen(value); + if (stringLength) { + *stringLength = static_cast( + std::min(valueLen, static_cast(std::numeric_limits::max()))); + } + + if (stringBuffer && bufferLength > 0) { + size_t maxCopy = std::min(valueLen, static_cast(bufferLength - 1)); + + std::memcpy(stringBuffer, value, maxCopy); + static_cast(stringBuffer)[maxCopy] = '\0'; + if (maxCopy < valueLen) + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLAllocConnect(SQLHENV environmentHandle, SQLHDBC* connectionHandle) { + logMessage("Enter SQLAllocConnect"); + // This is a deprecated function, but unixodbc needs to call it. Call SQLAllocHandle function in this function. + SQLRETURN retcode = SQLAllocHandle(SQL_HANDLE_DBC, environmentHandle, connectionHandle); + if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO) { + // Handle error + std::ostringstream oss; + oss << "SQLAllocConnect: Failed to allocate connection handle. retcode = " << retcode << "\n"; + logMessage(oss.str()); + return retcode; + } + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLAllocEnv(SQLHENV* environmentHandle) { + logMessage("SQLAllocEnv"); + // Deprecated function, delegate to SQLAllocHandle for env allocation. + SQLRETURN retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, environmentHandle); + if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO) { + std::ostringstream oss; + oss << "SQLAllocEnv: Failed to allocate environment handle. retcode = " << retcode << "\n"; + logMessage(oss.str()); + return retcode; + } + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLAllocHandle(const SQLSMALLINT handleType, const SQLHANDLE parentHandle, + SQLHANDLE* outputHandle) { + logMessage("SQLAllocHandle: Entering."); + + if (!outputHandle) { + logMessage("SQLAllocHandle: outputHandle is null"); + return SQL_ERROR; + } + *outputHandle = nullptr; + + std::stringstream logStream; + logStream << "Parameters: " + << "handleType = " << handleType << ", parentHandle = " << handleToString(parentHandle); + logMessage(logStream.str()); + + try { + switch (handleType) { + case SQL_HANDLE_ENV: + if (parentHandle != SQL_NULL_HANDLE) + return SQL_INVALID_HANDLE; + *outputHandle = new EnvironmentHandle(); + break; + + case SQL_HANDLE_DBC: { + auto* env = static_cast(parentHandle); + if (!env || env->getHandleType() != SQL_HANDLE_ENV || env->isHandleFreed()) + return SQL_INVALID_HANDLE; + *outputHandle = new ConnectionHandle(env); + break; + } + + case SQL_HANDLE_STMT: { + auto* cnct = static_cast(parentHandle); + if (!cnct || cnct->getHandleType() != SQL_HANDLE_DBC || cnct->isHandleFreed()) + return SQL_INVALID_HANDLE; + auto* stmt = new StatementHandle(cnct); + *outputHandle = stmt; + break; + } + + case SQL_HANDLE_DESC: { + auto* cnct = static_cast(parentHandle); + if (!cnct || cnct->getHandleType() != SQL_HANDLE_DBC || cnct->isHandleFreed()) + return SQL_INVALID_HANDLE; + *outputHandle = new DescriptorHandle(cnct, DescriptorRole::EXPLICIT); + break; + } + + default: + logMessage("SQLAllocHandle: unsupported handle type"); + return SQL_ERROR; + } + } catch (const std::bad_alloc&) { + if (parentHandle) + static_cast(parentHandle)->addDiagnostic("HY001", "Memory allocation failed"); + return SQL_ERROR; + } catch (const std::exception& error) { + if (parentHandle) + static_cast(parentHandle)->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } + + logMessage("Allocated handle: " + handleToString(*outputHandle)); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLAllocStmt(SQLHDBC connectionHandle, SQLHSTMT* statementHandle) { + logMessage("Entering SQLAllocStmt"); + if (!connectionHandle) + return SQL_INVALID_HANDLE; + if (!statementHandle) { + static_cast(connectionHandle) + ->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + // Deprecated function, delegate to SQLAllocHandle for stmt allocation. + SQLRETURN retcode = SQLAllocHandle(SQL_HANDLE_STMT, connectionHandle, statementHandle); + if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO) { + std::ostringstream oss; + oss << "SQLAllocStmt: Failed to allocate statement handle. retcode = " << retcode << "\n"; + logMessage(oss.str()); + return retcode; + } + logMessage("Exiting SQLAllocStmt\n"); + return SQL_SUCCESS; +} + +/* +The numbering of columnNumber starts from 1, with column 0 being the bookmark column. However, IoTDB does not have a bookmark column. The numbering still starts from 1. +*/ +SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLSMALLINT targetType, SQLPOINTER targetValuePtr, SQLLEN bufferLength, + SQLLEN* strLen_or_IndPtr) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLBindCol: Entering", LOG_LEVEL_TRACE); + if (!stmt) + return SQL_INVALID_HANDLE; + stmt->clearDiagnostics(); + if (columnNumber == 0) { + stmt->addDiagnostic("HYC00", "Bookmarks are not supported"); + return SQL_ERROR; + } + if (columnNumber > static_cast(std::numeric_limits::max())) { + stmt->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "Parameters: " + << "statementHandle = " << handleToString(statementHandle) + << ", columnNumber = " << columnNumber << ", targetType = " << targetType + << ", targetValue = " << targetValuePtr << ", bufferLength = " << bufferLength + << ", strLen_or_Ind = " << strLen_or_IndPtr; + + logMessage(stmt->getConnection(), logStream.str(), LOG_LEVEL_TRACE); + } + + if (stmt->columnBindings.size() <= columnNumber) + stmt->columnBindings.resize(columnNumber + 1); + + BindColInfo& binding = stmt->columnBindings[columnNumber]; + if (!targetValuePtr && !strLen_or_IndPtr) { + binding = BindColInfo(); + auto* ard = static_cast(stmt->appRowDesc); + if (DescriptorRecord* record = ard->findRecord(columnNumber)) { + record->dataPtr = nullptr; + record->octetLengthPtr = nullptr; + record->indicatorPtr = nullptr; + } + if (stmt->resultSetPtr) + stmt->resultSetPtr->bindColInfo = stmt->columnBindings; + return SQL_SUCCESS; + } + + SQLRETURN result = SQL_SUCCESS; + if (stmt->resultSetPtr) { + result = stmt->resultSetPtr->bindColumn(columnNumber, targetType, targetValuePtr, bufferLength, + strLen_or_IndPtr); + if (!SQL_SUCCEEDED(result)) + return result; + stmt->columnBindings = stmt->resultSetPtr->bindColInfo; + } else { + binding.targetType = targetType; + binding.targetValuePtr = targetValuePtr; + binding.bufferLength = bufferLength; + binding.strLen_or_IndPtr = strLen_or_IndPtr; + binding.isBound = true; + } + + auto* ard = static_cast(stmt->appRowDesc); + DescriptorRecord& record = ard->record(static_cast(columnNumber)); + ard->count = std::max(ard->count, static_cast(columnNumber)); + record.setConciseType(targetType); + record.dataPtr = targetValuePtr; + record.octetLength = bufferLength; + record.octetLengthPtr = strLen_or_IndPtr; + record.indicatorPtr = strLen_or_IndPtr; + + logMessage(stmt->getConnection(), "SQLBindCol: Exiting", LOG_LEVEL_TRACE); + return result; +} + +SQLRETURN SQL_API SQLBindParam(SQLHSTMT statementHandle, SQLUSMALLINT parameterNumber, + SQLSMALLINT valueType, SQLSMALLINT parameterType, + SQLULEN lengthPrecision, SQLSMALLINT parameterScale, + SQLPOINTER parameterValue, SQLLEN* strLen_or_Ind) { + return SQLBindParameter(statementHandle, parameterNumber, SQL_PARAM_INPUT, valueType, + parameterType, lengthPrecision, parameterScale, parameterValue, + static_cast(lengthPrecision), strLen_or_Ind); +} + +SQLRETURN SQL_API SQLBindParameter(SQLHSTMT statementHandle, SQLUSMALLINT parameterNumber, + SQLSMALLINT inputOutputType, SQLSMALLINT valueType, + SQLSMALLINT parameterType, SQLULEN columnSize, + SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, + SQLLEN bufferLength, SQLLEN* strLen_or_IndPtr) { + if (!statementHandle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + if (parameterNumber == 0 || + parameterNumber > static_cast(std::numeric_limits::max())) { + stmt->addDiagnostic("07009", "Invalid parameter number"); + return SQL_ERROR; + } + if (inputOutputType != SQL_PARAM_INPUT) { + stmt->addDiagnostic("HYC00", "Only input parameters are supported"); + return SQL_ERROR; + } + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (stmt->parameterBindings.size() < parameterNumber) + stmt->parameterBindings.resize(parameterNumber); + ParameterBinding& binding = stmt->parameterBindings[parameterNumber - 1]; + binding.inputOutputType = inputOutputType; + binding.valueType = valueType; + binding.parameterType = parameterType; + binding.columnSize = columnSize; + binding.decimalDigits = decimalDigits; + binding.valuePtr = parameterValuePtr; + binding.bufferLength = bufferLength; + binding.indicatorPtr = strLen_or_IndPtr; + binding.streamedData.clear(); + binding.streamedNull.clear(); + binding.bound = true; + + auto* apd = static_cast(stmt->appParamDesc); + DescriptorRecord& appRecord = apd->record(static_cast(parameterNumber)); + apd->count = std::max(apd->count, static_cast(parameterNumber)); + appRecord.setConciseType(valueType); + appRecord.dataPtr = parameterValuePtr; + appRecord.octetLength = bufferLength; + appRecord.octetLengthPtr = strLen_or_IndPtr; + appRecord.indicatorPtr = strLen_or_IndPtr; + auto* ipd = static_cast(stmt->impParamDesc); + DescriptorRecord& impRecord = ipd->record(static_cast(parameterNumber)); + ipd->count = std::max(ipd->count, static_cast(parameterNumber)); + impRecord.setConciseType(parameterType); + impRecord.length = columnSize; + impRecord.precision = static_cast( + std::min(columnSize, std::numeric_limits::max())); + impRecord.scale = decimalDigits; + impRecord.parameterType = inputOutputType; + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLCancel(SQLHSTMT statementHandle) { + if (!statementHandle) { + logMessage(nullptr, "SQLCancel: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + stmt->needsParameterData = false; + stmt->nextDataSet = 0; + stmt->activeDataSet = std::numeric_limits::max(); + stmt->nextDataParameter = 0; + stmt->activeDataParameter = std::numeric_limits::max(); + for (auto& binding : stmt->parameterBindings) { + binding.streamedData.clear(); + binding.streamedNull.clear(); + } + // Other operations are synchronous. Once control returns to the application + // there is no active operation to cancel. + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLCancelHandle(SQLSMALLINT handleType, SQLHANDLE inputHandle) { + if (!inputHandle) { + logMessage(nullptr, "SQLCancelHandle: Invalid handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + if (handleType == SQL_HANDLE_STMT) + return SQLCancel(static_cast(inputHandle)); + if (handleType == SQL_HANDLE_DBC) { + static_cast(inputHandle)->clearDiagnostics(); + // All connection operations are synchronous, so there is no pending work. + return SQL_SUCCESS; + } + logMessage("SQLCancelHandle: Invalid handle type"); + return SQL_INVALID_HANDLE; +} + +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { + if (!statementHandle) { + logMessage(nullptr, "SQLCloseCursor: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt->getConnection(); + stmt->clearDiagnostics(); + if (!stmt->resultSetPtr) { + stmt->addDiagnostic("24000", "Invalid cursor state"); + return SQL_ERROR; + } + stmt->ClearResultSet(); + stmt->curRow = -1; + stmt->rowsReturned = 0; + stmt->lastGetDataRow = -1; + stmt->lastGetDataCol = 0; + stmt->getDataOffsets.clear(); + logMessage(cnct, "SQLCloseCursor: Cursor closed", LOG_LEVEL_DEBUG); + return SQL_SUCCESS; +} + +// This function obtains the column name for the specified column number in the result set. +SQLRETURN getColName(StatementHandle* stmt, const SQLUSMALLINT columnNumber, + const SQLPOINTER stringBuffer, const SQLSMALLINT bufferLength, + SQLSMALLINT* stringLength) { + ConnectionHandle* cnct = stmt->getConnection(); + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + logMessage(cnct, "Entering getColName", LOG_LEVEL_TRACE); + + // Use ODBCResultSet to get column names (columnNumber is 1-based, getColumnName uses 0-based) + std::string columnExpression = stmt->resultSetPtr->getColumnName(columnNumber - 1); + + // Check if a valid column name is obtained + if (columnExpression.empty() && columnNumber > stmt->resultSetPtr->getNumColumns()) { + std::string errorMsg = "Requested column " + std::to_string(columnNumber) + " but only " + + std::to_string(stmt->resultSetPtr->getNumColumns()) + + " columns available"; + logMessage(cnct, errorMsg, LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + + SQLSMALLINT actualLength = static_cast(std::min( + columnExpression.size(), static_cast(std::numeric_limits::max()))); + // Set StringLength to the length of the column name + if (stringLength != nullptr) { + *stringLength = actualLength; + logMessage(cnct, "Set string length to " + std::to_string(*stringLength), LOG_LEVEL_DEBUG); + } + + // Copy columnExpression to stringBuffer + if (stringBuffer != nullptr) { + SQLSMALLINT lengthToCopy = (bufferLength > 0) + ? std::min(static_cast(actualLength), + static_cast(bufferLength - 1)) + : 0; + if (lengthToCopy > 0) { + std::memcpy(stringBuffer, columnExpression.c_str(), lengthToCopy); + } + + if (bufferLength > 0) { + static_cast(stringBuffer)[lengthToCopy] = '\0'; + } + + if (static_cast(lengthToCopy) < columnExpression.size()) { + std::string truncatedString = + lengthToCopy == 0 ? "[Empty string]" : columnExpression.substr(0, lengthToCopy); + logMessage(cnct, + "Returning Column name truncated: " + truncatedString + " (original: '" + + columnExpression + "')", + LOG_LEVEL_WARN); + stmt->addDiagnostic("01004", "String data, right truncated"); + return SQL_SUCCESS_WITH_INFO; + } else { + logMessage(cnct, "Returning column name: " + std::string(static_cast(stringBuffer)), + LOG_LEVEL_DEBUG); + } + } + + logMessage(cnct, "Exiting getColName", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +// Map 10 IoTDB data types to ODBC data types, output result via numericAttribute. +void colTypeMapping(ConnectionHandle* cnct, std::string iotdbDataType, + const SQLUSMALLINT columnNumber, SQLSMALLINT* numericAttribute) { + static const std::unordered_map typeMap = { + {"INT16", SQL_SMALLINT}, {"BOOLEAN", SQL_BIT}, {"INT32", SQL_INTEGER}, + {"INT64", SQL_BIGINT}, {"FLOAT", SQL_REAL}, {"DOUBLE", SQL_DOUBLE}, + {"TEXT", SQL_WLONGVARCHAR}, {"STRING", SQL_WVARCHAR}, {"BLOB", SQL_LONGVARBINARY}, + {"TIMESTAMP", SQL_BIGINT}, {"DATE", SQL_TYPE_DATE}}; + + auto it = typeMap.find(iotdbDataType); + if (it != typeMap.end()) { + *numericAttribute = it->second; + static const std::unordered_map typeNameMap = { + {SQL_BIT, "SQL_BIT"}, + {SQL_INTEGER, "SQL_INTEGER"}, + {SQL_BIGINT, "SQL_BIGINT"}, + {SQL_REAL, "SQL_REAL"}, + {SQL_DOUBLE, "SQL_DOUBLE"}, + {SQL_LONGVARCHAR, "SQL_LONGVARCHAR"}, + {SQL_VARCHAR, "SQL_VARCHAR"}, + {SQL_LONGVARBINARY, "SQL_LONGVARBINARY"}, + {SQL_TIMESTAMP, "SQL_TIMESTAMP"}, + {SQL_DATE, "SQL_DATE"}}; + + if (isLogLevelEnabled(cnct, LOG_LEVEL_DEBUG)) { + std::string typeName = typeNameMap.count(*numericAttribute) + ? typeNameMap.at(*numericAttribute) + : "UNKNOWN_TYPE_CODE"; + + std::string logMsg = "colTypeMapping: Column " + std::to_string(columnNumber) + + ": IoTDB type '" + iotdbDataType + "' mapped to ODBC type '" + typeName + + "'"; + logMessage(cnct, logMsg, LOG_LEVEL_DEBUG); + } + } else { + *numericAttribute = SQL_UNKNOWN_TYPE; + + if (isLogLevelEnabled(cnct, LOG_LEVEL_DEBUG)) { + std::string logMsg = "colTypeMapping: Column " + std::to_string(columnNumber) + + ": Unknown IoTDB type '" + iotdbDataType + + "' mapped to SQL_UNKNOWN_TYPE"; + logMessage(cnct, logMsg, LOG_LEVEL_DEBUG); + } + } +} + +// Abstract function to get column type based on column number +SQLRETURN getColType(StatementHandle* stmt, const SQLUSMALLINT columnNumber, + SQLSMALLINT* numericAttribute) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "getColType: Entering", LOG_LEVEL_TRACE); + + if (!numericAttribute) { + logMessage(cnct, "getColType: numericAttribute is null", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + if (!stmt->resultSetPtr->getIsMetaData() || stmt->getConnection()->isTableModel || + !stmt->resultSetPtr->getColumnType(columnNumber - 1).empty()) { + logMessage(cnct, "getColType: Using column types from ODBCResultSet for type mapping", + LOG_LEVEL_DEBUG); + + // Table model: get type directly from ODBCResultSet column types + const std::string iotdbDataType = stmt->resultSetPtr->getColumnType(columnNumber - 1); + if (iotdbDataType.empty()) { + logMessage(cnct, + "getColType: Failed to get data type for column " + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + colTypeMapping(cnct, iotdbDataType, columnNumber, numericAttribute); + + } else { + // Metadata mode: determine type by examining actual data from ODBCResultSet + logMessage(cnct, "getColType: Metadata mode - determining type from data content", + LOG_LEVEL_DEBUG); + + bool foundFlag = false; + int numRows = stmt->resultSetPtr->getNumRows(); + for (int curRow = 0; curRow < numRows; curRow++) { + const ODBCField& value = stmt->resultSetPtr->getValue(curRow, columnNumber - 1); + if (value.isNull()) { + continue; + } + + switch (value.getDataType()) { + case TSDataType::BOOLEAN: + *numericAttribute = SQL_BIT; + break; + case TSDataType::INT32: + *numericAttribute = SQL_INTEGER; + break; + case TSDataType::INT64: + *numericAttribute = SQL_BIGINT; + break; + case TSDataType::FLOAT: + *numericAttribute = SQL_REAL; + break; + case TSDataType::DOUBLE: + *numericAttribute = SQL_DOUBLE; + break; + case TSDataType::TEXT: + *numericAttribute = SQL_WLONGVARCHAR; + break; + case TSDataType::STRING: + *numericAttribute = SQL_WVARCHAR; + break; + case TSDataType::DATE: + *numericAttribute = SQL_WVARCHAR; + break; + case TSDataType::BLOB: + *numericAttribute = SQL_LONGVARBINARY; + break; + case TSDataType::TIMESTAMP: + *numericAttribute = SQL_BIGINT; + break; + default: + *numericAttribute = SQL_UNKNOWN_TYPE; + break; + } + foundFlag = true; + } + + if (!foundFlag) { + // All rows are empty/null + *numericAttribute = SQL_UNKNOWN_TYPE; // Default to UNKNOWN_TYPE for null columns + logMessage(cnct, + "getColType: All values empty for column " + std::to_string(columnNumber) + + ", defaulting to SQL_VARCHAR", + LOG_LEVEL_DEBUG); + } + } + + logMessage(cnct, "getColType: Exiting", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN getColumnSize(SQLSMALLINT odbcType, SQLULEN* columnSize) { + /* + * Map an ODBC SQL type to its “column size” (display width in characters, + * not bytes) according to + * https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size + */ + if (columnSize == nullptr) { + return SQL_ERROR; + } + + switch (odbcType) { + case SQL_CHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + *columnSize = 2147483647; // IoTDB places no length limit on strings + break; + + case SQL_BIT: + *columnSize = 1; + break; + case SQL_TINYINT: + *columnSize = 3; + break; + case SQL_SMALLINT: + *columnSize = 5; + break; + case SQL_INTEGER: + *columnSize = 10; + break; + case SQL_BIGINT: + *columnSize = 19; + break; + case SQL_REAL: + *columnSize = 7; + break; + case SQL_FLOAT: + case SQL_DOUBLE: + *columnSize = 15; + break; + + case SQL_LONGVARBINARY: + *columnSize = 2147483647; + break; + + case SQL_TYPE_DATE: + case SQL_DATE: + *columnSize = 10; // yyyy-mm-dd + break; + case SQL_TYPE_TIMESTAMP: + case SQL_TIMESTAMP: + *columnSize = 23; // yyyy-mm-dd hh:mm:ss.nnn + break; + + case SQL_DECIMAL: + case SQL_NUMERIC: + // format: [sign][integer digits].[fractional digits] + // assume max precision 38, scale 10 + *columnSize = 40; // 38 digits + decimal point + optional sign + break; + + case SQL_TIME: + *columnSize = 8; // hh:mm:ss + break; + + case SQL_GUID: + *columnSize = 36; // canonical form: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + case SQL_UNKNOWN_TYPE: + return SQL_ERROR; + + default: + return SQL_ERROR; + } + + return SQL_SUCCESS; +} + +SQLRETURN getNullable(StatementHandle* stmt, const SQLUSMALLINT columnNumber, + SQLSMALLINT* nullable) { + // Retrieve the connection handle for logging purposes + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + // Validate input parameters + if (nullable == nullptr) { + logMessage(cnct, "getNullable: Output pointer is null", LOG_LEVEL_ERROR); + if (stmt) + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + if (stmt == nullptr) { + logMessage(cnct, "getNullable: Statement handle is null", LOG_LEVEL_ERROR); + return SQL_ERROR; + } + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "getNullable: Result set is null or empty", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "No query result available"); + return SQL_ERROR; + } + + // Log debug information + logMessage(cnct, "getNullable: Checking nullability for column " + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + + *nullable = SQL_NULLABLE_UNKNOWN; + + logMessage(cnct, "getNullable: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN getColTypeName(StatementHandle* stmt, const SQLUSMALLINT columnNumber, + const SQLPOINTER stringBuffer, const SQLSMALLINT bufferLength, + SQLSMALLINT* stringLength) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "getColTypeName: Entering", LOG_LEVEL_TRACE); + + // Declared catalog types remain available for empty and all-NULL results. + const auto declaredType = stmt->resultSetPtr->getColumnType(columnNumber - 1); + if (!declaredType.empty()) + return setString(declaredType, stringBuffer, bufferLength, stringLength); + + if (!stringBuffer) { + logMessage(cnct, "getColTypeName: stringBuffer is null", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + if (!stmt->resultSetPtr->getIsMetaData() || stmt->getConnection()->isTableModel) { + logMessage(cnct, "getColTypeName: Using column types from ODBCResultSet for type name mapping", + LOG_LEVEL_DEBUG); + + if (stmt->getConnection()->isTableModel) { + // Table model: get type name directly from ODBCResultSet column types + const std::string iotdbDataType = stmt->resultSetPtr->getColumnType(columnNumber - 1); + if (iotdbDataType.empty()) { + logMessage(cnct, + "getColTypeName: Failed to get data type for column " + + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + SQLRETURN result = setString(iotdbDataType, stringBuffer, bufferLength, stringLength); + if (result != SQL_SUCCESS) { + logMessage( + cnct, "getColTypeName: Failed to set string for column " + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + return result; + } + logMessage(cnct, + "getColTypeName: " + iotdbDataType + " returned for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + } else { + // Tree model: not metadata, determine type name from column types + if (columnNumber == 1) { + SQLRETURN result = setString("TIMESTAMP", stringBuffer, bufferLength, stringLength); + if (result != SQL_SUCCESS) { + logMessage(cnct, + "getColTypeName: Failed to set string for column " + + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + return result; + } + logMessage(cnct, + "getColTypeName: TIMESTAMP returned for column " + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + } else { + const std::string iotdbDataType = stmt->resultSetPtr->getColumnType(columnNumber - 1); + if (iotdbDataType.empty()) { + logMessage(cnct, + "getColTypeName: Failed to get data type for column " + + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + SQLRETURN result = setString(iotdbDataType, stringBuffer, bufferLength, stringLength); + if (result != SQL_SUCCESS) { + logMessage(cnct, + "getColTypeName: Failed to set string for column " + + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + return result; + } + logMessage(cnct, + "getColTypeName: " + iotdbDataType + " returned for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + } + } + } else { + // Metadata mode: determine type name by examining actual data from ODBCResultSet + logMessage(cnct, "getColTypeName: Metadata mode - determining type name from data content", + LOG_LEVEL_DEBUG); + + bool foundFlag = false; + int numRows = stmt->resultSetPtr->getNumRows(); + for (int curRow = 0; curRow < numRows; curRow++) { + const ODBCField& value = stmt->resultSetPtr->getValue(curRow, columnNumber - 1); + if (value.isNull()) { + continue; + } + SQLRETURN result; + switch (value.getDataType()) { + case TSDataType::BOOLEAN: + result = setString("BOOLEAN", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::FLOAT: + result = setString("FLOAT", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::DOUBLE: + result = setString("DOUBLE", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::STRING: + result = setString("STRING", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::INT32: + result = setString("INT32", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::INT64: + result = setString("INT64", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::TIMESTAMP: + result = setString("TIMESTAMP", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::DATE: + result = setString("DATE", stringBuffer, bufferLength, stringLength); + break; + case TSDataType::BLOB: + result = setString("BLOB", stringBuffer, bufferLength, stringLength); + break; + default: + result = setString("", stringBuffer, bufferLength, stringLength); + break; + } + foundFlag = true; + } + + if (!foundFlag) { + // All rows are empty/null + SQLRETURN result = setString("", stringBuffer, bufferLength, stringLength); + if (result != SQL_SUCCESS) { + logMessage(cnct, + "getColTypeName: Failed to set empty string for null column " + + std::to_string(columnNumber), + LOG_LEVEL_ERROR); + return result; + } + logMessage(cnct, + "getColTypeName: Empty string returned for null column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + } + } + + logMessage(cnct, "getColTypeName: Exiting", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN getPrecision(SQLSMALLINT odbcType, SQLULEN* precision) { + if (precision == nullptr) { + logMessage("getPrecision: precision pointer is nullptr"); + return SQL_SUCCESS; + } + + switch (odbcType) { + case SQL_BIT: // BOOLEAN + *precision = 1; // Boolean value fixed to 1 bit + break; + + case SQL_INTEGER: // INT32 + *precision = 10; // 32-bit integer maximum 10 digits (2,147,483,647) + break; + + case SQL_BIGINT: // INT64 + *precision = 19; // 64-bit integer maximum 19 digits (9,223,372,036,854,775,807) + break; + + case SQL_REAL: // FLOAT + *precision = 23; // IEEE 754 single precision has 23 bits of binary precision + break; + + case SQL_DOUBLE: // DOUBLE + *precision = 52; // IEEE 754 double precision has 52 bits of binary precision + break; + + case SQL_WCHAR: + case SQL_WVARCHAR: + + case SQL_WLONGVARCHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_LONGVARBINARY: + case SQL_TYPE_TIMESTAMP: + case SQL_TIMESTAMP: + case SQL_TYPE_DATE: + case SQL_DATE: + *precision = 0; + break; + + default: + *precision = 0; // Unknown type defaults to 0 + break; + } + return SQL_SUCCESS; +} + +SQLRETURN getPrecRadix(SQLSMALLINT odbcType, SQLULEN* radix) { + if (radix == nullptr) { + logMessage("getPrecRadix: precision pointer is nullptr"); + return SQL_SUCCESS; + } + + switch (odbcType) { + case SQL_REAL: + case SQL_DOUBLE: + case SQL_BIT: // BOOLEAN + *radix = 2; // Boolean value fixed to 1 bit + break; + case SQL_INTEGER: + case SQL_BIGINT: // INT64 + *radix = 10; // 64-bit integer maximum 19 digits (9,223,372,036,854,775,807) + break; + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_LONGVARBINARY: + case SQL_TYPE_TIMESTAMP: + case SQL_TIMESTAMP: + case SQL_TYPE_DATE: + case SQL_DATE: + *radix = 0; + break; + + default: + *radix = 0; // Unknown type defaults to 0 + break; + } + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLColAttribute(const SQLHSTMT statementHandle, const SQLUSMALLINT columnNumber, + const SQLUSMALLINT fieldIdentifier, + const SQLPOINTER characterAttribute, + const SQLSMALLINT bufferLength, SQLSMALLINT* stringLength, + SQLLEN* numericAttribute) { + // Get connection handle for logging + StatementHandle* stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLColAttribute: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLColAttribute parameters: " + << "StatementHandle = " << handleToString(statementHandle) + << ", ColumnNumber = " << columnNumber << ", FieldIdentifier = " << fieldIdentifier + << ", CharacterAttribute = " << characterAttribute + << ", BufferLength = " << bufferLength; + + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (statementHandle == nullptr) { + logMessage(cnct, "SQLColAttribute: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + stmt->clearDiagnostics(); + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "SQLColAttribute: Missing result set", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "No query result available"); + return SQL_ERROR; + } + + // Validate column number + if (columnNumber < 1 || columnNumber > stmt->resultSetPtr->getNumColumns()) { + logMessage(cnct, + "SQLColAttribute: Invalid column number: " + std::to_string(columnNumber) + + ", max columns: " + std::to_string(stmt->resultSetPtr->getNumColumns()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + + // Initialize output parameters + if (characterAttribute) { + memset(characterAttribute, 0, bufferLength); + } + if (stringLength) { + *stringLength = 0; + } + if (numericAttribute) { + *numericAttribute = 0; + } + + // Handle based on fieldIdentifier + SQLRETURN sqlreturn; + SQLSMALLINT columnType; + SQLLEN octetLength; + SQLLEN scale; + switch (fieldIdentifier) { + case SQL_DESC_LABEL: + logMessage(cnct, "SQLColAttribute: Processing SQL_DESC_LABEL", LOG_LEVEL_DEBUG); + return getColName(stmt, columnNumber, characterAttribute, bufferLength, stringLength); + break; + + case SQL_DESC_COUNT: + logMessage(cnct, "SQLColAttribute: Processing SQL_DESC_COUNT", LOG_LEVEL_DEBUG); + { + int columnCount = stmt->resultSetPtr->getNumColumns(); + + if (numericAttribute) { + *numericAttribute = static_cast(columnCount); + } + logMessage(cnct, "SQLColAttribute: Column count = " + std::to_string(columnCount), + LOG_LEVEL_DEBUG); + } + break; + + case SQL_DESC_NAME: + logMessage(cnct, "SQLColAttribute: Processing SQL_DESC_NAME", LOG_LEVEL_DEBUG); + sqlreturn = getColName(stmt, columnNumber, characterAttribute, bufferLength, stringLength); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column name", LOG_LEVEL_ERROR); + return sqlreturn; + } + break; + + case SQL_COLUMN_TYPE: + case SQL_DESC_TYPE: + logMessage(cnct, "SQLColAttribute: Processing SQL_COLUMN_TYPE/SQL_DESC_TYPE", LOG_LEVEL_DEBUG); + if (!numericAttribute) { + logMessage(cnct, "SQLColAttribute: numericAttribute is null for type request", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type", LOG_LEVEL_ERROR); + return sqlreturn; + } + *numericAttribute = columnType; + break; + + case SQL_COLUMN_DISPLAY_SIZE: + logMessage(cnct, + "SQLColAttribute: Processing SQL_COLUMN_DISPLAY_SIZE for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + { + // First get the column type + columnType = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for display size", + LOG_LEVEL_ERROR); + return sqlreturn; + } + + SQLLEN displaySize = 0; + switch (columnType) { + case SQL_CHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + // For string types, use the actual length if available, otherwise default + displaySize = 255; // Default for string types + break; + + case SQL_DECIMAL: + case SQL_NUMERIC: + displaySize = 20; // Reasonable default for decimal types + break; + + case SQL_INTEGER: + displaySize = 11; // Max display size for a signed 32-bit integer (-2147483648) + break; + + case SQL_SMALLINT: + displaySize = 6; // Max display size for a signed 16-bit integer (-32768) + break; + + case SQL_BIGINT: + displaySize = 20; // Max display size for a signed 64-bit integer + break; + + case SQL_FLOAT: + case SQL_DOUBLE: + case SQL_REAL: + displaySize = 24; // Typical display size for floating-point numbers + break; + + case SQL_BIT: + displaySize = 1; // 0 or 1 + break; + + case SQL_TYPE_DATE: + displaySize = 10; // YYYY-MM-DD + break; + + case SQL_TYPE_TIME: + displaySize = 8; // HH:MM:SS + break; + + case SQL_TYPE_TIMESTAMP: + displaySize = 23; // YYYY-MM-DD HH:MM:SS.sss + break; + + default: + displaySize = 0; // Unknown type + logMessage(cnct, + "SQLColAttribute: Unknown column type for display size: " + + std::to_string(columnType), + LOG_LEVEL_WARN); + break; + } + + if (numericAttribute) { + *numericAttribute = displaySize; + logMessage(cnct, "SQLColAttribute: Display size set to " + std::to_string(displaySize), + LOG_LEVEL_DEBUG); + } + + if (characterAttribute && bufferLength > 0) { + const std::string displaySizeStr = std::to_string(displaySize); + const size_t copyLen = + std::min(static_cast(bufferLength - 1), displaySizeStr.length()); + std::memcpy(characterAttribute, displaySizeStr.c_str(), copyLen); + static_cast(characterAttribute)[copyLen] = '\0'; + + if (stringLength) { + *stringLength = static_cast(displaySizeStr.length()); + } + } + } + break; + + case SQL_COLUMN_UNSIGNED: { + // FieldIdentifier == 8 + logMessage(cnct, + "SQLColAttribute: Processing SQL_COLUMN_UNSIGNED for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + + // Default: Column is signed unless explicitly determined otherwise + SQLLEN isUnsigned = SQL_FALSE; + + columnType = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for unsigned check", + LOG_LEVEL_ERROR); + return sqlreturn; + } + + switch (columnType) { + case SQL_BIT: // BIT type is effectively unsigned + isUnsigned = SQL_TRUE; + break; + + default: + isUnsigned = SQL_FALSE; // Most types are signed by default + break; + } + + if (numericAttribute) { + *numericAttribute = isUnsigned; + logMessage(cnct, "SQLColAttribute: Unsigned attribute set to " + std::to_string(isUnsigned), + LOG_LEVEL_DEBUG); + } + + if (characterAttribute && bufferLength > 0) { + // Return a textual representation + const char* unsignedStr = (isUnsigned == SQL_TRUE) ? "TRUE" : "FALSE"; + const size_t copyLen = + std::min(static_cast(bufferLength - 1), std::strlen(unsignedStr)); + std::memcpy(characterAttribute, unsignedStr, copyLen); + static_cast(characterAttribute)[copyLen] = '\0'; + + if (stringLength) { + *stringLength = static_cast(std::strlen(unsignedStr)); + } + } + break; + } + + case SQL_DESC_OCTET_LENGTH: // Byte length + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_OCTET_LENGTH for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + + // Get the column type to determine octet length + columnType = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for octet length", + LOG_LEVEL_ERROR); + return sqlreturn; + } + + octetLength = 0; + switch (columnType) { + case SQL_BIT: + octetLength = 1; + break; + case SQL_TINYINT: + octetLength = 1; + break; + case SQL_SMALLINT: + octetLength = 2; + break; + case SQL_INTEGER: + octetLength = 4; + break; + case SQL_BIGINT: + octetLength = 8; + break; + case SQL_REAL: + octetLength = 4; + break; + case SQL_FLOAT: + case SQL_DOUBLE: + octetLength = 8; + break; + case SQL_TYPE_DATE: + octetLength = 6; // Size of DATE_STRUCT + break; + case SQL_TYPE_TIME: + octetLength = 6; // Size of TIME_STRUCT + break; + case SQL_TYPE_TIMESTAMP: + octetLength = 16; // Size of TIMESTAMP_STRUCT + break; + default: + // For string types, use a reasonable default or calculate based on data + octetLength = 255; + break; + } + + if (numericAttribute) { + *numericAttribute = octetLength; + } + break; + + case SQL_COLUMN_PRECISION: + case SQL_DESC_PRECISION: // Precision + { + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_PRECISION for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + columnType = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for precision", LOG_LEVEL_ERROR); + return sqlreturn; + } + SQLULEN value; + sqlreturn = getPrecision(columnType, &value); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get precision", LOG_LEVEL_ERROR); + return sqlreturn; + } + if (numericAttribute) { + *numericAttribute = value; + } + break; + } + + case SQL_COLUMN_SCALE: + case SQL_DESC_SCALE: // Scale + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_SCALE for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + + // Get the column type to determine scale + columnType = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &columnType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for scale", LOG_LEVEL_ERROR); + return sqlreturn; + } + + scale = 0; + switch (columnType) { + case SQL_DECIMAL: + case SQL_NUMERIC: + // For decimal types, you might need to get the actual scale from metadata + scale = 0; // Default scale for decimal types + break; + default: + // For other types, scale is not applicable + scale = 0; + break; + } + + if (numericAttribute) { + *numericAttribute = scale; + } + break; + + case SQL_DESC_UPDATABLE: + if (numericAttribute) + *numericAttribute = SQL_ATTR_READONLY; + break; + case SQL_DESC_AUTO_UNIQUE_VALUE: + if (numericAttribute) + *numericAttribute = SQL_FALSE; + break; + case SQL_DESC_CASE_SENSITIVE: + if (numericAttribute) + *numericAttribute = SQL_TRUE; + break; + case SQL_DESC_SEARCHABLE: + if (numericAttribute) + *numericAttribute = SQL_PRED_SEARCHABLE; + break; + case SQL_DESC_TYPE_NAME: + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_TYPE_NAME for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + sqlreturn = getColTypeName(stmt, columnNumber, characterAttribute, bufferLength, stringLength); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type name", LOG_LEVEL_ERROR); + return sqlreturn; + } + break; + + case SQL_DESC_NULLABLE: { + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_NULLABLE for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + SQLSMALLINT value = 0; + sqlreturn = getNullable(stmt, columnNumber, &value); + if (numericAttribute) { + *numericAttribute = value; + } + break; + } + + case SQL_DESC_LENGTH: { + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_LENGTH for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + SQLSMALLINT type = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &type); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for length", LOG_LEVEL_ERROR); + return sqlreturn; + } + SQLULEN value; + sqlreturn = getColumnSize(type, &value); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get length", LOG_LEVEL_ERROR); + return sqlreturn; + } + if (numericAttribute) { + *numericAttribute = value; + } + break; + } + + case SQL_DESC_NUM_PREC_RADIX: { + logMessage(cnct, + "SQLColAttribute: Processing SQL_DESC_NUM_PREC_RADIX for column " + + std::to_string(columnNumber), + LOG_LEVEL_DEBUG); + SQLSMALLINT type = SQL_UNKNOWN_TYPE; + sqlreturn = getColType(stmt, columnNumber, &type); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for precision radix", + LOG_LEVEL_ERROR); + return sqlreturn; + } + SQLULEN value; + sqlreturn = getPrecRadix(type, &value); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLColAttribute: Failed to get column type for precision radix", + LOG_LEVEL_ERROR); + return sqlreturn; + } + if (numericAttribute) { + *numericAttribute = value; + } + break; + } + + default: + logMessage(cnct, + "SQLColAttribute: Unsupported field identifier: " + std::to_string(fieldIdentifier), + LOG_LEVEL_WARN); + stmt->addDiagnostic("HY000", "Unsupported field identifier"); + return SQL_ERROR; + } + + logMessage(cnct, "SQLColAttribute: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLConnect(SQLHDBC connectionHandle, SQLCHAR* serverName, SQLSMALLINT nameLength1, + SQLCHAR* userName, SQLSMALLINT nameLength2, SQLCHAR* password, + SQLSMALLINT nameLength3) { + if (!connectionHandle) { + logMessage(nullptr, "SQLConnect: Invalid connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto cnct = static_cast(connectionHandle); + cnct->clearDiagnostics(); + logMessage(cnct, "SQLConnect: Entering", LOG_LEVEL_TRACE); + + if ((nameLength1 < 0 && nameLength1 != SQL_NTS) || (nameLength2 < 0 && nameLength2 != SQL_NTS) || + (nameLength3 < 0 && nameLength3 != SQL_NTS)) { + cnct->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + try { + std::string dsnName; + if (serverName) { + if (nameLength1 == SQL_NTS) + dsnName = reinterpret_cast(serverName); + else if (nameLength1 > 0) + dsnName = std::string(reinterpret_cast(serverName), nameLength1); + } + logMessage(cnct, "SQLConnect: DSN='" + dsnName + "'", LOG_LEVEL_INFO); + + if (dsnName.empty()) { + logMessage(cnct, "SQLConnect: DSN name is empty", LOG_LEVEL_ERROR); + cnct->addDiagnostic("IM002", "Data source name not found and no default driver specified"); + return SQL_ERROR; + } + + cnct->LoadDsnFromOdbcIni(dsnName); + + if (userName) { + std::string uid; + if (nameLength2 == SQL_NTS) + uid = reinterpret_cast(userName); + else if (nameLength2 > 0) + uid = std::string(reinterpret_cast(userName), nameLength2); + if (!uid.empty()) { + cnct->userName = uid; + logMessage(cnct, "SQLConnect: UID overridden from parameter: " + uid, LOG_LEVEL_DEBUG); + } + } + + if (password) { + std::string pwd; + if (nameLength3 == SQL_NTS) + pwd = reinterpret_cast(password); + else if (nameLength3 > 0) + pwd = std::string(reinterpret_cast(password), nameLength3); + if (!pwd.empty()) { + cnct->password = pwd; + logMessage(cnct, "SQLConnect: PWD overridden from parameter", LOG_LEVEL_DEBUG); + } + } + + if (cnct->isTableModel && cnct->database.empty()) { + logMessage(cnct, "SQLConnect: Database not set, using 'information_schema' as default", + LOG_LEVEL_INFO); + cnct->database = "information_schema"; + } + + if (isLogLevelEnabled(cnct, LOG_LEVEL_INFO)) { + std::ostringstream oss; + oss << "SQLConnect: Connecting with Server=" << cnct->serverHostName + << ", Port=" << cnct->serverPort << ", UID=" << cnct->userName + << ", Database=" << cnct->database + << ", TableModel=" << (cnct->isTableModel ? "true" : "false"); + logMessage(cnct, oss.str(), LOG_LEVEL_INFO); + } + + SQLRETURN returnCode; + if (cnct->useRestful) { + logMessage(cnct, "SQLConnect: Connecting using RESTful API", LOG_LEVEL_DEBUG); + returnCode = IoTDB_DriverConnect_Rest(cnct); + } else { + logMessage(cnct, "SQLConnect: Connecting using Session API", LOG_LEVEL_DEBUG); + returnCode = IoTDB_DriverConnect_Session(cnct); + } + + logMessage(cnct, "SQLConnect: Exiting with return code: " + std::to_string(returnCode), + LOG_LEVEL_TRACE); + return returnCode; + } catch (const std::exception& error) { + cnct->addDiagnostic("08001", error.what()); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLCopyDesc(SQLHDESC sourceDescHandle, SQLHDESC targetDescHandle) { + if (!sourceDescHandle || !targetDescHandle) + return SQL_INVALID_HANDLE; + auto* source = static_cast(sourceDescHandle); + auto* target = static_cast(targetDescHandle); + if (source->getHandleType() != SQL_HANDLE_DESC || target->getHandleType() != SQL_HANDLE_DESC) + return SQL_INVALID_HANDLE; + target->clearDiagnostics(); + if (target->role == DescriptorRole::IMPLEMENTATION_ROW) { + target->addDiagnostic("HY016", "Cannot modify an implementation row descriptor"); + return SQL_ERROR; + } + if (source->connection != target->connection) { + target->addDiagnostic("HY024", "Descriptor handles belong to different connections"); + return SQL_ERROR; + } + if (source->role == DescriptorRole::IMPLEMENTATION_ROW && source->owner) + PopulateImplementationRowDescriptor(source->owner); + target->arraySize = source->arraySize; + target->arrayStatusPtr = source->arrayStatusPtr; + target->bindOffsetPtr = source->bindOffsetPtr; + target->bindType = source->bindType; + target->count = source->count; + target->rowsProcessedPtr = source->rowsProcessedPtr; + target->records = source->records; + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLDataSources(SQLHENV environmentHandle, SQLUSMALLINT direction, + SQLCHAR* serverName, SQLSMALLINT bufferLength1, + SQLSMALLINT* nameLength1Ptr, SQLCHAR* description, + SQLSMALLINT bufferLength2, SQLSMALLINT* nameLength2Ptr) { + if (!environmentHandle) + return SQL_INVALID_HANDLE; + auto* env = static_cast(environmentHandle); + env->clearDiagnostics(); + if (direction != SQL_FETCH_FIRST && direction != SQL_FETCH_NEXT && + direction != SQL_FETCH_FIRST_USER && direction != SQL_FETCH_FIRST_SYSTEM) { + env->addDiagnostic("HY103", "Invalid retrieval code"); + return SQL_ERROR; + } + if (bufferLength1 < 0 || bufferLength2 < 0) { + env->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (nameLength1Ptr) + *nameLength1Ptr = 0; + if (nameLength2Ptr) + *nameLength2Ptr = 0; + if (serverName && bufferLength1 > 0) + serverName[0] = '\0'; + if (description && bufferLength2 > 0) + description[0] = '\0'; + // Driver Manager-owned DSN enumeration is not duplicated by the driver. + return SQL_NO_DATA; +} + +SQLRETURN SQL_API SQLDescribeCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLCHAR* columnName, SQLSMALLINT bufferLength, + SQLSMALLINT* nameLength, SQLSMALLINT* dataType, + SQLULEN* columnSize, SQLSMALLINT* decimalDigits, + SQLSMALLINT* nullable) { + // Most content copied from SQLColAttribute + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLDescribeCol: Entering", LOG_LEVEL_TRACE); + + // Prepare log message for the parameters + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLDescribeCol parameters: " + << "StatementHandle = " << handleToString(statementHandle) + << ", ColumnNumber = " << columnNumber + << ", ColumnName = " << static_cast(columnName) + << ", BufferLength = " << bufferLength << ", NameLength = " << nameLength + << ", DataType = " << dataType << ", ColumnSize = " << columnSize + << ", DecimalDigits = " << decimalDigits << ", Nullable = " << nullable; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (statementHandle == nullptr) { + logMessage(cnct, "SQLDescribeCol: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "SQLDescribeCol: Missing result set", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "No query result available"); + return SQL_ERROR; + } + + // Validate column number + if (columnNumber < 1 || columnNumber > stmt->resultSetPtr->getNumColumns()) { + logMessage(cnct, + "SQLDescribeCol: Invalid column number: " + std::to_string(columnNumber) + + ", max columns: " + std::to_string(stmt->resultSetPtr->getNumColumns()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("07009", "Invalid column number"); + return SQL_ERROR; + } + + stmt->clearDiagnostics(); + // Column name + SQLRETURN sqlreturn = getColName(stmt, columnNumber, columnName, bufferLength, nameLength); + if (!SQL_SUCCEEDED(sqlreturn)) { + logMessage(cnct, "SQLDescribeCol: Failed to get column name", LOG_LEVEL_ERROR); + return sqlreturn; + } + const SQLRETURN nameResult = sqlreturn; + + // Data type + SQLSMALLINT localType = SQL_UNKNOWN_TYPE; + if (dataType || columnSize) { + sqlreturn = getColType(stmt, columnNumber, &localType); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLDescribeCol: Failed to get column type", LOG_LEVEL_ERROR); + return sqlreturn; + } + if (dataType) + *dataType = localType; + } + + // Column size + if (columnSize) { + sqlreturn = getColumnSize(localType, columnSize); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLDescribeCol: Failed to get column size", LOG_LEVEL_ERROR); + return sqlreturn; + } + + logMessage(cnct, "SQLDescribeCol: Column size set to " + std::to_string(*columnSize), + LOG_LEVEL_DEBUG); + } + + if (decimalDigits) { + // This field is only applicable for fixed-point values. + // For timestamps with sub-second precision and SQL_DECIMAL/SQL_NUMERIC types, + // it should have a non-zero value; for other cases, it should be 0 or NULL. + *decimalDigits = 0; + } + + // Nullable + if (nullable) { + sqlreturn = getNullable(stmt, columnNumber, nullable); + if (sqlreturn != SQL_SUCCESS) { + logMessage(cnct, "SQLDescribeCol: Failed to get nullable attribute", LOG_LEVEL_ERROR); + return sqlreturn; + } + + const char* nullableStr = (*nullable == SQL_NULLABLE) ? "NULLABLE" + : (*nullable == SQL_NO_NULLS) ? "NOT NULL" + : "UNKNOWN"; + logMessage(cnct, "SQLDescribeCol: Nullable attribute set to " + std::string(nullableStr), + LOG_LEVEL_DEBUG); + } + + logMessage(cnct, "SQLDescribeCol: Exiting successfully", LOG_LEVEL_TRACE); + return nameResult; +} + +SQLRETURN SQL_API SQLDisconnect(SQLHDBC connectionHandle) { + if (!connectionHandle) { + logMessage(nullptr, "SQLDisconnect: Invalid connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto cnct = static_cast(connectionHandle); + try { + cnct->CloseSession(); + return SQL_SUCCESS; + } catch (const std::exception& e) { + cnct->addDiagnostic("08003", e.what()); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT completionType) { + if (!handle) { + logMessage(nullptr, "SQLEndTran: Invalid handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + auto* baseHandle = static_cast(handle); + + EnvironmentHandle* env = nullptr; + ConnectionHandle* cnct = nullptr; + if (handleType == SQL_HANDLE_ENV) { + if (baseHandle && baseHandle->getHandleType() != SQL_HANDLE_ENV) { + baseHandle->addDiagnostic("HY092", "Invalid handle type for SQLEndTran: expected ENV"); + return SQL_INVALID_HANDLE; + } + env = static_cast(handle); + } else if (handleType == SQL_HANDLE_DBC) { + if (baseHandle && baseHandle->getHandleType() != SQL_HANDLE_DBC) { + baseHandle->addDiagnostic("HY092", "Invalid handle type for SQLEndTran: expected DBC"); + return SQL_INVALID_HANDLE; + } + cnct = static_cast(handle); + env = cnct ? cnct->getEnvironment() : nullptr; + } + + if (cnct && isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + std::string handleTypeStr = "UNKNOWN"; + switch (handleType) { + case SQL_HANDLE_ENV: + handleTypeStr = "ENV"; + break; + case SQL_HANDLE_DBC: + handleTypeStr = "DBC"; + break; + case SQL_HANDLE_STMT: + handleTypeStr = "STMT"; + break; + default: + handleTypeStr = "UNKNOWN"; + break; + } + std::string completionTypeStr = "UNKNOWN"; + switch (completionType) { + case SQL_COMMIT: + completionTypeStr = "COMMIT"; + break; + case SQL_ROLLBACK: + completionTypeStr = "ROLLBACK"; + break; + } + logStream << "SQLEndTran parameters: " + << ", handleType = " << handleTypeStr << "(" << handleType << ")" + << ", handle = " << handleToString(handle) + << ", completionType = " << completionTypeStr << "(" << completionType << ")"; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // Determine which handle to attach diagnostics to. + // Prefer connection diagnostics (most callers use DBC/STMT); fall back to stmt/env when needed. + ODBCHandle* diagTarget = nullptr; + if (cnct) { + diagTarget = cnct; + } else if (env) { + diagTarget = env; + } else if (baseHandle) { + diagTarget = baseHandle; + } + + // Validate handleType + if (handleType != SQL_HANDLE_ENV && handleType != SQL_HANDLE_DBC) { + logMessage(cnct, "SQLEndTran: Invalid HandleType", LOG_LEVEL_ERROR); + if (diagTarget) { + diagTarget->addDiagnostic("HY092", "Invalid attribute/option identifier"); + } + return SQL_ERROR; + } + + // Validate completionType + if (completionType != SQL_COMMIT && completionType != SQL_ROLLBACK) { + logMessage(cnct, "SQLEndTran: Invalid CompletionType", LOG_LEVEL_ERROR); + if (diagTarget) { + diagTarget->addDiagnostic("HY012", "Invalid transaction operation code"); + } + return SQL_ERROR; + } + + // IoTDB reports SQL_TXN_CAPABLE = SQL_TC_NONE (no transactions). + if (completionType == SQL_COMMIT) { + logMessage(cnct, "SQLEndTran: COMMIT requested; IoTDB has no transactions, no-op", + LOG_LEVEL_TRACE); + return SQL_SUCCESS; + } + + logMessage(cnct, "SQLEndTran: ROLLBACK requested but IoTDB has no transactions", LOG_LEVEL_DEBUG); + if (diagTarget) { + diagTarget->addDiagnostic("HYC00", "IoTDB does not support transactions; rollback ignored"); + } + return SQL_ERROR; +} + +SQLRETURN SQLError(SQLHENV environmentHandle, // Input: Environment handle + SQLHDBC connectionHandle, // Input: Connection handle + SQLHSTMT statementHandle, // Input: Statement handle + SQLCHAR* sqlstate, // Output: SQLSTATE code + SQLINTEGER* nativeError, // Output: Native error code + SQLCHAR* messageText, // Output: Error message text + SQLSMALLINT bufferLength, // Input: Message text buffer length + SQLSMALLINT* textLength // Output: Actual length of message text +) { + // Determine the appropriate connection handle for logging + ConnectionHandle* cnct = nullptr; + if (statementHandle != SQL_NULL_HSTMT) { + const auto stmt = reinterpret_cast(statementHandle); + cnct = stmt->getConnection(); + } else if (connectionHandle != SQL_NULL_HDBC) { + cnct = reinterpret_cast(connectionHandle); + } + + logMessage(cnct, "SQLError: Entering", LOG_LEVEL_TRACE); + + // Log input parameters if TRACE level is enabled + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLError parameters: " + << "EnvironmentHandle = " << environmentHandle + << ", ConnectionHandle = " << connectionHandle + << ", StatementHandle = " << statementHandle + << ", SqlState = " << (sqlstate ? reinterpret_cast(sqlstate) : "nullptr") + << ", NativeError = " << (nativeError ? std::to_string(*nativeError) : "nullptr") + << ", MessageText = " + << (messageText ? reinterpret_cast(messageText) : "nullptr") + << ", BufferLength = " << bufferLength + << ", TextLength = " << (textLength ? std::to_string(*textLength) : "nullptr"); + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + SQLHANDLE handle; + SQLSMALLINT handle_type; + + // Determine handle type and handle + if (statementHandle != SQL_NULL_HSTMT) { + handle = statementHandle; + handle_type = SQL_HANDLE_STMT; + logMessage(cnct, "SQLError: Using statement handle", LOG_LEVEL_DEBUG); + } else if (connectionHandle != SQL_NULL_HDBC) { + handle = connectionHandle; + handle_type = SQL_HANDLE_DBC; + logMessage(cnct, "SQLError: Using connection handle", LOG_LEVEL_DEBUG); + } else if (environmentHandle != SQL_NULL_HENV) { + handle = environmentHandle; + handle_type = SQL_HANDLE_ENV; + logMessage(cnct, "SQLError: Using environment handle", LOG_LEVEL_DEBUG); + } else { + logMessage(cnct, "SQLError: No valid handle provided", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; // Invalid handle + } + + logMessage(cnct, "SQLError: Calling SQLGetDiagRec", LOG_LEVEL_DEBUG); + + // Call SQLGetDiagRec to retrieve error information + SQLRETURN retcode = SQLGetDiagRec(handle_type, handle, 1, sqlstate, nativeError, messageText, + bufferLength, textLength); + if (SQL_SUCCEEDED(retcode)) + static_cast(handle)->consumeFirstDiagnostic(); + + // Log the result of SQLGetDiagRec + if (retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO) { + std::stringstream resultStream; + resultStream << "SQLError: Retrieved error - SQLSTATE: " + << (sqlstate ? reinterpret_cast(sqlstate) : "null") + << ", NativeError: " << (nativeError ? std::to_string(*nativeError) : "null") + << ", Message: " + << (messageText ? reinterpret_cast(messageText) : "null"); + logMessage(cnct, resultStream.str(), LOG_LEVEL_DEBUG); + } else if (retcode == SQL_NO_DATA) { + logMessage(cnct, "SQLError: No error information available", LOG_LEVEL_DEBUG); + } else { + logMessage(cnct, "SQLError: SQLGetDiagRec failed", LOG_LEVEL_ERROR); + } + + // Return appropriate SQLError return value based on SQLGetDiagRec's return code + logMessage(cnct, "SQLError: Exiting", LOG_LEVEL_TRACE); + + switch (retcode) { + case SQL_SUCCESS: + case SQL_SUCCESS_WITH_INFO: + return SQL_SUCCESS; + case SQL_NO_DATA: + return SQL_NO_DATA; + default: + return SQL_ERROR; + } +} + +/** + * Safely converts SQLCHAR array to std::string with proper handling of unsigned char + * + * This function ensures that unsigned char values are properly converted to char + * without sign extension issues, making it safe for all character values (0-255). + * + * @param statementText Pointer to SQLCHAR array (unsigned char*) + * @param textLength Length of the text or SQL_NTS for null-terminated string + * @return std::string containing the converted text, or empty string on error + */ +std::string ConvertSQLCHARToString(SQLCHAR* statementText, SQLINTEGER textLength) { + if (statementText == nullptr) { + return std::string(); + } + + // Determine the actual length + size_t length = 0; + if (textLength == SQL_NTS) { + // Calculate length for null-terminated string + const SQLCHAR* ptr = statementText; + while (*ptr != '\0') { + ++length; + ++ptr; + } + } else if (textLength > 0) { + length = static_cast(textLength); + } else { + return std::string(); // Invalid length + } + + // Convert each character safely + std::string result; + result.reserve(length); + for (size_t i = 0; i < length; ++i) { + // Safe conversion: static_cast ensures proper value preservation + result.push_back(static_cast(statementText[i])); + } + + return result; +} + +/** + * If the SQL is a SELECT whose first column is exactly "Time" followed by a comma, + * removes that "Time," so IoTDB receives a SELECT without the Time column. + * (IoTDB tree model always returns a time column; PowerBI adds Time to the SELECT + * list, which the server does not accept.) Only the leading column name "Time" is + * matched; other columns or "Timestamp" etc. are left unchanged. + * @param sql Statement to modify in place; unchanged if pattern does not match. + */ +static void StripLeadingTimeColumnFromSelect(std::string& sql) { + size_t i = 0; + while (i < sql.size() && (sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\r' || sql[i] == '\n')) { + ++i; + } + if (i + 6 > sql.size()) + return; + const char* p = sql.c_str() + i; + if ((p[0] == 's' || p[0] == 'S') && (p[1] == 'e' || p[1] == 'E') && + (p[2] == 'l' || p[2] == 'L') && (p[3] == 'e' || p[3] == 'E') && + (p[4] == 'c' || p[4] == 'C') && (p[5] == 't' || p[5] == 'T')) { + i += 6; + } else { + return; + } + while (i < sql.size() && (sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\r' || sql[i] == '\n')) { + ++i; + } + if (i + 4 > sql.size()) + return; + p = sql.c_str() + i; + if ((p[0] != 't' && p[0] != 'T') || (p[1] != 'i' && p[1] != 'I') || + (p[2] != 'm' && p[2] != 'M') || (p[3] != 'e' && p[3] != 'E')) { + return; + } + size_t timeStart = i; + i += 4; + if (i < sql.size()) { + char c = sql[i]; + if (c != ',' && c != ' ' && c != '\t' && c != '\r' && c != '\n') + return; + } + while (i < sql.size() && (sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\r' || sql[i] == '\n')) { + ++i; + } + if (i >= sql.size() || sql[i] != ',') + return; + sql.erase(timeStart, i - timeStart + 1); +} + +// saveStatementText: Optional, defaults to true +SQLRETURN IoTDB_ExecDirect(StatementHandle* const stmt, const std::string& statementText, + bool saveStatementText) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "IoTDB_ExecDirect: Entering", LOG_LEVEL_TRACE); + + std::string sqlForServer = statementText; + if (!cnct->isTableModel) + StripLeadingTimeColumnFromSelect(sqlForServer); + if (saveStatementText) { + stmt->statementText = sqlForServer; + logMessage(cnct, "IoTDB_ExecDirect: Saved statement text", LOG_LEVEL_DEBUG); + } + + // Choose API based on connection settings + if (cnct->useRestful) { + logMessage(cnct, "IoTDB_ExecDirect: Using RESTful API", LOG_LEVEL_DEBUG); + return IoTDB_ExecDirect_Rest(stmt, sqlForServer); + } else { + logMessage(cnct, "IoTDB_ExecDirect: Using Session API", LOG_LEVEL_DEBUG); + return IoTDB_ExecDirect_Session(stmt, sqlForServer); + } +} + +static void PopulateImplementationRowDescriptor(StatementHandle* stmt) { + auto* descriptor = stmt->implicitImpRowDesc; + descriptor->records.clear(); + descriptor->count = 0; + if (!stmt->resultSetPtr) + return; + descriptor->records.resize(static_cast(stmt->resultSetPtr->getNumColumns())); + descriptor->count = + static_cast(std::min(stmt->resultSetPtr->getNumColumns(), + static_cast(std::numeric_limits::max()))); + for (SQLSMALLINT index = 0; index < descriptor->count; ++index) { + DescriptorRecord& record = descriptor->records[static_cast(index)]; + record.name = stmt->resultSetPtr->getColumnName(static_cast(index)); + record.unnamed = record.name.empty() ? SQL_UNNAMED : SQL_NAMED; + std::string type = stmt->resultSetPtr->getColumnType(static_cast(index)); + std::transform(type.begin(), type.end(), type.begin(), [](unsigned char character) { + return static_cast(std::toupper(character)); + }); + if (type == "BOOLEAN") { + record.type = record.conciseType = SQL_BIT; + record.length = record.octetLength = 1; + } else if (type == "INT16") { + record.type = record.conciseType = SQL_SMALLINT; + record.length = record.octetLength = sizeof(SQLSMALLINT); + record.precision = 5; + record.numPrecRadix = 10; + } else if (type == "INT32") { + record.type = record.conciseType = SQL_INTEGER; + record.length = record.octetLength = sizeof(SQLINTEGER); + record.precision = 10; + record.numPrecRadix = 10; + } else if (type == "INT64" || type == "TIMESTAMP") { + record.type = record.conciseType = SQL_BIGINT; + record.length = record.octetLength = sizeof(SQLBIGINT); + record.precision = 19; + record.numPrecRadix = 10; + } else if (type == "FLOAT") { + record.type = record.conciseType = SQL_REAL; + record.length = record.octetLength = sizeof(float); + record.precision = 7; + record.numPrecRadix = 2; + } else if (type == "DOUBLE") { + record.type = record.conciseType = SQL_DOUBLE; + record.length = record.octetLength = sizeof(double); + record.precision = 15; + record.numPrecRadix = 2; + } else if (type == "DATE") { + record.setConciseType(SQL_TYPE_DATE); + record.length = 10; + record.octetLength = sizeof(DATE_STRUCT); + } else if (type == "BLOB") { + record.type = record.conciseType = SQL_LONGVARBINARY; + } else { + record.type = record.conciseType = SQL_WVARCHAR; + } + record.nullable = SQL_NULLABLE_UNKNOWN; + } +} + +static SQLRETURN ExecuteBoundStatement(StatementHandle* stmt); + +SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, SQLCHAR* statementText, + SQLINTEGER textLength) { + StatementHandle* stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLExecDirect: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLExecDirect parameters: " + << "statementHandle = " << handleToString(statementHandle) + << ", statementText = " << (statementText ? "provided" : "nullptr") + << ", textLength = " << textLength; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (!statementHandle) { + logMessage(cnct, "SQLExecDirect: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + if (!statementText) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + if (textLength < 0 && textLength != SQL_NTS) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + stmt->prepared = false; + stmt->rowsReturned = 0; + stmt->curRow = -1; + stmt->streamCurRow = 0; + stmt->isStream = false; + + // Convert SQLCHAR to std::string + std::string sqlStatement = ConvertSQLCHARToString(statementText, textLength); + logMessage(cnct, "SQLExecDirect: Executing statement", LOG_LEVEL_DEBUG); + + stmt->statementText = sqlStatement; + const SQLRETURN returnCode = ExecuteBoundStatement(stmt); + if (returnCode != SQL_SUCCESS) { + logMessage(cnct, "SQLExecDirect: Execution failed with code: " + std::to_string(returnCode), + LOG_LEVEL_ERROR); + } else { + logMessage(cnct, "SQLExecDirect: Execution completed successfully", LOG_LEVEL_INFO); + } + + logMessage(cnct, "SQLExecDirect: Exiting", LOG_LEVEL_TRACE); + return returnCode; +} + +static bool IsDataAtExecution(SQLLEN indicator) { + return indicator == SQL_DATA_AT_EXEC || indicator <= SQL_LEN_DATA_AT_EXEC_OFFSET; +} + +static size_t CTypeSize(SQLSMALLINT type, SQLLEN bufferLength) { + switch (type) { + case SQL_C_BIT: + case SQL_C_TINYINT: + case SQL_C_STINYINT: + case SQL_C_UTINYINT: + return 1; + case SQL_C_SSHORT: + case SQL_C_SHORT: + case SQL_C_USHORT: + return sizeof(SQLSMALLINT); + case SQL_C_SLONG: + case SQL_C_LONG: + case SQL_C_ULONG: + return sizeof(SQLINTEGER); + case SQL_C_SBIGINT: + case SQL_C_UBIGINT: + return sizeof(SQLBIGINT); + case SQL_C_FLOAT: + return sizeof(float); + case SQL_C_DOUBLE: + return sizeof(double); + case SQL_C_TYPE_DATE: + case SQL_C_DATE: + return sizeof(DATE_STRUCT); + case SQL_C_TYPE_TIME: + case SQL_C_TIME: + return sizeof(TIME_STRUCT); + case SQL_C_TYPE_TIMESTAMP: + case SQL_C_TIMESTAMP: + return sizeof(TIMESTAMP_STRUCT); + default: + return static_cast(std::max(bufferLength, 1)); + } +} + +static SQLSMALLINT DefaultParameterCType(SQLSMALLINT sqlType) { + switch (sqlType) { + case SQL_BIT: + return SQL_C_BIT; + case SQL_TINYINT: + return SQL_C_STINYINT; + case SQL_SMALLINT: + return SQL_C_SSHORT; + case SQL_INTEGER: + return SQL_C_SLONG; + case SQL_BIGINT: + return SQL_C_SBIGINT; + case SQL_REAL: + return SQL_C_FLOAT; + case SQL_FLOAT: + case SQL_DOUBLE: + return SQL_C_DOUBLE; + case SQL_DECIMAL: + case SQL_NUMERIC: + return SQL_C_CHAR; + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + return SQL_C_WCHAR; + case SQL_TYPE_DATE: + case SQL_DATE: + return SQL_C_TYPE_DATE; + case SQL_TYPE_TIME: + case SQL_TIME: + return SQL_C_TYPE_TIME; + case SQL_TYPE_TIMESTAMP: + case SQL_TIMESTAMP: + return SQL_C_TYPE_TIMESTAMP; + case SQL_BINARY: + case SQL_VARBINARY: + case SQL_LONGVARBINARY: + return SQL_C_BINARY; + default: + return SQL_C_CHAR; + } +} + +static SQLPOINTER ParameterValueAt(const StatementHandle* stmt, const ParameterBinding& binding, + SQLULEN set) { + if (!binding.valuePtr) + return nullptr; + const SQLULEN offset = stmt->paramBindOffsetPtr ? *stmt->paramBindOffsetPtr : 0; + const size_t stride = stmt->paramBindType == SQL_BIND_BY_COLUMN + ? CTypeSize(binding.valueType == SQL_C_DEFAULT + ? DefaultParameterCType(binding.parameterType) + : binding.valueType, + binding.bufferLength) + : static_cast(stmt->paramBindType); + return static_cast(binding.valuePtr) + offset + set * stride; +} + +static SQLLEN ParameterIndicatorAt(const StatementHandle* stmt, const ParameterBinding& binding, + SQLULEN set) { + const SQLULEN offset = stmt->paramBindOffsetPtr ? *stmt->paramBindOffsetPtr : 0; + const SQLULEN stride = + stmt->paramBindType == SQL_BIND_BY_COLUMN ? sizeof(SQLLEN) : stmt->paramBindType; + auto read = [&](const SQLLEN* pointer) { + SQLLEN result; + std::memcpy(&result, reinterpret_cast(pointer) + offset + set * stride, + sizeof(result)); + return result; + }; + if (binding.indicatorPtr && read(binding.indicatorPtr) == SQL_NULL_DATA) + return SQL_NULL_DATA; + if (binding.octetLengthPtr) + return read(binding.octetLengthPtr); + const SQLSMALLINT type = binding.valueType == SQL_C_DEFAULT + ? DefaultParameterCType(binding.parameterType) + : binding.valueType; + return type == SQL_C_CHAR || type == SQL_C_WCHAR ? SQL_NTS : binding.bufferLength; +} + +static std::string QuoteSqlString(const std::string& value) { + std::string result("'"); + for (char character : value) { + result.push_back(character); + if (character == '\'') + result.push_back('\''); + } + result.push_back('\''); + return result; +} + +static bool ParameterLiteral(StatementHandle* stmt, ParameterBinding& binding, SQLULEN set, + std::string& literal) { + const SQLSMALLINT valueType = binding.valueType == SQL_C_DEFAULT + ? DefaultParameterCType(binding.parameterType) + : binding.valueType; + SQLLEN indicator = ParameterIndicatorAt(stmt, binding, set); + SQLPOINTER value = ParameterValueAt(stmt, binding, set); + alignas(long double) char streamedValue[sizeof(TIMESTAMP_STRUCT)]{}; + if (indicator == SQL_NULL_DATA) { + literal = "NULL"; + return true; + } + if (IsDataAtExecution(indicator)) { + if (set >= binding.streamedData.size() || set >= binding.streamedNull.size()) { + stmt->addDiagnostic("HY010", "Parameter data has not been supplied"); + return false; + } + if (binding.streamedNull[set]) { + literal = "NULL"; + return true; + } + const auto& bytes = binding.streamedData[set]; + indicator = static_cast(bytes.size()); + value = const_cast(bytes.data()); + if (valueType != SQL_C_CHAR && valueType != SQL_C_WCHAR && valueType != SQL_C_BINARY) { + const size_t required = CTypeSize(valueType, binding.bufferLength); + if (required > sizeof(streamedValue) || bytes.size() != required) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return false; + } + std::memcpy(streamedValue, bytes.data(), required); + value = streamedValue; + } + } + if (!value) { + stmt->addDiagnostic("HY009", "Invalid use of null parameter value pointer"); + return false; + } + std::ostringstream output; + if (indicator < 0 && + !(indicator == SQL_NTS && (valueType == SQL_C_CHAR || valueType == SQL_C_WCHAR))) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return false; + } + output.imbue(std::locale::classic()); + output << std::setprecision(std::numeric_limits::max_digits10); + switch (valueType) { + case SQL_C_CHAR: { + const char* text = static_cast(value); + const size_t length = indicator == SQL_NTS ? std::strlen(text) : static_cast(indicator); + literal = QuoteSqlString(std::string(text, length)); + return true; + } + case SQL_C_WCHAR: { + const auto* text = static_cast(value); + const size_t length = indicator == SQL_NTS ? std::char_traits::length(text) + : static_cast(indicator) / sizeof(SQLWCHAR); + std::u16string utf16(length, u'\0'); + if (length) + std::memcpy(&utf16[0], text, length * sizeof(SQLWCHAR)); + literal = QuoteSqlString( + std::wstring_convert, char16_t>().to_bytes(utf16)); + return true; + } + case SQL_C_BIT: + output << (*static_cast(value) ? 1 : 0); + break; + case SQL_C_STINYINT: + case SQL_C_TINYINT: + output << static_cast(*static_cast(value)); + break; + case SQL_C_UTINYINT: + output << static_cast(*static_cast(value)); + break; + case SQL_C_SSHORT: + case SQL_C_SHORT: + output << *static_cast(value); + break; + case SQL_C_USHORT: + output << *static_cast(value); + break; + case SQL_C_SLONG: + case SQL_C_LONG: + output << *static_cast(value); + break; + case SQL_C_ULONG: + output << *static_cast(value); + break; + case SQL_C_SBIGINT: + output << *static_cast(value); + break; + case SQL_C_UBIGINT: + output << *static_cast(value); + break; + case SQL_C_FLOAT: + output << *static_cast(value); + break; + case SQL_C_DOUBLE: + output << *static_cast(value); + break; + case SQL_C_BINARY: { + const size_t length = static_cast(indicator); + static constexpr char digits[] = "0123456789ABCDEF"; + literal = "X'"; + for (size_t index = 0; index < length; ++index) { + const unsigned char byte = static_cast(value)[index]; + literal.push_back(digits[byte >> 4]); + literal.push_back(digits[byte & 0x0F]); + } + literal.push_back('\''); + return true; + } + case SQL_C_TYPE_DATE: + case SQL_C_DATE: { + const auto* date = static_cast(value); + output << "'" << std::setfill('0') << std::setw(4) << date->year << "-" << std::setw(2) + << date->month << "-" << std::setw(2) << date->day << "'"; + break; + } + case SQL_C_TYPE_TIME: + case SQL_C_TIME: { + const auto* time = static_cast(value); + output << "'" << std::setfill('0') << std::setw(2) << time->hour << ":" << std::setw(2) + << time->minute << ":" << std::setw(2) << time->second << "'"; + break; + } + case SQL_C_TYPE_TIMESTAMP: + case SQL_C_TIMESTAMP: { + const auto* timestamp = static_cast(value); + output << "'" << std::setfill('0') << std::setw(4) << timestamp->year << "-" << std::setw(2) + << timestamp->month << "-" << std::setw(2) << timestamp->day << " " << std::setw(2) + << timestamp->hour << ":" << std::setw(2) << timestamp->minute << ":" << std::setw(2) + << timestamp->second << "." << std::setw(9) << timestamp->fraction << "'"; + break; + } + default: + stmt->addDiagnostic("HYC00", "Unsupported parameter C data type"); + return false; + } + literal = output.str(); + return true; +} + +static bool RenderParameters(StatementHandle* stmt, SQLULEN set, std::string& sql) { + const std::vector markers = ParameterMarkerPositions(stmt->statementText); + if (markers.size() > stmt->parameterBindings.size()) { + stmt->addDiagnostic("07002", "COUNT field incorrect: not all parameters are bound"); + return false; + } + sql.clear(); + size_t previous = 0; + for (size_t index = 0; index < markers.size(); ++index) { + ParameterBinding& binding = stmt->parameterBindings[index]; + if (!binding.bound) { + stmt->addDiagnostic("07002", "COUNT field incorrect: parameter is not bound"); + return false; + } + std::string literal; + if (!ParameterLiteral(stmt, binding, set, literal)) + return false; + sql.append(stmt->statementText, previous, markers[index] - previous); + sql += literal; + previous = markers[index] + 1; + } + sql.append(stmt->statementText, previous, std::string::npos); + return true; +} + +static SQLRETURN ExecuteParameterSets(StatementHandle* stmt) { + SQLRETURN result = SQL_SUCCESS; + if (stmt->paramsProcessedPtr) + *stmt->paramsProcessedPtr = 0; + const SQLULEN setCount = + ParameterMarkerPositions(stmt->statementText).empty() ? 1 : stmt->paramSetSize; + for (SQLULEN set = 0; set < setCount; ++set) { + std::string sql; + if (!RenderParameters(stmt, set, sql)) { + result = SQL_ERROR; + } else { + logMessage(stmt->getConnection(), "SQLExecute: Executing prepared statement", + LOG_LEVEL_DEBUG); + stmt->curRow = -1; + stmt->streamCurRow = 0; + stmt->isStream = false; + result = IoTDB_ExecDirect(stmt, sql, false); + if (SQL_SUCCEEDED(result)) + PopulateImplementationRowDescriptor(stmt); + } + if (stmt->paramStatusPtr) + stmt->paramStatusPtr[set] = SQL_SUCCEEDED(result) ? SQL_PARAM_SUCCESS : SQL_PARAM_ERROR; + if (stmt->paramsProcessedPtr) + *stmt->paramsProcessedPtr = set + 1; + if (!SQL_SUCCEEDED(result)) + return result; + } + return result; +} + +static void SyncParameterBindingsFromDescriptors(StatementHandle* stmt) { + auto* apd = static_cast(stmt->appParamDesc); + auto* ipd = static_cast(stmt->impParamDesc); + stmt->paramSetSize = apd->arraySize; + stmt->paramBindType = apd->bindType; + stmt->paramBindOffsetPtr = reinterpret_cast(apd->bindOffsetPtr); + stmt->paramsProcessedPtr = ipd->rowsProcessedPtr; + stmt->paramStatusPtr = ipd->arrayStatusPtr; + const SQLSMALLINT count = apd->count; + stmt->parameterBindings.assign(static_cast(count), ParameterBinding()); + for (int number = 1; number <= count; ++number) { + const DescriptorRecord* app = apd->findRecord(number); + const DescriptorRecord* implementation = ipd->findRecord(number); + if (!app) + continue; + ParameterBinding& binding = stmt->parameterBindings[static_cast(number - 1)]; + binding.valueType = app->conciseType; + binding.valuePtr = app->dataPtr; + binding.bufferLength = app->octetLength; + binding.indicatorPtr = app->indicatorPtr; + binding.octetLengthPtr = app->octetLengthPtr; + if (implementation) { + binding.inputOutputType = implementation->parameterType; + binding.parameterType = implementation->conciseType; + binding.columnSize = implementation->length; + binding.decimalDigits = implementation->scale; + } + binding.bound = app->dataPtr != nullptr || binding.indicatorPtr != nullptr || + binding.octetLengthPtr != nullptr; + } +} + +SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { + if (!statementHandle) { + logMessage(nullptr, "SQLExecute: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + if (!stmt->prepared) { + stmt->addDiagnostic("HY010", "Statement has not been prepared"); + return SQL_ERROR; + } + return ExecuteBoundStatement(stmt); +} + +static SQLRETURN ExecuteBoundStatement(StatementHandle* stmt) { + stmt->rowsReturned = 0; + SyncParameterBindingsFromDescriptors(stmt); + const std::vector markers = ParameterMarkerPositions(stmt->statementText); + if (stmt->paramsProcessedPtr) + *stmt->paramsProcessedPtr = 0; + if (stmt->paramStatusPtr) + std::fill(stmt->paramStatusPtr, stmt->paramStatusPtr + stmt->paramSetSize, SQL_PARAM_UNUSED); + for (size_t index = 0; index < markers.size(); ++index) { + if (index >= stmt->parameterBindings.size() || !stmt->parameterBindings[index].bound) { + stmt->addDiagnostic("07002", "COUNT field incorrect: not all parameters are bound"); + return SQL_ERROR; + } + } + for (size_t index = 0; index < markers.size(); ++index) { + for (SQLULEN set = 0; set < stmt->paramSetSize; ++set) { + if (IsDataAtExecution(ParameterIndicatorAt(stmt, stmt->parameterBindings[index], set))) { + stmt->needsParameterData = true; + stmt->nextDataSet = 0; + stmt->activeDataSet = std::numeric_limits::max(); + stmt->nextDataParameter = 0; + stmt->activeDataParameter = std::numeric_limits::max(); + for (auto& binding : stmt->parameterBindings) { + binding.streamedData.assign(stmt->paramSetSize, std::string()); + binding.streamedNull.assign(stmt->paramSetSize, false); + } + return SQL_NEED_DATA; + } + } + } + try { + return ExecuteParameterSets(stmt); + } catch (const std::exception& error) { + stmt->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } +} + +static void SyncColumnBindingsFromDescriptor(StatementHandle* stmt) { + auto* ard = static_cast(stmt->appRowDesc); + stmt->rowArraySize = ard->arraySize; + stmt->rowBindType = ard->bindType; + stmt->rowBindOffsetPtr = reinterpret_cast(ard->bindOffsetPtr); + auto* ird = static_cast(stmt->impRowDesc); + stmt->rowsFetchedPtr = ird->rowsProcessedPtr; + stmt->rowStatusPtr = ird->arrayStatusPtr; + stmt->columnBindings.assign(static_cast(ard->count) + 1, BindColInfo()); + for (int number = 1; number <= ard->count; ++number) { + const DescriptorRecord* record = ard->findRecord(number); + if (!record) + continue; + BindColInfo& binding = stmt->columnBindings[static_cast(number)]; + binding.targetType = record->conciseType == SQL_C_DEFAULT && stmt->resultSetPtr + ? stmt->resultSetPtr->getDefaultCTypeForColumn(number) + : record->conciseType; + binding.targetValuePtr = record->dataPtr; + binding.bufferLength = record->octetLength; + binding.strLen_or_IndPtr = record->indicatorPtr ? record->indicatorPtr : record->octetLengthPtr; + binding.isBound = record->dataPtr != nullptr || binding.strLen_or_IndPtr != nullptr; + } + if (stmt->resultSetPtr) + stmt->resultSetPtr->bindColInfo = stmt->columnBindings; +} + +SQLRETURN fillColBindBuffer(StatementHandle* stmt, SQLUSMALLINT col, const ODBCField& value, + SQLULEN rowsetIndex) { + BindColInfo* binding = stmt->resultSetPtr->getBindColInfo(col); + if (!binding || !binding->isBound) { + return SQL_ERROR; + } + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + const SQLULEN offset = stmt->rowBindOffsetPtr ? *stmt->rowBindOffsetPtr : 0; + const size_t valueStride = stmt->rowBindType == SQL_BIND_BY_COLUMN + ? CTypeSize(binding->targetType, binding->bufferLength) + : static_cast(stmt->rowBindType); + const size_t indicatorStride = stmt->rowBindType == SQL_BIND_BY_COLUMN + ? sizeof(SQLLEN) + : static_cast(stmt->rowBindType); + SQLPOINTER target = binding->targetValuePtr + ? static_cast(static_cast(binding->targetValuePtr) + + offset + rowsetIndex * valueStride) + : nullptr; + const auto* record = static_cast(stmt->appRowDesc)->findRecord(col); + auto pointerAt = [&](SQLLEN* pointer) -> SQLLEN* { + return pointer ? reinterpret_cast(reinterpret_cast(pointer) + offset + + rowsetIndex * indicatorStride) + : nullptr; + }; + SQLLEN* indicator = pointerAt(record->indicatorPtr); + SQLLEN* octetLength = pointerAt(record->octetLengthPtr); + + if (value.isNull()) { + if (indicator) { + *indicator = SQL_NULL_DATA; + } else { + stmt->addDiagnostic("22002", "Indicator variable required but not supplied"); + return SQL_ERROR; + } + + if (target && binding->bufferLength > 0) { + if (binding->targetType == SQL_C_CHAR) { + static_cast(target)[0] = '\0'; + } else if (binding->targetType == SQL_C_WCHAR && + binding->bufferLength >= static_cast(sizeof(SQLWCHAR))) { + static_cast(target)[0] = 0; + } + } + return SQL_SUCCESS; + } + + SQLLEN length = 0; + SQLRETURN result = CopyFieldToTarget(stmt, value, binding->targetType, target, + binding->bufferLength, &length, true, col); + if (SQL_SUCCEEDED(result)) { + if (indicator && indicator != octetLength) + *indicator = 0; + if (octetLength) + *octetLength = length; + } + return result; +} + +SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLFetch: Entering", LOG_LEVEL_TRACE); + + // Prepare log message for the parameters + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLFetch parameters: " + << "statementHandle = " << handleToString(statementHandle); + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLAllocHandle we're allocating a NULL_Handle, so I disabled this. + if (!statementHandle) { + logMessage(cnct, "SQLFetch: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + SyncColumnBindingsFromDescriptor(stmt); + stmt->lastGetDataRow = -1; + stmt->lastGetDataCol = 0; + stmt->getDataOffsets.clear(); + if (stmt->rowsFetchedPtr) + *stmt->rowsFetchedPtr = 0; + if (stmt->rowStatusPtr) { + for (SQLULEN index = 0; index < stmt->rowArraySize; ++index) + stmt->rowStatusPtr[index] = SQL_ROW_NOROW; + } + if (stmt->maxRows > 0 && stmt->rowsReturned >= stmt->maxRows) + return SQL_NO_DATA; + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "SQLFetch: No result set available", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "No result set available"); + return SQL_ERROR; + } + + SQLULEN fetched = 0; + SQLULEN failed = 0; + SQLRETURN overall = SQL_SUCCESS; + while (fetched < stmt->rowArraySize && + (stmt->maxRows == 0 || stmt->rowsReturned < stmt->maxRows)) { + stmt->curRow = stmt->curRow < 0 ? 0 : stmt->curRow + 1; + if (stmt->curRow == stmt->resultSetPtr->getNumRows()) { + const SQLSMALLINT streamResult = streamNextBatch(stmt); + if (streamResult == SQL_NO_DATA) + break; + if (streamResult != SQL_SUCCESS) + return streamResult; + stmt->curRow = 0; + } + if (stmt->curRow < 0 || stmt->curRow >= stmt->resultSetPtr->getNumRows()) + break; + + SQLRETURN rowResult = SQL_SUCCESS; + for (SQLUSMALLINT col = 1; col <= stmt->resultSetPtr->getNumColumns(); ++col) { + BindColInfo* binding = stmt->resultSetPtr->getBindColInfo(col); + if (!binding || !binding->isBound) + continue; + const ODBCField& value = stmt->resultSetPtr->getValue(stmt->curRow, col - 1); + const SQLRETURN result = fillColBindBuffer(stmt, col, value, fetched); + if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) { + rowResult = SQL_ERROR; + overall = SQL_SUCCESS_WITH_INFO; + ++failed; + break; + } + if (result == SQL_SUCCESS_WITH_INFO) + rowResult = overall = SQL_SUCCESS_WITH_INFO; + } + if (stmt->rowStatusPtr) + stmt->rowStatusPtr[fetched] = rowResult == SQL_ERROR ? SQL_ROW_ERROR + : rowResult == SQL_SUCCESS_WITH_INFO ? SQL_ROW_SUCCESS_WITH_INFO + : SQL_ROW_SUCCESS; + ++fetched; + ++stmt->rowsReturned; + } + if (stmt->rowsFetchedPtr) + *stmt->rowsFetchedPtr = fetched; + return fetched == 0 ? SQL_NO_DATA : failed == fetched ? SQL_ERROR : overall; +} + +SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT statementHandle, SQLSMALLINT fetchOrientation, + SQLLEN fetchOffset) { + if (!statementHandle) { + logMessage(nullptr, "SQLFetchScroll: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + (void)fetchOffset; + if (fetchOrientation == SQL_FETCH_NEXT) + return SQLFetch(statementHandle); + stmt->addDiagnostic("HYC00", "Only forward SQL_FETCH_NEXT is supported"); + return SQL_ERROR; +} + +SQLRETURN SQL_API SQLFreeConnect(SQLHDBC connectionHandle) { + logMessage("SQLFreeConnect"); + SQLRETURN retcode = SQLFreeHandle(SQL_HANDLE_DBC, connectionHandle); + if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO) { + std::ostringstream oss; + oss << "SQLFreeConnect: Failed to free connection handle. retcode = " << retcode << "\n"; + logMessage(oss.str()); + return retcode; + } + return retcode; +} + +SQLRETURN SQL_API SQLFreeEnv(SQLHENV environmentHandle) { + logMessage("SQLFreeEnv"); + // Deprecated function, delegate to SQLAllocHandle for env allocation. + SQLRETURN retcode = SQLFreeHandle(SQL_HANDLE_ENV, environmentHandle); + if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO) { + std::ostringstream oss; + oss << "SQLAllocEnv: Failed to free environment handle. retcode = " << retcode << "\n"; + logMessage(oss.str()); + return retcode; + } + return retcode; +} + +SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { + logMessage("Entering SQLFreeHandle"); + + // Check if handle is NULL + if (!ODBCHandle::isLiveHandle(handle, handleType)) { + logMessage("SQLFreeHandle: handle is already NULL."); + return SQL_INVALID_HANDLE; // Invalid handle + } + + // Check if handleType is valid + if (handleType != SQL_HANDLE_ENV && handleType != SQL_HANDLE_DBC && + handleType != SQL_HANDLE_STMT && handleType != SQL_HANDLE_DESC) { + logMessage("SQLFreeHandle: invalid handle type."); + return SQL_ERROR; // Invalid handle type + } + + // Convert handle to ODBCHandle type + ODBCHandle* odbcHandle = static_cast(handle); + + if (odbcHandle->getHandleType() != handleType) { + logMessage("SQLFreeHandle: handle type does not match the allocated handle."); + return SQL_INVALID_HANDLE; + } + + // Check if handle has already been freed + if (odbcHandle->isHandleFreed()) { + logMessage("SQLFreeHandle: handle is already freed."); + return SQL_SUCCESS; // Handle already freed, return success directly + } + + if (handleType == SQL_HANDLE_DESC && static_cast(handle)->implicit) { + odbcHandle->clearDiagnostics(); + odbcHandle->addDiagnostic("HY017", "Invalid use of an automatically allocated descriptor"); + return SQL_ERROR; + } + + // Mark handle as freed + odbcHandle->markAsFreed(); + + // Release resources based on handleType + switch (handleType) { + case SQL_HANDLE_ENV: { + logMessage("SQLFreeHandle: freeing environment handle."); + delete static_cast(handle); + break; + } + case SQL_HANDLE_DBC: { + logMessage("SQLFreeHandle: freeing connection handle."); + delete static_cast(handle); + break; + } + case SQL_HANDLE_STMT: { + logMessage("SQLFreeHandle: freeing statement handle."); + delete static_cast(handle); + break; + } + case SQL_HANDLE_DESC: { + auto* descriptor = static_cast(handle); + for (auto* statement : descriptor->connection->statements) { + if (statement->appRowDesc == descriptor) + statement->appRowDesc = statement->implicitAppRowDesc; + if (statement->appParamDesc == descriptor) + statement->appParamDesc = statement->implicitAppParamDesc; + } + delete descriptor; + break; + } + default: { + logMessage("SQLFreeHandle: unknown handle type."); + return SQL_ERROR; // Unknown handle type + } + } + + logMessage("Exiting SQLFreeHandle"); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT statementHandle, SQLUSMALLINT option) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLFreeStmt: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLFreeStmt parameters: " + << "statementHandle = " << handleToString(statementHandle) << ", option = " << option; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (!statementHandle) { + logMessage(cnct, "SQLFreeStmt: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + + switch (option) { + case SQL_CLOSE: { + logMessage(cnct, "SQLFreeStmt: Processing SQL_CLOSE option", LOG_LEVEL_DEBUG); + // Close cursor and reset result set + stmt->curRow = -1; // Reset current row + stmt->ClearResultSet(); + logMessage(cnct, "SQLFreeStmt: Cursor closed and result set reset", LOG_LEVEL_DEBUG); + break; + } + + case SQL_UNBIND: { + logMessage(cnct, "SQLFreeStmt: Processing SQL_UNBIND option", LOG_LEVEL_DEBUG); + // Unbind columns + if (stmt->resultSetPtr) { + stmt->resultSetPtr->unbindColumns(); + } + stmt->columnBindings.clear(); + auto* ard = static_cast(stmt->appRowDesc); + ard->records.clear(); + ard->count = 0; + logMessage(cnct, "SQLFreeStmt: Column bindings released", LOG_LEVEL_DEBUG); + break; + } + + case SQL_RESET_PARAMS: { + logMessage(cnct, "SQLFreeStmt: Processing SQL_RESET_PARAMS option", LOG_LEVEL_DEBUG); + stmt->parameterBindings.clear(); + stmt->needsParameterData = false; + stmt->nextDataSet = 0; + stmt->activeDataSet = std::numeric_limits::max(); + stmt->nextDataParameter = 0; + stmt->activeDataParameter = std::numeric_limits::max(); + auto* apd = static_cast(stmt->appParamDesc); + apd->records.clear(); + apd->count = 0; + auto* ipd = static_cast(stmt->impParamDesc); + ipd->records.clear(); + ipd->count = 0; + break; + } + + case SQL_DROP: { + logMessage(cnct, "SQLFreeStmt: Processing SQL_DROP option", LOG_LEVEL_DEBUG); + // This option is deprecated in ODBC 3.0, recommend using SQLFreeHandle instead + logMessage(cnct, "SQLFreeStmt: SQL_DROP is deprecated in ODBC 3.0, use SQLFreeHandle instead", + LOG_LEVEL_WARN); + return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); + } + + default: { + logMessage(cnct, "SQLFreeStmt: Unrecognized option: " + std::to_string(option), LOG_LEVEL_WARN); + stmt->addDiagnostic("HY092", "Invalid option value"); + return SQL_ERROR; + } + } + logMessage(cnct, "SQLFreeStmt: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, + SQLPOINTER valuePtr, SQLINTEGER bufferLength, + SQLINTEGER* stringLength) { + ConnectionHandle* cnct = static_cast(connectionHandle); + + logMessage(cnct, "SQLGetConnectAttr: Entering", LOG_LEVEL_TRACE); + + // Only build parameter log message if TRACE level is enabled + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLGetConnectAttr parameters: " + << "ConnectionHandle = " << handleToString(connectionHandle) + << ", Attribute = " << attribute << ", ValuePtr = " << valueToString(valuePtr) + << ", BufferLength = " << bufferLength + << ", StringLengthPtr = " << valueToString(stringLength); + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLAllocHandle we're allocating a NULL_Handle, so I disabled this. + if (!connectionHandle) { + logMessage(cnct, "SQLGetConnectAttr: Invalid connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + cnct->clearDiagnostics(); + + switch (attribute) { + // Connection metadata attributes + case SQL_ATTR_CONNECTION_DEAD: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_CONNECTION_DEAD", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = SQL_CD_FALSE; // Assume connection is alive + logMessage(cnct, "SQLGetConnectAttr: Connection status set to ALIVE", LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_CONNECTION_TIMEOUT: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_CONNECTION_TIMEOUT", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = cnct->timeoutConnection; + logMessage(cnct, + "SQLGetConnectAttr: Connection timeout set to " + + std::to_string(cnct->timeoutConnection), + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_LOGIN_TIMEOUT: + if (valuePtr) + *static_cast(valuePtr) = cnct->timeoutLogin; + break; + + // Transaction control attributes + case SQL_ATTR_AUTOCOMMIT: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_AUTOCOMMIT", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = cnct->autoCommit ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF; + logMessage(cnct, "SQLGetConnectAttr: Auto-commit set to ON", LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_TXN_ISOLATION: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_TXN_ISOLATION", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = 0; // SQL_TXN_CAPABLE is SQL_TC_NONE. + } + break; + + // Catalog/schema attributes + case SQL_ATTR_CURRENT_CATALOG: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_CURRENT_CATALOG", LOG_LEVEL_DEBUG); + { + const SQLINTEGER requiredLength = static_cast(cnct->database.size()); + if (stringLength) + *stringLength = requiredLength; + if (!valuePtr) + break; + if (bufferLength <= 0) { + cnct->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + const size_t copyLength = + std::min(cnct->database.size(), static_cast(bufferLength - 1)); + std::memcpy(valuePtr, cnct->database.data(), copyLength); + static_cast(valuePtr)[copyLength] = '\0'; + if (copyLength < cnct->database.size()) { + cnct->addDiagnostic("01004", "String data, right truncated"); + return SQL_SUCCESS_WITH_INFO; + } + break; + } + + // ODBC behavior control attributes + case SQL_ATTR_ODBC_CURSORS: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_ODBC_CURSORS", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = SQL_CUR_USE_DRIVER; // Use driver cursors + logMessage(cnct, "SQLGetConnectAttr: ODBC cursor type set to USE_DRIVER", LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_ACCESS_MODE: + logMessage(cnct, "SQLGetConnectAttr: Processing SQL_ATTR_ACCESS_MODE", LOG_LEVEL_DEBUG); + if (valuePtr) { + *((SQLUINTEGER*)valuePtr) = SQL_MODE_READ_WRITE; // Default read-write mode + logMessage(cnct, "SQLGetConnectAttr: Access mode set to READ_WRITE", LOG_LEVEL_DEBUG); + } + break; + + default: + logMessage(cnct, "SQLGetConnectAttr: Unsupported attribute: " + std::to_string(attribute), + LOG_LEVEL_WARN); + cnct->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } + + if (stringLength && attribute != SQL_ATTR_CURRENT_CATALOG) + *stringLength = 0; + + logMessage(cnct, "SQLGetConnectAttr: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLGetConnectOption(SQLHDBC connectionHandle, SQLUSMALLINT option, + SQLPOINTER value) { + if (!connectionHandle) { + logMessage(nullptr, "SQLGetConnectOption: Invalid connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + switch (option) { + case SQL_ACCESS_MODE: + return SQLGetConnectAttr(connectionHandle, SQL_ATTR_ACCESS_MODE, value, 0, nullptr); + case SQL_AUTOCOMMIT: + return SQLGetConnectAttr(connectionHandle, SQL_ATTR_AUTOCOMMIT, value, 0, nullptr); + case SQL_LOGIN_TIMEOUT: + return SQLGetConnectAttr(connectionHandle, SQL_ATTR_LOGIN_TIMEOUT, value, 0, nullptr); + case SQL_TXN_ISOLATION: + return SQLGetConnectAttr(connectionHandle, SQL_ATTR_TXN_ISOLATION, value, 0, nullptr); + case SQL_CURRENT_QUALIFIER: + return SQLGetConnectAttr(connectionHandle, SQL_ATTR_CURRENT_CATALOG, value, + SQL_MAX_OPTION_STRING_LENGTH, nullptr); + default: + auto* cnct = static_cast(connectionHandle); + cnct->clearDiagnostics(); + cnct->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLGetCursorName(SQLHSTMT statementHandle, SQLCHAR* cursorName, + SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr) { + if (!statementHandle) { + logMessage(nullptr, "SQLGetCursorName: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + SQLRETURN result = setString(stmt->cursorName, cursorName, bufferLength, nameLengthPtr); + if (result == SQL_SUCCESS_WITH_INFO) + stmt->addDiagnostic("01004", "Cursor name was truncated"); + return result; +} + +// REST responses are decoded from the server's JSON result representation. +SQLRETURN SQL_API SQLGetData(const SQLHSTMT statementHandle, const SQLUSMALLINT columnNumber, + const SQLSMALLINT targetType, const SQLPOINTER targetValue, + const SQLLEN bufferLength, SQLLEN* strLenOrInd) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLGetData: Entering", LOG_LEVEL_TRACE); + + // Prepare log message for the parameters + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLGetData parameters: " + << "statementHandle = " << handleToString(statementHandle) + << ", columnNumber = " << columnNumber + << ", targetType = " << CDataTypeName(targetType) << ", targetValue = " << targetValue + << ", bufferLength = " << bufferLength << ", strLenOrInd = " << strLenOrInd; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLAllocHandle we're allocating a NULL_Handle, so I disabled this. + if (!statementHandle) { + logMessage(cnct, "SQLGetData: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + if (!stmt->resultSetPtr || stmt->curRow < 0 || stmt->curRow >= stmt->resultSetPtr->getNumRows()) { + stmt->addDiagnostic("24000", "Invalid cursor state"); + return SQL_ERROR; + } + if (columnNumber == 0 || columnNumber > stmt->resultSetPtr->getNumColumns()) { + stmt->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + if (bufferLength < 0) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + logMessage(cnct, "SQLGetData: stmt->curRow = " + std::to_string(stmt->curRow), LOG_LEVEL_TRACE); + + if (stmt->curRow != stmt->lastGetDataRow) { + stmt->lastGetDataRow = stmt->curRow; + stmt->lastGetDataCol = 0; + const int numCols = stmt->resultSetPtr ? stmt->resultSetPtr->getNumColumns() : 0; + stmt->getDataOffsets.assign(static_cast(numCols + 1), 0); + } + + const ODBCField* valuePtr = nullptr; + + // Retrieve data as string and parse as JSON (works for both Session and REST APIs with our new approach) + logMessage(cnct, "SQLGetData: Retrieving data for processing", LOG_LEVEL_DEBUG); + + try { + valuePtr = &stmt->resultSetPtr->getValue(stmt->curRow, columnNumber - 1); + if (valuePtr->isNull()) { + logMessage(cnct, + "SQLGetData: NULL data retrieved for column " + std::to_string(columnNumber) + + ", row " + std::to_string(stmt->curRow), + LOG_LEVEL_DEBUG); + if (strLenOrInd != nullptr) { + logMessage(cnct, "SQLGetData: Setting strLenOrInd to SQL_NULL_DATA", LOG_LEVEL_DEBUG); + *strLenOrInd = SQL_NULL_DATA; // Complies with ODBC specification, marks data as NULL + return SQL_SUCCESS; + } else { + // Indicator pointer not provided, triggers ODBC standard error: 22002 + logMessage(cnct, + "SQLGetData: Error - Indicator variable (strLenOrInd) required but not supplied " + "for NULL value", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("22002", "Indicator variable required but not supplied"); + return SQL_ERROR; + } + } else { + logMessage(cnct, "SQLGetData: Successfully retrieved value", LOG_LEVEL_DEBUG); + } + } catch (const std::exception& e) { + logMessage(cnct, "SQLGetData: Unexpected error during data retrieval: " + std::string(e.what()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "Failed to retrieve data value"); + return SQL_ERROR; + } + + if (columnNumber != stmt->lastGetDataCol && + stmt->getDataOffsets.size() > static_cast(columnNumber)) { + stmt->getDataOffsets[columnNumber] = 0; + } + stmt->lastGetDataCol = columnNumber; + + SQLSMALLINT effectiveTargetType = (targetType == SQL_C_DEFAULT) + ? stmt->resultSetPtr->getDefaultCTypeForColumn(columnNumber) + : targetType; + if (targetType == SQL_ARD_TYPE) { + const auto* record = static_cast(stmt->appRowDesc)->findRecord(columnNumber); + if (!record) { + stmt->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + effectiveTargetType = record->conciseType; + if (effectiveTargetType == SQL_C_DEFAULT) + effectiveTargetType = stmt->resultSetPtr->getDefaultCTypeForColumn(columnNumber); + } + + SQLRETURN copyResult = SQL_SUCCESS; + if (effectiveTargetType == SQL_C_CHAR || effectiveTargetType == SQL_C_WCHAR || + effectiveTargetType == SQL_C_BINARY) { + copyResult = CopyFieldToTargetVariable(stmt, *valuePtr, effectiveTargetType, + const_cast(targetValue), bufferLength, + strLenOrInd, columnNumber); + } else { + copyResult = CopyFieldToTarget(stmt, *valuePtr, effectiveTargetType, + const_cast(targetValue), bufferLength, strLenOrInd, + false, targetType == SQL_ARD_TYPE ? columnNumber : 0); + } + if (copyResult != SQL_SUCCESS && copyResult != SQL_SUCCESS_WITH_INFO && + copyResult != SQL_NO_DATA) { + logMessage(cnct, + "SQLGetData: Unknown target type: " + CDataTypeName(targetType) + + ", returning SQL_ERROR", + LOG_LEVEL_TRACE); + return copyResult; + } + + logMessage(cnct, "SQLGetData: Exiting successfully", LOG_LEVEL_TRACE); + return copyResult; +} + +SQLRETURN SQL_API SQLGetDescField(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, + SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* stringLength) { + if (!descriptorHandle) + return SQL_INVALID_HANDLE; + auto* descriptor = static_cast(descriptorHandle); + if (descriptor->getHandleType() != SQL_HANDLE_DESC) + return SQL_INVALID_HANDLE; + descriptor->clearDiagnostics(); + + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW && descriptor->owner) + PopulateImplementationRowDescriptor(descriptor->owner); + + auto setStringField = [&](const std::string& text) -> SQLRETURN { + if (stringLength) + *stringLength = static_cast(text.size()); + if (!value) + return SQL_SUCCESS; + if (bufferLength < 0) { + descriptor->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (bufferLength == 0) + return text.empty() ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + const size_t copied = std::min(text.size(), static_cast(bufferLength - 1)); + std::memcpy(value, text.data(), copied); + static_cast(value)[copied] = '\0'; + return copied < text.size() ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; + }; + auto setSmallInt = [&](SQLSMALLINT result) { + if (value) + *static_cast(value) = result; + if (stringLength) + *stringLength = sizeof(SQLSMALLINT); + return SQL_SUCCESS; + }; + auto setLength = [&](SQLLEN result) { + if (value) + *static_cast(value) = result; + if (stringLength) + *stringLength = sizeof(SQLLEN); + return SQL_SUCCESS; + }; + auto setUnsignedLength = [&](SQLULEN result) { + if (value) + *static_cast(value) = result; + if (stringLength) + *stringLength = sizeof(SQLULEN); + return SQL_SUCCESS; + }; + auto setPointer = [&](SQLPOINTER result) { + if (value) + *static_cast(value) = result; + if (stringLength) + *stringLength = sizeof(SQLPOINTER); + return SQL_SUCCESS; + }; + + switch (fieldIdentifier) { + case SQL_DESC_ALLOC_TYPE: + return setSmallInt(descriptor->implicit ? SQL_DESC_ALLOC_AUTO : SQL_DESC_ALLOC_USER); + case SQL_DESC_ARRAY_SIZE: + return setUnsignedLength(descriptor->arraySize); + case SQL_DESC_ARRAY_STATUS_PTR: + return setPointer(descriptor->arrayStatusPtr); + case SQL_DESC_BIND_OFFSET_PTR: + return setPointer(descriptor->bindOffsetPtr); + case SQL_DESC_BIND_TYPE: + return setUnsignedLength(descriptor->bindType); + case SQL_DESC_COUNT: + return setSmallInt(descriptor->count); + case SQL_DESC_ROWS_PROCESSED_PTR: + return setPointer(descriptor->rowsProcessedPtr); + default: + break; + } + + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW && descriptor->owner && + !descriptor->owner->resultSetPtr) { + descriptor->addDiagnostic("HY007", "Associated statement is not prepared or executed"); + return SQL_ERROR; + } + + if (recNumber < 0) { + descriptor->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + if (recNumber == 0 || recNumber > descriptor->count) + return SQL_NO_DATA; + const DescriptorRecord* record = descriptor->findRecord(recNumber); + if (!record) + return SQL_NO_DATA; + switch (fieldIdentifier) { + case SQL_DESC_CONCISE_TYPE: + return setSmallInt(record->conciseType); + case SQL_DESC_TYPE: + return setSmallInt(record->type); + case SQL_DESC_DATETIME_INTERVAL_CODE: + return setSmallInt(record->datetimeIntervalCode); + case SQL_DESC_OCTET_LENGTH: + return setLength(record->octetLength); + case SQL_DESC_LENGTH: + return setUnsignedLength(record->length); + case SQL_DESC_PRECISION: + return setSmallInt(record->precision); + case SQL_DESC_SCALE: + return setSmallInt(record->scale); + case SQL_DESC_NULLABLE: + return setSmallInt(record->nullable); + case SQL_DESC_PARAMETER_TYPE: + return setSmallInt(record->parameterType); + case SQL_DESC_UNNAMED: + return setSmallInt(record->unnamed); + case SQL_DESC_NUM_PREC_RADIX: + if (value) + *static_cast(value) = record->numPrecRadix; + if (stringLength) + *stringLength = sizeof(SQLINTEGER); + return SQL_SUCCESS; + case SQL_DESC_DATA_PTR: + return setPointer(record->dataPtr); + case SQL_DESC_INDICATOR_PTR: + return setPointer(record->indicatorPtr); + case SQL_DESC_OCTET_LENGTH_PTR: + return setPointer(record->octetLengthPtr); + case SQL_DESC_NAME: + return setStringField(record->name); + default: + descriptor->addDiagnostic("HY091", "Invalid descriptor field identifier"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLGetDescRec(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, SQLCHAR* name, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr, + SQLSMALLINT* typePtr, SQLSMALLINT* subTypePtr, SQLLEN* lengthPtr, + SQLSMALLINT* precisionPtr, SQLSMALLINT* scalePtr, + SQLSMALLINT* nullablePtr) { + if (!descriptorHandle) + return SQL_INVALID_HANDLE; + auto* descriptor = static_cast(descriptorHandle); + if (descriptor->getHandleType() != SQL_HANDLE_DESC) + return SQL_INVALID_HANDLE; + descriptor->clearDiagnostics(); + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW && descriptor->owner && + !descriptor->owner->resultSetPtr) { + descriptor->addDiagnostic("HY007", "Associated statement is not prepared or executed"); + return SQL_ERROR; + } + if (recNumber < 1) { + descriptor->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW && descriptor->owner) + PopulateImplementationRowDescriptor(descriptor->owner); + const DescriptorRecord* record = descriptor->findRecord(recNumber); + if (!record || recNumber > descriptor->count) + return SQL_NO_DATA; + SQLINTEGER nameLength32 = 0; + SQLRETURN result = SQLGetDescField(descriptorHandle, recNumber, SQL_DESC_NAME, name, bufferLength, + &nameLength32); + record = descriptor->findRecord(recNumber); + if (stringLengthPtr) + *stringLengthPtr = static_cast(std::min(nameLength32, SHRT_MAX)); + if (typePtr) + *typePtr = record->type; + if (subTypePtr) + *subTypePtr = record->datetimeIntervalCode; + if (lengthPtr) + *lengthPtr = static_cast(record->length); + if (precisionPtr) + *precisionPtr = record->precision; + if (scalePtr) + *scalePtr = record->scale; + if (nullablePtr) + *nullablePtr = record->nullable; + return result; +} + +SQLRETURN SQL_API SQLGetDiagField(const SQLSMALLINT handleType, const SQLHANDLE handle, + const SQLSMALLINT recNumber, const SQLSMALLINT diagIdentifier, + const SQLPOINTER diagInfoPtr, const SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + if (!handle) + return SQL_INVALID_HANDLE; + auto* odbcHandle = static_cast(handle); + if (odbcHandle->getHandleType() != handleType) + return SQL_INVALID_HANDLE; + if (bufferLength < 0) + return SQL_ERROR; + + if (diagIdentifier == SQL_DIAG_NUMBER) { + if (recNumber != 0) + return SQL_ERROR; + if (diagInfoPtr) + *static_cast(diagInfoPtr) = + static_cast(odbcHandle->diagnosticCount()); + if (stringLengthPtr) + *stringLengthPtr = sizeof(SQLINTEGER); + return SQL_SUCCESS; + } + if (recNumber <= 0) + return SQL_ERROR; + + std::string sqlState; + std::string message; + int nativeError = 0; + if (!odbcHandle->getDiagnostic(recNumber, sqlState, message, nativeError)) + return SQL_NO_DATA; + + switch (diagIdentifier) { + case SQL_DIAG_SQLSTATE: + return setString(sqlState, diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_NATIVE: + if (diagInfoPtr) + *static_cast(diagInfoPtr) = nativeError; + if (stringLengthPtr) + *stringLengthPtr = sizeof(SQLINTEGER); + return SQL_SUCCESS; + case SQL_DIAG_MESSAGE_TEXT: + return setString(message, diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_CLASS_ORIGIN: + case SQL_DIAG_SUBCLASS_ORIGIN: + return setString(sqlState.compare(0, 2, "IM") == 0 || sqlState.compare(0, 2, "HY") == 0 + ? "ODBC 3.0" + : "ISO 9075", + diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_CONNECTION_NAME: + return setString("", diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_SERVER_NAME: + return setString("Apache IoTDB", diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_ROW_NUMBER: + if (diagInfoPtr) + *static_cast(diagInfoPtr) = SQL_NO_ROW_NUMBER; + return SQL_SUCCESS; + case SQL_DIAG_COLUMN_NUMBER: + if (diagInfoPtr) + *static_cast(diagInfoPtr) = SQL_NO_COLUMN_NUMBER; + return SQL_SUCCESS; + default: + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT recNumber, + SQLCHAR* sqlStatePtr, SQLINTEGER* nativeErrorPtr, + SQLCHAR* messageTextPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* textLengthPtr) { + if (!handle) + return SQL_INVALID_HANDLE; + auto* odbcHandle = static_cast(handle); + if (odbcHandle->getHandleType() != handleType) + return SQL_INVALID_HANDLE; + if (recNumber <= 0 || bufferLength < 0) + return SQL_ERROR; + + std::string sqlState; + std::string message; + int nativeError = 0; + if (!odbcHandle->getDiagnostic(recNumber, sqlState, message, nativeError)) + return SQL_NO_DATA; + + if (sqlStatePtr) { + std::memcpy(sqlStatePtr, sqlState.data(), std::min(5, sqlState.size())); + sqlStatePtr[std::min(5, sqlState.size())] = '\0'; + } + if (nativeErrorPtr) + *nativeErrorPtr = nativeError; + if (textLengthPtr) + *textLengthPtr = static_cast( + std::min(message.size(), static_cast(std::numeric_limits::max()))); + + if (!messageTextPtr) + return SQL_SUCCESS; + if (bufferLength == 0) + return message.empty() ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + const size_t copyLength = std::min(message.size(), static_cast(bufferLength - 1)); + std::memcpy(messageTextPtr, message.data(), copyLength); + messageTextPtr[copyLength] = '\0'; + return copyLength < message.size() ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV environmentHandle, SQLINTEGER attribute, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* stringLength) { + if (!environmentHandle) { + logMessage(nullptr, "SQLGetEnvAttr: Invalid environment handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto env = static_cast(environmentHandle); + env->clearDiagnostics(); + if (!value) { + env->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + (void)bufferLength; + switch (attribute) { + case SQL_ATTR_ODBC_VERSION: + *static_cast(value) = env->odbcVersion; + break; + case SQL_ATTR_OUTPUT_NTS: + *static_cast(value) = SQL_TRUE; + break; + default: + env->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } + if (stringLength) + *stringLength = sizeof(SQLINTEGER); + return SQL_SUCCESS; +} + +static bool IsSupportedOdbcFunction(SQLUSMALLINT functionId) { + static const SQLUSMALLINT supportedFunctions[] = { + SQL_API_SQLALLOCCONNECT, SQL_API_SQLALLOCENV, SQL_API_SQLALLOCHANDLE, + SQL_API_SQLALLOCSTMT, SQL_API_SQLBINDCOL, SQL_API_SQLBINDPARAM, + SQL_API_SQLBINDPARAMETER, SQL_API_SQLCANCEL, SQL_API_SQLCLOSECURSOR, + SQL_API_SQLCOLATTRIBUTE, SQL_API_SQLCOLUMNS, SQL_API_SQLCONNECT, + SQL_API_SQLCOPYDESC, SQL_API_SQLDATASOURCES, SQL_API_SQLDESCRIBECOL, + SQL_API_SQLDISCONNECT, SQL_API_SQLDRIVERS, SQL_API_SQLENDTRAN, + SQL_API_SQLERROR, SQL_API_SQLEXECDIRECT, SQL_API_SQLEXECUTE, + SQL_API_SQLFETCH, SQL_API_SQLFETCHSCROLL, SQL_API_SQLFREECONNECT, + SQL_API_SQLFREEENV, SQL_API_SQLFREEHANDLE, SQL_API_SQLFREESTMT, + SQL_API_SQLGETCONNECTATTR, SQL_API_SQLGETCONNECTOPTION, SQL_API_SQLGETCURSORNAME, + SQL_API_SQLGETDATA, SQL_API_SQLGETDESCFIELD, SQL_API_SQLGETDESCREC, + SQL_API_SQLGETDIAGFIELD, SQL_API_SQLGETDIAGREC, SQL_API_SQLGETENVATTR, + SQL_API_SQLGETFUNCTIONS, SQL_API_SQLGETINFO, SQL_API_SQLGETSTMTATTR, + SQL_API_SQLGETSTMTOPTION, SQL_API_SQLGETTYPEINFO, SQL_API_SQLMORERESULTS, + SQL_API_SQLNUMPARAMS, SQL_API_SQLNUMRESULTCOLS, SQL_API_SQLNATIVESQL, + SQL_API_SQLPARAMDATA, SQL_API_SQLPREPARE, SQL_API_SQLPUTDATA, + SQL_API_SQLROWCOUNT, SQL_API_SQLSETCONNECTATTR, SQL_API_SQLSETCONNECTOPTION, + SQL_API_SQLSETCURSORNAME, SQL_API_SQLSETDESCFIELD, SQL_API_SQLSETDESCREC, + SQL_API_SQLSETENVATTR, SQL_API_SQLSETPARAM, SQL_API_SQLSETSTMTATTR, + SQL_API_SQLSETSTMTOPTION, SQL_API_SQLSPECIALCOLUMNS, SQL_API_SQLSTATISTICS, + SQL_API_SQLTABLES, SQL_API_SQLTRANSACT, SQL_API_SQLDRIVERCONNECT}; +#ifdef SQL_API_SQLCANCELHANDLE + if (functionId == SQL_API_SQLCANCELHANDLE) + return true; +#endif + return std::find(std::begin(supportedFunctions), std::end(supportedFunctions), functionId) != + std::end(supportedFunctions); +} + +SQLRETURN SQL_API SQLGetFunctions(SQLHDBC connectionHandle, SQLUSMALLINT functionId, + SQLUSMALLINT* supportedPtr) { + if (!connectionHandle) + return SQL_INVALID_HANDLE; + auto* cnct = static_cast(connectionHandle); + cnct->clearDiagnostics(); + if (!supportedPtr) { + cnct->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + if (functionId == SQL_API_ALL_FUNCTIONS) { + std::fill(supportedPtr, supportedPtr + 100, SQL_FALSE); + for (SQLUSMALLINT id = 0; id < 100; ++id) { + if (IsSupportedOdbcFunction(id)) + supportedPtr[id] = SQL_TRUE; + } + return SQL_SUCCESS; + } + if (functionId == SQL_API_ODBC3_ALL_FUNCTIONS) { + std::fill(supportedPtr, supportedPtr + SQL_API_ODBC3_ALL_FUNCTIONS_SIZE, 0); + for (SQLUSMALLINT id = 0; id < SQL_API_ODBC3_ALL_FUNCTIONS_SIZE * 16; ++id) { + if (IsSupportedOdbcFunction(id)) + supportedPtr[id >> 4] |= static_cast(1U << (id & 0x000F)); + } + return SQL_SUCCESS; + } + + *supportedPtr = IsSupportedOdbcFunction(functionId) ? SQL_TRUE : SQL_FALSE; + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, SQLCHAR* inStatementText, + SQLINTEGER textLength1, SQLCHAR* outStatementText, + SQLINTEGER bufferLength, SQLINTEGER* textLength2Ptr) { + if (!connectionHandle) + return SQL_INVALID_HANDLE; + auto* cnct = static_cast(connectionHandle); + cnct->clearDiagnostics(); + if (!inStatementText) { + cnct->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + if ((textLength1 < 0 && textLength1 != SQL_NTS) || bufferLength < 0) { + cnct->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + + const size_t inputLength = textLength1 == SQL_NTS + ? std::strlen(reinterpret_cast(inStatementText)) + : static_cast(textLength1); + if (textLength2Ptr) { + *textLength2Ptr = static_cast( + std::min(inputLength, static_cast(std::numeric_limits::max()))); + } + if (!outStatementText) + return SQL_SUCCESS; + if (bufferLength == 0) { + cnct->addDiagnostic("01004", "String data, right truncated"); + return inputLength == 0 ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + } + + const size_t copyLength = std::min(inputLength, static_cast(bufferLength - 1)); + std::memcpy(outStatementText, inStatementText, copyLength); + outStatementText[copyLength] = '\0'; + if (copyLength < inputLength) { + cnct->addDiagnostic("01004", "String data, right truncated"); + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; +} + +/* +See the following link for data type mapping: +https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/c-data-types?view=sql-server-ver16 + +SQL_C_USHORT = SQLUSMALLINT = unsigned short int (default) +SQL_C_ULONG = SQLUINTEGER = unsigned long int +SQL_C_SSHORT = SQLSMALLINT = short int +SQL_C_SLONG = SQLINTEGER = long int +*/ +void SQLGetInfoSetNumeric(ConnectionHandle* cnct, std::string name, SQLUINTEGER value, + SQLPOINTER infoValuePtr, SQLSMALLINT valueType, + SQLSMALLINT* stringLengthPtr) { + logMessage(cnct, "SQLGetInfoSetNumeric: name = " + name, LOG_LEVEL_TRACE); + if (!infoValuePtr) { + logMessage(cnct, + "SQLGetInfoSetString: infoValuePtr = nullptr. Application is probing for required " + "buffer size.", + LOG_LEVEL_DEBUG); + if (stringLengthPtr) { + // Return required buffer size in bytes for the numeric type + switch (valueType) { + case SQL_C_USHORT: + *stringLengthPtr = sizeof(SQLUSMALLINT); + break; + case SQL_C_ULONG: + *stringLengthPtr = sizeof(SQLUINTEGER); + break; + case SQL_C_SSHORT: + *stringLengthPtr = sizeof(SQLSMALLINT); + break; + case SQL_C_SLONG: + *stringLengthPtr = sizeof(SQLINTEGER); + break; + default: + logMessage(cnct, "SQLGetInfoSetNumeric: Unsupported valueType during probing", + LOG_LEVEL_ERROR); + *stringLengthPtr = 0; + } + } + return; + } + + switch (valueType) { + case SQL_C_USHORT: + *static_cast(infoValuePtr) = static_cast(value); + break; + + case SQL_C_ULONG: + *static_cast(infoValuePtr) = value; + break; + + case SQL_C_SSHORT: + *static_cast(infoValuePtr) = static_cast(value); + break; + + case SQL_C_SLONG: + *static_cast(infoValuePtr) = static_cast(value); + break; + + default: + logMessage(cnct, "SQLGetInfoSetNumeric: Invalid valueType\n", LOG_LEVEL_TRACE); + } + + if (stringLengthPtr) { + switch (valueType) { + case SQL_C_USHORT: + *stringLengthPtr = sizeof(SQLUSMALLINT); + break; + case SQL_C_ULONG: + *stringLengthPtr = sizeof(SQLUINTEGER); + break; + case SQL_C_SSHORT: + *stringLengthPtr = sizeof(SQLSMALLINT); + break; + case SQL_C_SLONG: + *stringLengthPtr = sizeof(SQLINTEGER); + break; + default: + *stringLengthPtr = 0; + } + } + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::ostringstream oss; + oss << "SQLGetInfoSetNumeric returning " << name << " = " << value; + logMessage(cnct, oss.str(), LOG_LEVEL_TRACE); + } +} + +void SQLGetInfoSetString(ConnectionHandle* cnct, std::string name, std::string value, + SQLPOINTER infoValuePtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + logMessage(cnct, "SQLGetInfoSetString: name = " + name, LOG_LEVEL_TRACE); + + if (!infoValuePtr) { + logMessage(cnct, + "SQLGetInfoSetString: infoValuePtr = nullptr. Application is probing for required " + "buffer size.", + LOG_LEVEL_DEBUG); + if (stringLengthPtr) { + *stringLengthPtr = static_cast( + std::min(value.size(), static_cast(std::numeric_limits::max()))); + } + return; + } + + if (bufferLength <= 0) { + logMessage(cnct, "SQLGetInfoSetString: bufferLength <= 0. Exiting.", LOG_LEVEL_ERROR); + cnct->addDiagnostic(bufferLength < 0 ? "HY090" : "01004", + bufferLength < 0 ? "Invalid string or buffer length" + : "String data, right truncated"); + return; + } + + size_t max_copy = std::min(value.size(), static_cast(bufferLength - 1)); + std::strncpy(static_cast(infoValuePtr), value.data(), max_copy); + static_cast(infoValuePtr)[max_copy] = '\0'; + if (max_copy < value.size()) + cnct->addDiagnostic("01004", "String data, right truncated"); + + if (stringLengthPtr) { + // return the actual length of answer, not the copied length. + *stringLengthPtr = static_cast(value.size()); + } + + logMessage(cnct, "SQLGetInfoSetString returning " + name, LOG_LEVEL_TRACE); +} + +std::string getServerVersion(SQLHDBC connectionHandle) { + ConnectionHandle* cnct = static_cast(connectionHandle); + logMessage(cnct, "getServerVersion: Entering.", LOG_LEVEL_TRACE); + + // Create a temporary statement handle for the query + std::unique_ptr stmt(new StatementHandle(cnct)); + SQLRETURN ret = IoTDB_ExecDirect(stmt.get(), "show version"); + + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + logMessage(cnct, "getServerVersion: Failed to execute show version query", LOG_LEVEL_ERROR); + return "Unknown"; + } + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "getServerVersion: No result set available", LOG_LEVEL_ERROR); + return "Unknown"; + } + + // Get the version string from the first cell + std::string version = stmt->resultSetPtr->getValue(0, 0).toString(); + if (version.empty()) { + logMessage(cnct, "getServerVersion: Empty version string", LOG_LEVEL_WARN); + version = "Unknown"; + } + + logMessage(cnct, "getServerVersion: Retrieved version: " + version, LOG_LEVEL_DEBUG); + + logMessage(cnct, "getServerVersion: Exiting.", LOG_LEVEL_TRACE); + return version; +} + +std::string formatVersion(const std::string& ver) { + // ODBC requires ##.##.####. Accept suffixes such as "-SNAPSHOT" and + // return a stable unknown value instead of throwing through the C ABI. + const int widths[] = {2, 2, 4}; + unsigned int parts[] = {0, 0, 0}; + size_t offset = 0; + bool foundNumber = false; + for (size_t part = 0; part < 3 && offset < ver.size(); ++part) { + size_t end = offset; + unsigned int parsed = 0; + const unsigned int maximum = part < 2 ? 99U : 9999U; + while (end < ver.size() && std::isdigit(static_cast(ver[end]))) { + foundNumber = true; + const unsigned int digit = static_cast(ver[end] - '0'); + parsed = parsed > (maximum - digit) / 10U ? maximum : parsed * 10U + digit; + ++end; + } + parts[part] = parsed; + if (end >= ver.size() || ver[end] != '.') + break; + offset = end + 1; + } + if (!foundNumber) + return "00.00.0000"; + + std::ostringstream oss; + for (size_t i = 0; i < 3; ++i) { + if (i > 0) + oss << "."; + oss << std::setw(widths[i]) << std::setfill('0') << parts[i]; + } + return oss.str(); +} + +SQLRETURN SQL_API SQLGetInfo(SQLHDBC connectionHandle, SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + /* + About this function: + https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgetinfo-function?view=sql-server-ver16 + */ + ConnectionHandle* cnct = static_cast(connectionHandle); + logMessage(cnct, "Entering SQLGetInfo", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "Parameters: " + << "connectionHandle = " << handleToString(connectionHandle) + << ", infoValuePtr = " << infoValuePtr << ", infoType = " << infoType + << ", bufferLength = " << bufferLength << ", stringLengthPtr = " << stringLengthPtr; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + SQLUINTEGER value = 0; + + if (!connectionHandle) { + logMessage(cnct, "SQLGetInfo: Invalid connectionHandle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + cnct->clearDiagnostics(); + + switch (infoType) { + case SQL_DRIVER_NAME: // 6 sqlext.h +#ifdef _WIN32 + SQLGetInfoSetString(cnct, "SQL_DRIVER_NAME", "apache_iotdb_odbc.dll", infoValuePtr, + bufferLength, stringLengthPtr); +#else + SQLGetInfoSetString(cnct, "SQL_DRIVER_NAME", "libapache_iotdb_odbc.so", infoValuePtr, + bufferLength, stringLengthPtr); +#endif + break; + + case SQL_DRIVER_ODBC_VER: // 77 sqlext.h + SQLGetInfoSetString(cnct, "SQL_DRIVER_ODBC_VER", "03.80", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_DRIVER_VER: // 7 sqlext.h + SQLGetInfoSetString(cnct, "SQL_DRIVER_VER", DRIVER_VERSION, infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_DATABASE_NAME: // 16 sqlext.h + SQLGetInfoSetString(cnct, "SQL_DATABASE_NAME", cnct->database, infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_ACTIVE_CONNECTIONS: // 0 sqlext.h (Redefinition of SQL_MAX_DRIVER_CONNECTIONS from sql.h) + // Zero means that the limit is unknown or not defined. + SQLGetInfoSetNumeric(cnct, "SQL_ACTIVE_CONNECTIONS", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_TABLE_NAME_LEN: // 35 sql.h + SQLGetInfoSetNumeric(cnct, "SQL_MAX_TABLE_NAME_LEN", 64, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); // Example value + break; + + case SQL_ACTIVE_STATEMENTS: // 1 sqlext.h (Redefinition of SQL_MAX_CONCURRENT_ACTIVITIES from swl.h) + SQLGetInfoSetNumeric(cnct, "SQL_ACTIVE_STATEMENTS", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + +// iODBC only seems to support ODBC 3.0 +#ifdef SQL_OV_ODBC3_80 + case SQL_ASYNC_DBC_FUNCTIONS: // 10023 sqlext.h + SQLGetInfoSetNumeric(cnct, "SQL_ASYNC_DBC_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + // Most drivers do not support this + break; + + case SQL_ASYNC_NOTIFICATION: // 10025 sqlext.h + SQLGetInfoSetNumeric(cnct, "SQL_ASYNC_NOTIFICATION", SQL_ASYNC_NOTIFICATION_NOT_CAPABLE, + infoValuePtr, SQL_C_USHORT, stringLengthPtr); + // Default for simpler drivers + break; +#endif + + case SQL_CURSOR_COMMIT_BEHAVIOR: // 23 + SQLGetInfoSetNumeric(cnct, "SQL_CURSOR_COMMIT_BEHAVIOR", SQL_CB_CLOSE, infoValuePtr, + SQL_C_USHORT, stringLengthPtr); + // Specifies what happens to cursors on a commit. + // Possible values: + // SQL_CB_CLOSE: Cursors are closed on commit. + // SQL_CB_DELETE: Cursors are closed and rows are deleted on commit. + // SQL_CB_PRESERVE: Cursors remain open on commit. + break; + + case SQL_CURSOR_ROLLBACK_BEHAVIOR: // 24 + SQLGetInfoSetNumeric(cnct, "SQL_CURSOR_ROLLBACK_BEHAVIOR", SQL_CB_CLOSE, infoValuePtr, + SQL_C_USHORT, stringLengthPtr); + // Specifies what happens to cursors on a rollback. + // Possible values: + // SQL_CB_CLOSE: Cursors are closed on rollback. + // SQL_CB_DELETE: Cursors are closed and rows are deleted on rollback. + // SQL_CB_PRESERVE: Cursors remain open on rollback. + break; + + case SQL_GETDATA_EXTENSIONS: // 81 + SQLGetInfoSetNumeric(cnct, "SQL_GETDATA_EXTENSIONS", SQL_GD_ANY_COLUMN | SQL_GD_ANY_ORDER, + infoValuePtr, SQL_C_ULONG, stringLengthPtr); + // Indicates which `SQLGetData` features are supported. + // Possible values (bitmask): + // SQL_GD_BLOCK: Allows block fetch with SQLGetData. + // SQL_GD_BOUND: Allows data to be retrieved to bound columns and buffers. + // SQL_GD_ANY_COLUMN: Allows retrieval of data from any column. + // SQL_GD_ANY_ORDER: Allows retrieval of data in any order of columns. + break; + + case SQL_DTC_TRANSITION_COST: // 1750 + SQLGetInfoSetNumeric(cnct, "SQL_DTC_TRANSITION_COST", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + // Cost (in milliseconds) for a Distributed Transaction Coordinator (DTC) transaction transition. + // This value should be adjusted based on the driver and system's transaction coordination cost. + break; + + case SQL_DESCRIBE_PARAMETER: // 10002 + SQLGetInfoSetString(cnct, "SQL_DESCRIBE_PARAMETER", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_DBMS_NAME: // 17 + // Return the name of the data management system. Returned Apache IoTDB + SQLGetInfoSetString(cnct, "SQL_DBMS_NAME", "Apache IoTDB", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_DBMS_VER: // 18 + { + // Query and normalize the connected server's version. + std::string versionStr = getServerVersion(connectionHandle); + versionStr = formatVersion(versionStr); + SQLGetInfoSetString(cnct, "SQL_DBMS_VER", versionStr, infoValuePtr, bufferLength, + stringLengthPtr); + break; + } + + case SQL_IDENTIFIER_QUOTE_CHAR: // 29 + // Identifier quote character. For IoTDB tree model this character is backtick (`), for table model it's " + if (cnct->isTableModel) { + SQLGetInfoSetString(cnct, "SQL_IDENTIFIER_QUOTE_CHAR", "\"", infoValuePtr, bufferLength, + stringLengthPtr); + } else { + SQLGetInfoSetString(cnct, "SQL_IDENTIFIER_QUOTE_CHAR", "`", infoValuePtr, bufferLength, + stringLengthPtr); + } + break; + + case SQL_CATALOG_NAME: //10003 + /* + If the server supports catalog names, returns "Y"; if not, returns "N". + Drivers conforming to SQL-92 Full level standard will always return "Y". + */ + SQLGetInfoSetString(cnct, "SQL_CATALOG_NAME", cnct->isTableModel ? "Y" : "N", infoValuePtr, + bufferLength, stringLengthPtr); + break; + + case SQL_SCHEMA_USAGE: // 91 + // IoTDB exposes no separate schema component through this driver. + SQLGetInfoSetNumeric(cnct, "SQL_SCHEMA_USAGE", 0, infoValuePtr, SQL_C_ULONG, stringLengthPtr); + break; + /* + This attribute is a byte mask. + SQL_SU_DML_STATEMENTS mode can be used for all Data Manipulation Language (DML) statements: + • SELECT, INSERT, UPDATE, DELETE + • If supported, also includes SELECT FOR UPDATE and positioned update/delete statements. + + SQL_SU_PROCEDURE_INVOCATION mode can be used for ODBC stored procedure call statements (such as {CALL schema.proc_name}). + + SQL_SU_TABLE_DEFINITION mode can be used for all table definition statements: + • CREATE TABLE, CREATE VIEW + • ALTER TABLE, DROP TABLE, DROP VIEW. + + SQL_SU_INDEX_DEFINITION mode can be used for all index definition statements: + • CREATE INDEX, DROP INDEX. + + SQL_SU_PRIVILEGE_DEFINITION mode can be used for all privilege definition statements: + • GRANT, REVOKE. + + */ + + case SQL_CATALOG_USAGE: // 92 + // Table-model databases occupy the ODBC catalog component. Tree model has no catalog. + value = cnct->isTableModel ? SQL_SU_DML_STATEMENTS | SQL_SU_TABLE_DEFINITION : 0; + SQLGetInfoSetNumeric(cnct, "SQL_CATALOG_USAGE", value, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_CATALOG_NAME_SEPARATOR: // 41 + // Returns the character or string that the data source uses to separate catalog names from subsequent or preceding qualified name elements. + SQLGetInfoSetString(cnct, "SQL_CATALOG_NAME_SEPARATOR", cnct->isTableModel ? "." : "", + infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_CATALOG_LOCATION: // 114 + // This value indicates the position of the catalog name in the full table name: + // SQL_CL_START Catalog name is at the beginning of the table name (like file system path style) + SQLGetInfoSetNumeric(cnct, "SQL_CATALOG_LOCATION", cnct->isTableModel ? SQL_CL_START : 0, + infoValuePtr, SQL_C_USHORT, stringLengthPtr); + break; + + case SQL_SQL_CONFORMANCE: + // IoTDB SQL is model-specific; do not claim a complete SQL-92 conformance level. + SQLGetInfoSetNumeric(cnct, "SQL_SQL_CONFORMANCE", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + /* + This value is used to identify the SQL standard level supported by the driver, mainly including the following four compliance levels: + + Return Value Description + SQL_SC_SQL92_ENTRY Conforms to SQL-92 Entry Level standard + SQL_SC_SQL92_INTERMEDIATE Conforms to SQL-92 Intermediate Level standard + SQL_SC_SQL92_FULL Conforms to SQL-92 Full Level standard + SQL_SC_FIPS127_2_TRANSITIONAL Conforms to FIPS 127-2 Transitional Level standard + */ + + case SQL_MAX_COLUMNS_IN_ORDER_BY: //99 + // This value specifies the maximum number of columns allowed in ORDER BY clause + SQLGetInfoSetNumeric(cnct, "SQL_MAX_COLUMNS_IN_ORDER_BY", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_IDENTIFIER_LEN: // 10005 + // Zero means no driver-enforced limit is known. + SQLGetInfoSetNumeric(cnct, "SQL_MAX_IDENTIFIER_LEN", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_COLUMNS_IN_GROUP_BY: // 97 + // This value specifies the maximum number of columns allowed in a GROUP BY clause + SQLGetInfoSetNumeric(cnct, "SQL_MAX_COLUMNS_IN_GROUP_BY", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_COLUMNS_IN_SELECT: // 100 + // This attribute specifies the maximum number of columns that can be returned in a SELECT statement + SQLGetInfoSetNumeric(cnct, "SQL_MAX_COLUMNS_IN_SELECT", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_ORDER_BY_COLUMNS_IN_SELECT: // 90 + // "Y" if columns in ORDER BY clause must be in select list; otherwise "N". + SQLGetInfoSetString(cnct, "SQL_ORDER_BY_COLUMNS_IN_SELECT", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_STRING_FUNCTIONS: // 50 + // ODBC scalar escape translation ({fn ...}) is not implemented. + SQLGetInfoSetNumeric(cnct, "SQL_STRING_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_AGGREGATE_FUNCTIONS: // 169 + SQLGetInfoSetNumeric(cnct, "SQL_AGGREGATE_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SQL92_PREDICATES: // 160 + // No portable SQL-92 predicate subset is advertised. + SQLGetInfoSetNumeric(cnct, "SQL_SQL92_PREDICATES", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SQL92_RELATIONAL_JOIN_OPERATORS: // 161 + // Relational join operators supported in SELECT statements + SQLGetInfoSetNumeric(cnct, "SQL_SQL92_RELATIONAL_JOIN_OPERATORS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SQL92_VALUE_EXPRESSIONS: // 165 + SQLGetInfoSetNumeric(cnct, "SQL_SQL92_VALUE_EXPRESSIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_COLUMN_ALIAS: // 87 + SQLGetInfoSetString(cnct, "SQL_COLUMN_ALIAS", "Y", infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_GROUP_BY: // 88 + // The portable ODBC GROUP BY grammar is not advertised. + SQLGetInfoSetNumeric(cnct, "SQL_GROUP_BY", SQL_GB_NOT_SUPPORTED, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_NUMERIC_FUNCTIONS: // 49 + SQLGetInfoSetNumeric(cnct, "SQL_NUMERIC_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_TIMEDATE_FUNCTIONS: // 52 + SQLGetInfoSetNumeric(cnct, "SQL_TIMEDATE_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SYSTEM_FUNCTIONS: // 51 + SQLGetInfoSetNumeric(cnct, "SQL_SYSTEM_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_TIMEDATE_ADD_INTERVALS: + // ODBC TIMESTAMPADD escape intervals are not translated. + + SQLGetInfoSetNumeric(cnct, "SQL_TIMEDATE_ADD_INTERVALS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_TIMEDATE_DIFF_INTERVALS: + // ODBC TIMESTAMPDIFF escape intervals are not translated. + SQLGetInfoSetNumeric(cnct, "SQL_TIMEDATE_DIFF_INTERVALS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_CONCAT_NULL_BEHAVIOR: + // Indicates how the data source handles concatenation of NULL valued character data type columns with non-NULL valued character data type columns. + // SQL_CB_NULL = Result is NULL value. + // SQL_CB_NON_NULL = Result is concatenation of non-NULL valued column or columns. + // Drivers conforming to SQL-92 Entry Level standard will always return SQL_CB_NULL. + SQLGetInfoSetNumeric(cnct, "SQL_CONCAT_NULL_BEHAVIOR", SQL_CB_NULL, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_CATALOG_TERM: + // A character string that gives the name that the data source vendor uses for catalogs, such as "database" or "directory". This string can be upper case, lower case, or mixed case. + SQLGetInfoSetString(cnct, "SQL_CATALOG_TERM", cnct->isTableModel ? "database" : "", + infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_SCHEMA_TERM: + // A character string that gives the name that the data source vendor uses for schemas, which can be upper case, lower case, or mixed case. + // Returns an empty string if the data source does not support schemas. + SQLGetInfoSetString(cnct, "SQL_SCHEMA_TERM", "", infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_ODBC_INTERFACE_CONFORMANCE: + // A SQLUINTEGER value that indicates the level of ODBC 3.x interface conformance of the driver. + /* + SQL_OIC_CORE: The minimum level that all ODBC drivers should conform to. This level includes basic interface elements such as connection functions, functions for preparing and executing SQL statements, basic result set metadata functions, basic catalog functions, etc. + SQL_OIC_LEVEL1: Includes core standard compliance level features, plus scrollable cursors, bookmarks, positioned updates and deletes, etc. + SQL_OIC_LEVEL2: Includes Level 1 standard compliance level features, plus advanced features such as sensitive cursors; update, delete and refresh by bookmarks; stored procedure support; catalog functions for primary and foreign keys; multi-catalog support, etc. + */ + SQLGetInfoSetNumeric(cnct, "SQL_ODBC_INTERFACE_CONFORMANCE", SQL_OIC_CORE, infoValuePtr, + SQL_C_USHORT, stringLengthPtr); + break; + + case SQL_SEARCH_PATTERN_ESCAPE: + /* + A character string that specifies the escape character supported by the driver, which allows using pattern matching metacharacters underscore (_) and percent (%) as literal characters in search patterns. + This escape character only applies to catalog function parameters that support search strings. If this string is empty, the driver does not support search pattern escape characters. + */ + SQLGetInfoSetString(cnct, "SQL_SEARCH_PATTERN_ESCAPE", "\\", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_CONVERT_FUNCTIONS: + /* + A SQLUINTEGER bitmask that enumerates the scalar conversion functions supported by the driver and associated data source. + The following bitmasks are used to determine which conversion functions are supported: + SQL_FN_CVT_CAST + SQL_FN_CVT_CONVERT + */ + SQLGetInfoSetNumeric(cnct, "SQL_CONVERT_FUNCTIONS", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_CONVERT_BIGINT: + case SQL_CONVERT_BINARY: + case SQL_CONVERT_BIT: + case SQL_CONVERT_CHAR: + case SQL_CONVERT_DATE: + case SQL_CONVERT_DECIMAL: + case SQL_CONVERT_DOUBLE: + case SQL_CONVERT_FLOAT: + case SQL_CONVERT_GUID: + case SQL_CONVERT_INTEGER: + case SQL_CONVERT_LONGVARCHAR: + case SQL_CONVERT_NUMERIC: + case SQL_CONVERT_REAL: + case SQL_CONVERT_SMALLINT: + case SQL_CONVERT_TIME: + case SQL_CONVERT_TIMESTAMP: + case SQL_CONVERT_TINYINT: + case SQL_CONVERT_VARBINARY: + case SQL_CONVERT_VARCHAR: + case SQL_CONVERT_LONGVARBINARY: + /* + This series of attributes identifies the data type conversion capabilities supported by the data source through bitmasks, used for the CONVERT() scalar function. Each SQL_CONVERT_ corresponds to conversion support for a specific data type. + + The mask content is composed of multiple SQL_CVT_ ORed together. + + No ODBC CONVERT escape conversions are advertised. + */ + SQLGetInfoSetNumeric(cnct, "SQL_CONVERT_xxx", 0, infoValuePtr, SQL_C_ULONG, stringLengthPtr); + break; + + case SQL_CONVERT_WCHAR: + case SQL_CONVERT_WLONGVARCHAR: + case SQL_CONVERT_WVARCHAR: + /* + These three seem to be in the same series as the above. The corresponding SQL_CVT_ also exists. + But these three things cannot be found in the official documentation. These macros can only be found in header files. + */ + SQLGetInfoSetNumeric(cnct, "SQL_CONVERT_xxx_wide", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SPECIAL_CHARACTERS: + /* A string containing all special characters that can be included in identifiers + besides letters, numbers, and underscores. Returns empty string if none. + */ + SQLGetInfoSetString(cnct, "SQL_SPECIAL_CHARACTERS", "", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + /******** + * The following parameters have been deprecated in ODBC 3.0, + * but Excel uses ODBC 2.0, so support is needed for Excel compatibility ********/ + + case SQL_POS_OPERATIONS: // 79 + // Bitmask indicating the types of operations supported by the data source through the SQLSetPos function. + // SQLSetPos is not supported, so no positioned operations are advertised. + SQLGetInfoSetNumeric(cnct, "SQL_POS_OPERATIONS", 0, infoValuePtr, SQL_C_SLONG, stringLengthPtr); + break; + + case SQL_STATIC_SENSITIVITY: // 83 + // Indicates whether the application can detect changes made to static or keyset-driven cursors by SQLSetPos or positioned update/delete statements + // Static cursors and SQLSetPos are not supported. + SQLGetInfoSetNumeric(cnct, "SQL_STATIC_SENSITIVITY", 0, infoValuePtr, SQL_C_SLONG, + stringLengthPtr); + break; + + case SQL_LOCK_TYPES: // 78 + // Bitmask enumerating the lock types supported in the fLock argument of SQLSetPos function + // SQLSetPos lock modes are not supported. + SQLGetInfoSetNumeric(cnct, "SQL_LOCK_TYPES", 0, infoValuePtr, SQL_C_SLONG, stringLengthPtr); + break; + + case SQL_TXN_ISOLATION_OPTION: // 72。 + // Bitmask indicating the transaction isolation levels supported by the driver and data source. Each bit corresponds to a specific isolation level + // IoTDB has no transaction concept + SQLGetInfoSetNumeric(cnct, "SQL_TXN_ISOLATION_OPTION", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_BOOKMARK_PERSISTENCE: // 82 + // Bitmask enumerating the operations in which bookmarks remain valid + SQLGetInfoSetNumeric(cnct, "SQL_BOOKMARK_PERSISTENCE", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SCROLL_OPTIONS: // 44: + // Bitmask enumerating the scroll options supported for scrollable cursors. + SQLGetInfoSetNumeric(cnct, "SQL_SCROLL_OPTIONS", SQL_SO_FORWARD_ONLY, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_SCROLL_CONCURRENCY: // 43 + // Bitmask enumerating the concurrency control options supported by cursors + SQLGetInfoSetNumeric(cnct, "SQL_SCROLL_CONCURRENCY", SQL_SCCO_READ_ONLY, infoValuePtr, + SQL_C_SLONG, stringLengthPtr); + break; + + case SQL_DYNAMIC_CURSOR_ATTRIBUTES1: + // Bitmask describing the attributes of dynamic cursors supported by the driver. + SQLGetInfoSetNumeric(cnct, "SQL_DYNAMIC_CURSOR_ATTRIBUTES1", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_KEYSET_CURSOR_ATTRIBUTES1: + // Bitmask describing the attributes of keyset-driven cursors supported by the driver. This bitmask contains the first set of attributes; see SQL_KEYSET_CURSOR_ATTRIBUTES2 for the second set. + SQLGetInfoSetNumeric(cnct, "SQL_KEYSET_CURSOR_ATTRIBUTES1", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_STATIC_CURSOR_ATTRIBUTES1: + // Bitmask describing the attributes of static cursors supported by the driver. This bitmask contains the first set of attributes; see SQL_STATIC_CURSOR_ATTRIBUTES2 for the second set. + SQLGetInfoSetNumeric(cnct, "SQL_STATIC_CURSOR_ATTRIBUTES1", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1: // 146 + // Bitmask describing the attributes of forward-only cursors supported by the driver. This bitmask contains the first set of attributes; see SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2 for the second set. + SQLGetInfoSetNumeric(cnct, "SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1", SQL_CA1_NEXT, infoValuePtr, + SQL_C_ULONG, stringLengthPtr); + break; + + case SQL_KEYSET_CURSOR_ATTRIBUTES2: + // Bitmask describing the attributes of keyset-driven cursors supported by the driver. This bitmask contains the second set of attributes; see SQL_KEYSET_CURSOR_ATTRIBUTES1 for the first set. + SQLGetInfoSetNumeric(cnct, "SQL_KEYSET_CURSOR_ATTRIBUTES2", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_STATIC_CURSOR_ATTRIBUTES2: + // Bitmask describing the attributes of static cursors supported by the driver. This bitmask contains the second set of attributes; see SQL_STATIC_CURSOR_ATTRIBUTES1 for the first set. + SQLGetInfoSetNumeric(cnct, "SQL_STATIC_CURSOR_ATTRIBUTES2", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_NEED_LONG_DATA_LEN: + // A character string: "Y" if the data source needs to have the length of long data values (data type SQL_LONGVARCHAR, SQL_LONGVARBINARY, or a data source-specific long data type) before sending them to the data source; "N" if it does not. + SQLGetInfoSetString(cnct, "SQL_NEED_LONG_DATA_LEN", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_TXN_CAPABLE: + // A SQLUSMALLINT value describing transaction support in the driver or data source + SQLGetInfoSetNumeric(cnct, "SQL_TXN_CAPABLE", SQL_TC_NONE, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); //iotdb has no transactions + break; + + case SQL_NON_NULLABLE_COLUMNS: + // A SQLUSMALLINT value specifying whether the data source supports NOT NULL constraints in columns + SQLGetInfoSetNumeric(cnct, "SQL_NON_NULLABLE_COLUMNS", SQL_NNC_NON_NULL, infoValuePtr, + SQL_C_USHORT, stringLengthPtr); //iotdb has no transactions + break; + + case SQL_DATA_SOURCE_NAME: + // A character string representing the data source name used during connection. If the application called SQLConnect, this is the value of the szDSN parameter. If the application called SQLDriverConnect or SQLBrowseConnect, this is the value of the DSN keyword in the connection string passed to the driver. If the connection string does not contain a DSN keyword (for example, when it contains a DRIVER keyword), this is an empty string. + SQLGetInfoSetString(cnct, "SQL_DATA_SOURCE_NAME", cnct->dataSourceName, infoValuePtr, + bufferLength, stringLengthPtr); + break; + + case SQL_DATA_SOURCE_READ_ONLY: + // A character string. "Y" if the data source is set to read-only mode; "N" otherwise. + // This characteristic is only related to the data source itself, not the driver used to access the data source. A driver that supports read-write operations can be used with a read-only data source. If a driver is read-only, all its data sources must be read-only and must return SQL_DATA_SOURCE_READ_ONLY. + SQLGetInfoSetString(cnct, "SQL_DATA_SOURCE_READ_ONLY", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_IDENTIFIER_CASE: + // A SQLUSMALLINT value indicating case sensitivity. + /* + SQL_IC_UPPER = Identifiers in SQL are case-insensitive and stored in uppercase in system catalogs. + SQL_IC_LOWER = Identifiers in SQL are case-insensitive and stored in lowercase in system catalogs. + SQL_IC_SENSITIVE = Identifiers in SQL are case-sensitive and stored in mixed case in system catalogs. + SQL_IC_MIXED = Identifiers in SQL are case-insensitive and stored in mixed case in system catalogs. + */ + SQLGetInfoSetNumeric(cnct, "SQL_IDENTIFIER_CASE", SQL_IC_SENSITIVE, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_INDEX_SIZE: + // A SQLUINTEGER value specifying the maximum number of bytes allowed in the combined fields of an index. This value is set to zero if no limit is specified or the limit is unknown. + SQLGetInfoSetNumeric(cnct, "SQL_MAX_INDEX_SIZE", 0, infoValuePtr, SQL_C_ULONG, stringLengthPtr); + break; + + case SQL_MAX_ROW_SIZE: + /* + A SQLUINTEGER value that specifies the maximum length of a single row in a table. If no limit is specified or the limit is unknown, this value is set to zero. + Drivers conforming to FIPS Entry level standards will return at least 2,000. Drivers conforming to FIPS Intermediate level standards will return at least 8,000. + */ + SQLGetInfoSetNumeric(cnct, "SQL_MAX_ROW_SIZE", 0, infoValuePtr, SQL_C_ULONG, stringLengthPtr); + break; + + case SQL_MAX_ROW_SIZE_INCLUDES_LONG: + /* + A character string: "Y" if the returned SQL_MAX_ROW_SIZE maximum row size information includes the length of all SQL_LONGVARCHAR and SQL_LONGVARBINARY type columns in the row; otherwise "N". */ + SQLGetInfoSetString(cnct, "SQL_MAX_ROW_SIZE_INCLUDES_LONG", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_MAX_TABLES_IN_SELECT: + /* + A SQLUSMALLINT value that specifies the maximum number of tables allowed in the FROM clause of a SELECT statement. If no limit is specified or the limit is unknown, this value is set to zero. + Drivers conforming to FIPS Entry level standards will return at least 15. Drivers conforming to FIPS Intermediate level standards will return at least 50. + */ + SQLGetInfoSetNumeric(cnct, "SQL_MAX_TABLES_IN_SELECT", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_NULL_COLLATION: + /*A SQLUSMALLINT value that specifies the sort position of NULL values in result sets. + The options do not consider adjustable NULL value placement, so we can only choose to place them at the end based on IoTDB's default behavior. + */ + SQLGetInfoSetNumeric(cnct, "SQL_NULL_COLLATION", SQL_NC_END, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_PROCEDURE_TERM: + // A character string giving the name that the data source vendor uses for "procedure". + // IoTDB exposes no stored procedures through this driver. + SQLGetInfoSetString(cnct, "SQL_PROCEDURE_TERM", "", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_QUOTED_IDENTIFIER_CASE: + // A SQLUSMALLINT value describing the case sensitivity and storage of quoted identifiers in SQL + // In SQL-92 standard they are case sensitive, but IoTDB seems not to be sensitive. + SQLGetInfoSetNumeric(cnct, "SQL_QUOTED_IDENTIFIER_CASE", SQL_IC_SENSITIVE, infoValuePtr, + SQL_C_USHORT, stringLengthPtr); + break; + + case SQL_ODBC_SQL_CONFORMANCE: + // The driver accepts the ODBC minimum grammar used by interoperable applications. + SQLGetInfoSetNumeric(cnct, "SQL_ODBC_SQL_CONFORMANCE", SQL_OSC_MINIMUM, infoValuePtr, + SQL_C_SSHORT, stringLengthPtr); + break; + + case SQL_INTEGRITY: + // A string: "Y" if the data source supports Integrity Enhancement Facility; "N" if it does not. + // Includes entity integrity, referential integrity, domain integrity, user-defined integrity. + SQLGetInfoSetString(cnct, "SQL_INTEGRITY", "N", infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_SUBQUERIES: + // A SQLUINTEGER bitmask enumerating the types of predicates supported in subqueries + // Drivers conforming to SQL-92 Entry Level standard always return a bitmask with all these bits set. + // However, IoTDB does not support correlated subqueries. + + SQLGetInfoSetNumeric(cnct, "SQL_SUBQUERIES", 0, infoValuePtr, SQL_C_ULONG, stringLengthPtr); + break; + + case SQL_TABLE_TERM: // 45 + // The keyword in SQL that represents "table". Drivers conforming to SQL-92 Entry Level standard will always return "table". + SQLGetInfoSetString(cnct, "SQL_TABLE_TERM", "table", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_USER_NAME: + SQLGetInfoSetString(cnct, "SQL_USER_NAME", cnct->userName.c_str(), infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_MULT_RESULT_SETS: + // A character string: "Y" if the data source supports multiple result sets; "N" if it does not. + SQLGetInfoSetString(cnct, "SQL_MULT_RESULT_SETS", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_SERVER_NAME: + // A character string representing the server name as determined by the actual data source; this information is useful when connecting using SQLConnect, SQLDriverConnect, and SQLBrowseConnect when a data source name is used. + // Report the connected endpoint as the server name. + SQLGetInfoSetString(cnct, "SQL_SERVER_NAME", cnct->serverHostName, infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_ACCESSIBLE_PROCEDURES: + // A character string: "Y" if the user can execute all procedures returned by SQLProcedures; "N" if there may be procedures the user cannot execute. + SQLGetInfoSetString(cnct, "SQL_ACCESSIBLE_PROCEDURES", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_ACCESSIBLE_TABLES: // 19 + // A character string: "Y" if the user has SELECT privileges on all tables returned by SQLTables; "N" if there may be tables the user cannot access. + SQLGetInfoSetString(cnct, "SQL_ACCESSIBLE_TABLES", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_OJ_CAPABILITIES: // 115 + // A SQLUINTEGER bitmask enumerating the types of outer joins supported by the driver and data source. + SQLGetInfoSetNumeric(cnct, "SQL_OJ_CAPABILITIES", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_PROCEDURES: // 21 + // A character string: "Y" if the data source supports stored procedures and the driver supports ODBC procedure call syntax; "N" otherwise. + SQLGetInfoSetString(cnct, "SQL_PROCEDURES", "N", infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_LIKE_ESCAPE_CLAUSE: // 113 + // A character string: "Y" if the data source supports defining escape characters for percent (%) and underscore (_) characters in LIKE predicates and the driver supports ODBC-defined LIKE predicate escape character syntax; "N" otherwise. + // IoTDB does not support LIKE. + SQLGetInfoSetString(cnct, "SQL_LIKE_ESCAPE_CLAUSE", "N", infoValuePtr, bufferLength, + stringLengthPtr); + break; + + case SQL_MAX_COLUMNS_IN_INDEX: // 98 + // A SQLUSMALLINT value specifying the maximum number of columns allowed in an index. This value is set to zero if no limit is specified or the limit is unknown. + SQLGetInfoSetNumeric(cnct, "SQL_MAX_COLUMNS_IN_INDEX", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_COLUMNS_IN_TABLE: // 101 + // A SQLUSMALLINT value specifying the maximum number of columns allowed in a table. This value is set to zero if no limit is specified or the limit is unknown. + SQLGetInfoSetNumeric(cnct, "SQL_MAX_COLUMNS_IN_TABLE", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_OUTER_JOINS: // 38 + SQLGetInfoSetString(cnct, "SQL_OUTER_JOINS", "N", infoValuePtr, bufferLength, stringLengthPtr); + break; + + case SQL_FILE_USAGE: // 84 + // A SQLUSMALLINT value that indicates how a single-tier driver directly handles files in the data source + // The driver is not a single-tier driver. + SQLGetInfoSetNumeric(cnct, "SQL_FILE_USAGE", SQL_FILE_NOT_SUPPORTED, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_DEFAULT_TXN_ISOLATION: // 26 + // A SQLUINTEGER value that indicates the default transaction isolation level supported by the driver or data source, returns zero if the data source does not support transactions. + SQLGetInfoSetNumeric(cnct, "SQL_DEFAULT_TXN_ISOLATION", 0, infoValuePtr, SQL_C_ULONG, + stringLengthPtr); + break; + + case SQL_MAX_OWNER_NAME_LEN: + /* + An SQLUSMALLINT value that specifies the maximum length of a schema name in the data source. If there is no maximum length or the length is unknown, this value is set to zero. + + An FIPS Entry level-conformant driver will return at least 18. An FIPS Intermediate level-conformant driver will return at least 128. + + This InfoType has been renamed for ODBC 3.0 from the ODBC 2.0 InfoType SQL_MAX_OWNER_NAME_LEN. + + there's no schema in IoTDB. the "Database" = "catalog". returning 0. + */ + SQLGetInfoSetNumeric(cnct, "SQL_MAX_OWNER_NAME_LEN", 0, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + case SQL_MAX_CATALOG_NAME_LEN: + SQLGetInfoSetNumeric(cnct, "SQL_MAX_CATALOG_NAME_LEN", 64, infoValuePtr, SQL_C_USHORT, + stringLengthPtr); + break; + + default: + logMessage(cnct, "SQLGetInfo: received unsupported InfoType:" + std::to_string(infoType) + "\n", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY096", "Unsupported information type"); + return SQL_ERROR; + } + + logMessage(cnct, "SQLGetInfo: Exiting\n", LOG_LEVEL_TRACE); + if (cnct->diagnosticCount() == 0) + return SQL_SUCCESS; + std::string state; + std::string message; + int nativeError = 0; + cnct->getDiagnostic(1, state, message, nativeError); + return state == "01004" ? SQL_SUCCESS_WITH_INFO : SQL_ERROR; +} + +SQLRETURN SQL_API SQLGetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* stringLength) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLGetStmtAttr: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLGetStmtAttr parameters: " + << "statementHandle = " << handleToString(statementHandle) + << ", attribute = " << attribute << ", value = " << valueToString(value) + << ", bufferLength = " << bufferLength << ", stringLength = " << stringLength; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLAllocHandle we're allocating a NULL_Handle, so I disabled this. + if (!statementHandle) { + logMessage(cnct, "SQLGetStmtAttr: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + + const auto* ard = static_cast(stmt->appRowDesc); + const auto* apd = static_cast(stmt->appParamDesc); + const auto* ird = static_cast(stmt->impRowDesc); + const auto* ipd = static_cast(stmt->impParamDesc); + stmt->rowArraySize = ard->arraySize; + stmt->rowBindType = ard->bindType; + stmt->rowBindOffsetPtr = reinterpret_cast(ard->bindOffsetPtr); + stmt->rowsFetchedPtr = ird->rowsProcessedPtr; + stmt->rowStatusPtr = ird->arrayStatusPtr; + stmt->paramSetSize = apd->arraySize; + stmt->paramBindType = apd->bindType; + stmt->paramBindOffsetPtr = reinterpret_cast(apd->bindOffsetPtr); + stmt->paramsProcessedPtr = ipd->rowsProcessedPtr; + stmt->paramStatusPtr = ipd->arrayStatusPtr; + + switch (attribute) { + case SQL_ATTR_APP_ROW_DESC: + // Return descriptor handle if applicable + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_APP_ROW_DESC", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = + stmt->appRowDesc; // Replace with actual descriptor handle if available + logMessage(cnct, "SQLGetStmtAttr: Returning application row descriptor handle", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_APP_PARAM_DESC: + // Return descriptor handle if applicable + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_APP_PARAM_DESC", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = stmt->appParamDesc; + logMessage(cnct, "SQLGetStmtAttr: Returning application parameter descriptor handle", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_IMP_ROW_DESC: + // Return descriptor handle if applicable + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_IMP_ROW_DESC", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = + stmt->impRowDesc; // Replace with actual descriptor handle if available + logMessage(cnct, "SQLGetStmtAttr: Returning implementation row descriptor handle", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_IMP_PARAM_DESC: + // Return descriptor handle if applicable + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_IMP_PARAM_DESC", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = stmt->impParamDesc; + logMessage(cnct, "SQLGetStmtAttr: Returning implementation parameter descriptor handle", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_CURSOR_SCROLLABLE: + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_CURSOR_SCROLLABLE", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = SQL_NONSCROLLABLE; // Example: default to non-scrollable + logMessage(cnct, "SQLGetStmtAttr: Cursor scrollable set to SQL_NONSCROLLABLE", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_CURSOR_SENSITIVITY: + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_CURSOR_SENSITIVITY", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = SQL_UNSPECIFIED; // Example: unspecified sensitivity + logMessage(cnct, "SQLGetStmtAttr: Cursor sensitivity set to SQL_UNSPECIFIED", + LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_ROW_BIND_TYPE: + logMessage(cnct, "SQLGetStmtAttr: Processing SQL_ATTR_ROW_BIND_TYPE", LOG_LEVEL_DEBUG); + if (value) { + *static_cast(value) = stmt->rowBindType; + logMessage(cnct, "SQLGetStmtAttr: Row bind type set to SQL_BIND_BY_COLUMN", LOG_LEVEL_DEBUG); + } + break; + + case SQL_ATTR_ROW_ARRAY_SIZE: + if (value) + *static_cast(value) = stmt->rowArraySize; + break; + case SQL_ATTR_ROWS_FETCHED_PTR: + if (value) + *static_cast(value) = stmt->rowsFetchedPtr; + break; + case SQL_ATTR_ROW_STATUS_PTR: + if (value) + *static_cast(value) = stmt->rowStatusPtr; + break; + case SQL_ATTR_ROW_BIND_OFFSET_PTR: + if (value) + *static_cast(value) = stmt->rowBindOffsetPtr; + break; + case SQL_ATTR_PARAM_STATUS_PTR: + if (value) + *static_cast(value) = stmt->paramStatusPtr; + break; + case SQL_ATTR_PARAMS_PROCESSED_PTR: + if (value) + *static_cast(value) = stmt->paramsProcessedPtr; + break; + case SQL_ATTR_PARAMSET_SIZE: + if (value) + *static_cast(value) = stmt->paramSetSize; + break; + case SQL_ATTR_PARAM_BIND_TYPE: + if (value) + *static_cast(value) = stmt->paramBindType; + break; + case SQL_ATTR_PARAM_BIND_OFFSET_PTR: + if (value) + *static_cast(value) = stmt->paramBindOffsetPtr; + break; + case SQL_ATTR_ENABLE_AUTO_IPD: + if (value) + *static_cast(value) = stmt->enableAutoIPD; + break; + case SQL_ATTR_CURSOR_TYPE: + if (value) + *static_cast(value) = stmt->cursorType; + break; + case SQL_ATTR_CONCURRENCY: + if (value) + *static_cast(value) = stmt->concurrency; + break; + case SQL_ATTR_USE_BOOKMARKS: + if (value) + *static_cast(value) = stmt->useBookmarks; + break; + case SQL_ATTR_MAX_ROWS: + if (value) + *static_cast(value) = stmt->maxRows; + break; + case SQL_ATTR_MAX_LENGTH: + if (value) + *static_cast(value) = stmt->maxLength; + break; + case SQL_ATTR_NOSCAN: + if (value) + *static_cast(value) = stmt->noScan; + break; + case SQL_ATTR_RETRIEVE_DATA: + if (value) + *static_cast(value) = stmt->retrieveData; + break; + case SQL_ATTR_METADATA_ID: + if (value) + *static_cast(value) = stmt->metadataId; + break; + + default: + logMessage(cnct, "SQLGetStmtAttr: Unsupported attribute: " + std::to_string(attribute), + LOG_LEVEL_WARN); + stmt->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } + + // Update StringLength if required + if (stringLength) { + *stringLength = 0; // Update with actual size if applicable + } + + logMessage(cnct, "SQLGetStmtAttr: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLGetStmtOption(SQLHSTMT statementHandle, SQLUSMALLINT option, + SQLPOINTER value) { + if (!statementHandle) { + logMessage(nullptr, "SQLGetStmtOption: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + switch (option) { + case SQL_QUERY_TIMEOUT: + if (value) + *static_cast(value) = 0; + return SQL_SUCCESS; + case SQL_MAX_ROWS: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_MAX_ROWS, value, 0, nullptr); + case SQL_NOSCAN: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_NOSCAN, value, 0, nullptr); + case SQL_MAX_LENGTH: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_MAX_LENGTH, value, 0, nullptr); + case SQL_BIND_TYPE: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_ROW_BIND_TYPE, value, 0, nullptr); + case SQL_CURSOR_TYPE: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_CURSOR_TYPE, value, 0, nullptr); + case SQL_CONCURRENCY: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_CONCURRENCY, value, 0, nullptr); + case SQL_ROWSET_SIZE: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_ROW_ARRAY_SIZE, value, 0, nullptr); + case SQL_RETRIEVE_DATA: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_RETRIEVE_DATA, value, 0, nullptr); + case SQL_USE_BOOKMARKS: + return SQLGetStmtAttr(statementHandle, SQL_ATTR_USE_BOOKMARKS, value, 0, nullptr); + default: + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + stmt->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + if (!statementHandle) { + logMessage(cnct, "SQLGetTypeInfo: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + logMessage(cnct, "SQLGetTypeInfo: Entering", LOG_LEVEL_TRACE); + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLGetTypeInfo parameters:" + << "statementHandle = " << handleToString(statementHandle) + << ", dataType = " << dataType; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + stmt->clearDiagnostics(); + stmt->ClearResultSet(); + stmt->AllocateSessionResultSet(); + stmt->curRow = -1; + stmt->rowsReturned = 0; + stmt->isQuery = true; + stmt->isStream = false; + stmt->lastGetDataRow = -1; + stmt->getDataOffsets.clear(); + + // Define the type information data + std::vector columnNames = { + "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREFIX", + "LITERAL_SUFFIX", "CREATE_PARAMS", "NULLABLE", "CASE_SENSITIVE", + "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE", + "LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", + "SQL_DATETIME_SUB", "NUM_PREC_RADIX", "INTERVAL_PRECISION"}; + + std::vector columnTypes = { + "TEXT", "INT16", "INT32", "TEXT", "TEXT", "TEXT", "INT16", "INT16", "INT16", "INT16", + "INT16", "INT16", "TEXT", "INT16", "INT16", "INT16", "INT16", "INT32", "INT16"}; + + ODBCField null = ODBCField::null(); + std::vector> typeData = { + {"BOOLEAN", -7, 1, null, null, null, 1, 0, 2, null, 1, null, "BOOLEAN", null, null, -7, null, + null, null}, + {"INT64", -5, 19, null, null, null, 1, 0, 2, 0, 1, 0, "INT64", 0, 0, -5, null, 10, null}, + {"BLOB", -4, 0, null, null, null, 1, 0, 1, null, 0, null, "BLOB", null, null, -4, null, null, + null}, + {"TEXT", -1, 0, null, null, null, 1, 1, 1, null, 0, null, "TEXT", null, null, -1, null, null, + null}, + {"INT32", 4, 10, null, null, null, 1, 0, 2, 0, 1, 0, "INT32", 0, 0, 4, null, 10, null}, + {"FLOAT", 7, 24, null, null, null, 1, 0, 2, 0, 0, 0, "FLOAT", null, null, 7, null, 2, null}, + {"DOUBLE", 8, 53, null, null, null, 1, 0, 2, 0, 0, 0, "DOUBLE", null, null, 8, null, 2, null}, + {"STRING", 12, 0, null, null, "length", 1, 1, 1, null, 0, null, "STRING", null, null, 12, + null, null, null}, + {"DATE", 91, 10, null, null, null, 1, 0, 2, null, 1, null, "DATE", 0, 0, 9, 1, null, null}, + {"TIMESTAMP", 93, 23, null, null, null, 1, 0, 2, 1, 1, 0, "TIMESTAMP", 3, 3, 9, 3, null, + null}}; + + // Unicode consumers such as MSDASQL select SQL_C_WCHAR from these SQL types. + const size_t originalTypes = typeData.size(); + for (size_t i = 0; i < originalTypes; ++i) { + SQLSMALLINT type = 0; + typeData[i][1].toSShort(&type); + if (type == SQL_VARCHAR || type == SQL_LONGVARCHAR) { + auto wide = typeData[i]; + wide[1] = wide[15] = ODBCField(type == SQL_VARCHAR ? SQL_WVARCHAR : SQL_WLONGVARCHAR); + typeData.push_back(std::move(wide)); + } + } + // Filter data if specific dataType is requested + if (dataType != SQL_ALL_TYPES) { + std::vector> filteredData; + for (const auto& row : typeData) { + if (row.size() > 1) { + try { + SQLSMALLINT rowDataType = 0; + bool ret = row[1].toSShort(&rowDataType); + if (ret && rowDataType == dataType) { + filteredData.push_back(row); + } + } catch (const std::exception&) { + // Skip rows that can't be parsed + continue; + } + } + } + typeData = filteredData; + } + + // Set up ODBCResultSet with the data + stmt->resultSetPtr->columnNames = columnNames; + stmt->resultSetPtr->columnTypes = columnTypes; + stmt->resultSetPtr->data = typeData; + stmt->resultSetPtr->numRows = typeData.size(); + stmt->resultSetPtr->numColumns = columnNames.size(); + stmt->resultSetPtr->isMetaData = true; + + logMessage(cnct, + "SQLGetTypeInfo: Created ODBCResultSet with " + std::to_string(typeData.size()) + + " type records", + LOG_LEVEL_DEBUG); + stmt->resultSetPtr->outputTable(); + logMessage(cnct, "SQLGetTypeInfo: Exiting.\n", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, SQLSMALLINT* columnCount) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLNumResultCols: Entering", LOG_LEVEL_TRACE); + + // Prepare log message for the parameters + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLNumResultCols parameters: " + << "statementHandle = " << handleToString(statementHandle); + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLAllocHandle we're allocating a NULL_Handle, so I disabled this. + if (!statementHandle) { + logMessage(cnct, "SQLNumResultCols: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + if (!columnCount) { + logMessage(cnct, "SQLNumResultCols: columnCount pointer is null", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + if (stmt->resultSetPtr == nullptr) { + logMessage(cnct, "SQLNumResultCols: Missing result set, returning 0", LOG_LEVEL_ERROR); + *columnCount = 0; + return SQL_SUCCESS; + } + + PopulateImplementationRowDescriptor(stmt); + *columnCount = stmt->resultSetPtr->getNumColumns(); + logMessage(cnct, "SQLNumResultCols: Returning column count: " + std::to_string(*columnCount), + LOG_LEVEL_DEBUG); + + logMessage(cnct, "SQLNumResultCols: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLNumParams(SQLHSTMT statementHandle, SQLSMALLINT* parameterCountPtr) { + if (!statementHandle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (!parameterCountPtr) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + if (!stmt->prepared) { + stmt->addDiagnostic("HY010", "Statement has not been prepared"); + return SQL_ERROR; + } + const size_t count = ParameterMarkerPositions(stmt->statementText).size(); + if (count > static_cast(std::numeric_limits::max())) { + stmt->addDiagnostic("HY000", "Too many parameter markers"); + return SQL_ERROR; + } + *parameterCountPtr = static_cast(count); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLParamData(SQLHSTMT statementHandle, SQLPOINTER* value) { + if (!statementHandle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (!stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + const size_t markerCount = ParameterMarkerPositions(stmt->statementText).size(); + for (SQLULEN set = stmt->nextDataSet; set < stmt->paramSetSize; ++set) { + const size_t firstParameter = set == stmt->nextDataSet ? stmt->nextDataParameter : 0; + for (size_t index = firstParameter; index < markerCount; ++index) { + ParameterBinding& binding = stmt->parameterBindings[index]; + if (IsDataAtExecution(ParameterIndicatorAt(stmt, binding, set))) { + stmt->activeDataSet = set; + stmt->activeDataParameter = index; + stmt->nextDataSet = index + 1 < markerCount ? set : set + 1; + stmt->nextDataParameter = index + 1 < markerCount ? index + 1 : 0; + if (value) + *value = ParameterValueAt(stmt, binding, set); + return SQL_NEED_DATA; + } + } + } + stmt->needsParameterData = false; + stmt->activeDataSet = std::numeric_limits::max(); + stmt->activeDataParameter = std::numeric_limits::max(); + try { + return ExecuteParameterSets(stmt); + } catch (const std::exception& error) { + stmt->addDiagnostic("HY000", error.what()); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, SQLCHAR* statementText, + SQLINTEGER textLength) { + if (!statementHandle) { + logMessage(nullptr, "SQLPrepare: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + if (!statementText) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + if (textLength < 0 && textLength != SQL_NTS) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + stmt->statementText = ConvertSQLCHARToString(statementText, textLength); + stmt->prepared = true; + stmt->ClearResultSet(); + stmt->curRow = -1; + stmt->rowsReturned = 0; + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLPutData(SQLHSTMT statementHandle, SQLPOINTER data, SQLLEN strLen_or_Ind) { + if (!statementHandle) + return SQL_INVALID_HANDLE; + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (!stmt->needsParameterData || stmt->activeDataSet == std::numeric_limits::max() || + stmt->activeDataParameter == std::numeric_limits::max()) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + ParameterBinding& binding = stmt->parameterBindings[stmt->activeDataParameter]; + if (strLen_or_Ind == SQL_NULL_DATA) { + binding.streamedNull[stmt->activeDataSet] = true; + binding.streamedData[stmt->activeDataSet].clear(); + return SQL_SUCCESS; + } + if (strLen_or_Ind < 0 && strLen_or_Ind != SQL_NTS) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + if (!data) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + const char* bytes = static_cast(data); + const SQLSMALLINT valueType = binding.valueType == SQL_C_DEFAULT + ? DefaultParameterCType(binding.parameterType) + : binding.valueType; + size_t length = static_cast(strLen_or_Ind); + if (valueType != SQL_C_CHAR && valueType != SQL_C_WCHAR && valueType != SQL_C_BINARY) { + length = CTypeSize(valueType, binding.bufferLength); + } else if (strLen_or_Ind == SQL_NTS) { + if (valueType == SQL_C_BINARY) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + length = valueType == SQL_C_WCHAR + ? std::char_traits::length(static_cast(data)) * + sizeof(SQLWCHAR) + : std::strlen(bytes); + } + binding.streamedData[stmt->activeDataSet].append(bytes, length); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLRowCount(SQLHSTMT statementHandle, SQLLEN* rowCount) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + logMessage(cnct, "SQLRowCount: Entering", LOG_LEVEL_TRACE); + + // Prepare log message for the parameters + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLRowCount parameters: " + << "statementHandle = " << handleToString(statementHandle); + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (!statementHandle) { + logMessage(cnct, "SQLRowCount: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + if (!rowCount) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + + // IoTDB does not expose an affected-row count, and query row counts may be + // incomplete while results are streamed. + *rowCount = -1; + logMessage(cnct, "SQLRowCount: Returning row count: " + std::to_string(*rowCount), + LOG_LEVEL_DEBUG); + + logMessage(cnct, "SQLRowCount: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetConnectAttr(const SQLHDBC connectionHandle, const SQLINTEGER attribute, + SQLPOINTER value, const SQLINTEGER stringLength) { + ConnectionHandle* cnct = static_cast(connectionHandle); + + logMessage(cnct, "SQLSetConnectAttr: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLSetConnectAttr parameters: " + << "connectionHandle = " << handleToString(connectionHandle) + << ", attribute = " << attribute << ", value = " << valueToString(value) + << ", stringLength = " << stringLength; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (cnct == nullptr) { + logMessage("SQLSetConnectAttr: Invalid ConnectionHandle"); + return SQL_INVALID_HANDLE; + } + cnct->clearDiagnostics(); + + // Handle supported attributes + switch (attribute) { + case SQL_ATTR_AUTOCOMMIT: + logMessage(cnct, "SQLSetConnectAttr: Processing SQL_ATTR_AUTOCOMMIT", LOG_LEVEL_DEBUG); + if (value == reinterpret_cast(SQL_AUTOCOMMIT_ON)) { + cnct->autoCommit = true; + logMessage(cnct, "SQLSetConnectAttr: Autocommit enabled", LOG_LEVEL_DEBUG); + } else if (value == reinterpret_cast(SQL_AUTOCOMMIT_OFF)) { + cnct->addDiagnostic("HYC00", "Transactions are not supported"); + return SQL_ERROR; + } else { + logMessage(cnct, "SQLSetConnectAttr: Invalid value for SQL_ATTR_AUTOCOMMIT", LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY024", "Invalid attribute value"); + return SQL_ERROR; + } + break; + + case SQL_ATTR_CONNECTION_TIMEOUT: + logMessage(cnct, "SQLSetConnectAttr: Processing SQL_ATTR_CONNECTION_TIMEOUT", LOG_LEVEL_DEBUG); + cnct->timeoutConnection = static_cast(reinterpret_cast(value)); + break; + + case SQL_ATTR_LOGIN_TIMEOUT: + logMessage(cnct, "SQLSetConnectAttr: Processing SQL_ATTR_LOGIN_TIMEOUT", LOG_LEVEL_DEBUG); + { + const SQLUINTEGER timeout = static_cast(reinterpret_cast(value)); + cnct->timeoutLogin = timeout; + break; + } + + case SQL_ATTR_ACCESS_MODE: + if (value != reinterpret_cast(SQL_MODE_READ_WRITE)) { + cnct->addDiagnostic("HYC00", "Read-only connection mode is not supported"); + return SQL_ERROR; + } + break; + + case SQL_ATTR_TXN_ISOLATION: + if (reinterpret_cast(value) != 0) { + cnct->addDiagnostic("HYC00", "Transactions are not supported"); + return SQL_ERROR; + } + break; + + case SQL_ATTR_CURRENT_CATALOG: { + logMessage(cnct, "SQLSetConnectAttr: Processing SQL_ATTR_CURRENT_CATALOG", LOG_LEVEL_DEBUG); + if (!value) { + logMessage(cnct, "SQLSetConnectAttr: NULL value for SQL_ATTR_CURRENT_CATALOG", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY024", "Invalid attribute value"); + return SQL_ERROR; + } + if (stringLength < 0 && stringLength != SQL_NTS) { + cnct->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + const char* databaseValue = static_cast(value); + std::string databaseName(databaseValue, stringLength == SQL_NTS + ? std::strlen(databaseValue) + : static_cast(stringLength)); + if (databaseName.length() > 64) { + logMessage(cnct, "SQLSetConnectAttr: Catalog name too long (max 64 characters)", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY090", "String data, right truncation"); + return SQL_ERROR; + } + if (databaseName.empty() || databaseName.find('\0') != std::string::npos) { + cnct->addDiagnostic("HY024", "Invalid catalog name"); + return SQL_ERROR; + } + std::string quotedDatabase = "\""; + for (char character : databaseName) { + if (character == '"') + quotedDatabase += '"'; + quotedDatabase += character; + } + quotedDatabase += '"'; + if (cnct->isTableModel) { + logMessage(cnct, "SQLSetConnectAttr: Setting current database: USE " + cnct->database, + LOG_LEVEL_DEBUG); + SQLHANDLE statementHandle = nullptr; + SQLRETURN sqlreturn = SQLAllocHandle(SQL_HANDLE_STMT, connectionHandle, &statementHandle); + if (!SQL_SUCCEEDED(sqlreturn)) + return sqlreturn; + StatementHandle* stmt = static_cast(statementHandle); + sqlreturn = IoTDB_ExecDirect(stmt, "USE " + quotedDatabase); + if (sqlreturn != SQL_SUCCESS && sqlreturn != SQL_SUCCESS_WITH_INFO) { + logMessage(cnct, "SQLSetConnectAttr: Setting current catalog failed!", LOG_LEVEL_ERROR); + std::string state, message; + int native = 0; + if (stmt->getDiagnostic(1, state, message, native)) + cnct->addDiagnostic(state, message, native); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + return sqlreturn; + } + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + } + cnct->database = databaseName; + logMessage(cnct, "SQLSetConnectAttr: Set current catalog (= database) to " + cnct->database, + LOG_LEVEL_DEBUG); + break; + } + + default: + logMessage(cnct, + "SQLSetConnectAttr: Unsupported attribute " + std::to_string(attribute) + + ", returning SQL_ERROR", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; // Return error for unsupported attributes + } + + logMessage(cnct, "SQLSetConnectAttr: Exiting successfully\n", LOG_LEVEL_WARN); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetConnectOption(SQLHDBC connectionHandle, SQLUSMALLINT option, + SQLULEN value) { + if (!connectionHandle) { + logMessage(nullptr, "SQLSetConnectOption: Invalid connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + switch (option) { + case SQL_ACCESS_MODE: + return SQLSetConnectAttr(connectionHandle, SQL_ATTR_ACCESS_MODE, + reinterpret_cast(value), 0); + case SQL_AUTOCOMMIT: + return SQLSetConnectAttr(connectionHandle, SQL_ATTR_AUTOCOMMIT, + reinterpret_cast(value), 0); + case SQL_LOGIN_TIMEOUT: + return SQLSetConnectAttr(connectionHandle, SQL_ATTR_LOGIN_TIMEOUT, + reinterpret_cast(value), 0); + case SQL_TXN_ISOLATION: + return SQLSetConnectAttr(connectionHandle, SQL_ATTR_TXN_ISOLATION, + reinterpret_cast(value), 0); + case SQL_CURRENT_QUALIFIER: + return SQLSetConnectAttr(connectionHandle, SQL_ATTR_CURRENT_CATALOG, + reinterpret_cast(value), SQL_NTS); + default: + auto* cnct = static_cast(connectionHandle); + cnct->clearDiagnostics(); + cnct->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLSetCursorName(SQLHSTMT statementHandle, SQLCHAR* cursorName, + SQLSMALLINT nameLength) { + if (!statementHandle) { + logMessage(nullptr, "SQLSetCursorName: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (!cursorName) { + stmt->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + if (nameLength < 0 && nameLength != SQL_NTS) { + stmt->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + stmt->cursorName.assign(reinterpret_cast(cursorName), + nameLength == SQL_NTS + ? std::strlen(reinterpret_cast(cursorName)) + : static_cast(nameLength)); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetDescField(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, + SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength) { + if (!descriptorHandle) + return SQL_INVALID_HANDLE; + auto* descriptor = static_cast(descriptorHandle); + if (descriptor->getHandleType() != SQL_HANDLE_DESC) + return SQL_INVALID_HANDLE; + descriptor->clearDiagnostics(); + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW && + fieldIdentifier != SQL_DESC_ARRAY_STATUS_PTR && + fieldIdentifier != SQL_DESC_ROWS_PROCESSED_PTR) { + descriptor->addDiagnostic("HY016", "Cannot modify an implementation row descriptor"); + return SQL_ERROR; + } + const SQLULEN unsignedValue = reinterpret_cast(value); + const SQLLEN signedValue = reinterpret_cast(value); + switch (fieldIdentifier) { + case SQL_DESC_ARRAY_SIZE: + if (unsignedValue == 0) { + descriptor->addDiagnostic("HY092", "Invalid descriptor array size"); + return SQL_ERROR; + } + descriptor->arraySize = unsignedValue; + return SQL_SUCCESS; + case SQL_DESC_ARRAY_STATUS_PTR: + descriptor->arrayStatusPtr = static_cast(value); + return SQL_SUCCESS; + case SQL_DESC_BIND_OFFSET_PTR: + descriptor->bindOffsetPtr = static_cast(value); + return SQL_SUCCESS; + case SQL_DESC_BIND_TYPE: + descriptor->bindType = unsignedValue; + return SQL_SUCCESS; + case SQL_DESC_COUNT: + if (signedValue < 0 || signedValue > std::numeric_limits::max()) { + descriptor->addDiagnostic("HY092", "Invalid descriptor count"); + return SQL_ERROR; + } + descriptor->count = static_cast(signedValue); + descriptor->records.resize(static_cast(descriptor->count)); + return SQL_SUCCESS; + case SQL_DESC_ROWS_PROCESSED_PTR: + descriptor->rowsProcessedPtr = static_cast(value); + return SQL_SUCCESS; + case SQL_DESC_ALLOC_TYPE: + descriptor->addDiagnostic("HY091", "Descriptor allocation type is read-only"); + return SQL_ERROR; + default: + break; + } + + if (recNumber < 1) { + descriptor->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + DescriptorRecord& record = descriptor->record(recNumber); + descriptor->count = std::max(descriptor->count, recNumber); + switch (fieldIdentifier) { + case SQL_DESC_CONCISE_TYPE: + record.setConciseType(static_cast(signedValue)); + record.dataPtr = nullptr; + break; + case SQL_DESC_TYPE: + record.type = static_cast(signedValue); + record.updateConciseType(); + record.dataPtr = nullptr; + break; + case SQL_DESC_DATETIME_INTERVAL_CODE: + record.datetimeIntervalCode = static_cast(signedValue); + record.updateConciseType(); + record.dataPtr = nullptr; + break; + case SQL_DESC_OCTET_LENGTH: + record.octetLength = signedValue; + record.dataPtr = nullptr; + break; + case SQL_DESC_LENGTH: + record.length = unsignedValue; + record.dataPtr = nullptr; + break; + case SQL_DESC_PRECISION: + record.precision = static_cast(signedValue); + record.dataPtr = nullptr; + break; + case SQL_DESC_SCALE: + record.scale = static_cast(signedValue); + record.dataPtr = nullptr; + break; + case SQL_DESC_DATA_PTR: + record.dataPtr = value; + break; + case SQL_DESC_INDICATOR_PTR: + record.indicatorPtr = static_cast(value); + break; + case SQL_DESC_OCTET_LENGTH_PTR: + record.octetLengthPtr = static_cast(value); + break; + case SQL_DESC_PARAMETER_TYPE: + record.parameterType = static_cast(signedValue); + break; + case SQL_DESC_NAME: { + if (!value) { + record.name.clear(); + break; + } + if (bufferLength < 0 && bufferLength != SQL_NTS) { + descriptor->addDiagnostic("HY090", "Invalid string or buffer length"); + return SQL_ERROR; + } + const char* text = static_cast(value); + record.name.assign(text, bufferLength == SQL_NTS ? std::strlen(text) + : static_cast(bufferLength)); + record.unnamed = record.name.empty() ? SQL_UNNAMED : SQL_NAMED; + break; + } + default: + descriptor->addDiagnostic("HY091", "Invalid descriptor field identifier"); + return SQL_ERROR; + } + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetDescRec(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, SQLSMALLINT type, + SQLSMALLINT subType, SQLLEN length, SQLSMALLINT precision, + SQLSMALLINT scale, SQLPOINTER data, SQLLEN* stringLength, + SQLLEN* indicator) { + if (!descriptorHandle) + return SQL_INVALID_HANDLE; + auto* descriptor = static_cast(descriptorHandle); + if (descriptor->getHandleType() != SQL_HANDLE_DESC) + return SQL_INVALID_HANDLE; + descriptor->clearDiagnostics(); + if (descriptor->role == DescriptorRole::IMPLEMENTATION_ROW) { + descriptor->addDiagnostic("HY016", "Cannot modify an implementation row descriptor"); + return SQL_ERROR; + } + if (recNumber < 1) { + descriptor->addDiagnostic("07009", "Invalid descriptor index"); + return SQL_ERROR; + } + DescriptorRecord& record = descriptor->record(recNumber); + descriptor->count = std::max(descriptor->count, recNumber); + record.type = type; + record.datetimeIntervalCode = subType; + record.updateConciseType(); + record.octetLength = length; + record.length = static_cast(std::max(0, length)); + record.precision = precision; + record.scale = scale; + record.dataPtr = data; + record.octetLengthPtr = stringLength; + record.indicatorPtr = indicator; + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetEnvAttr(const SQLHENV environmentHandle, const SQLINTEGER attribute, + SQLPOINTER value, const SQLINTEGER stringLength) { + EnvironmentHandle* env = static_cast(environmentHandle); + logMessage(nullptr, "SQLSetEnvAttr: Entering", LOG_LEVEL_TRACE); + + if (!env) + return SQL_INVALID_HANDLE; + env->clearDiagnostics(); + + if (isLogLevelEnabled(nullptr, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLSetEnvAttr parameters: " + << "environmentHandle = " << handleToString(environmentHandle) + << ", attribute = " << attribute << ", valuePtr address = " << value + << ", stringLength = " << stringLength; + logMessage(nullptr, logStream.str(), LOG_LEVEL_TRACE); + } + + switch (attribute) { + + // In this case the client is passing int it's version of ODBC API. + // We use that to double-check if this is supported by this driver. + // We save it in the env, as we might need to respond differently + // based on the clients API version. + case SQL_ATTR_ODBC_VERSION: + logMessage(nullptr, "SQLSetEnvAttr: Processing SQL_ATTR_ODBC_VERSION", LOG_LEVEL_DEBUG); + if (value) { + const SQLINTEGER odbcVersion = static_cast(reinterpret_cast(value)); + // Check for supported ODBC versions + // We currently support 3.0 and 3.8 + if (odbcVersion == SQL_OV_ODBC3) { + // Store the version in your environment structure + const auto env = static_cast(environmentHandle); + env->odbcVersion = odbcVersion; + logMessage(nullptr, "SQLSetEnvAttr: ODBC version set to 3.0", LOG_LEVEL_INFO); + return SQL_SUCCESS; + } +// iODBC only seems to support ODBC 3.0 +#ifdef SQL_OV_ODBC3_80 + if (odbcVersion == SQL_OV_ODBC3_80) { + // Store the version in your environment structure + const auto env = static_cast(environmentHandle); + env->odbcVersion = odbcVersion; + logMessage(nullptr, "SQLSetEnvAttr: ODBC version set to 3.8", LOG_LEVEL_INFO); + return SQL_SUCCESS; + } +#endif + // MS Excel only support ODBC 2.0 + if (odbcVersion == SQL_OV_ODBC2) { + const auto env = static_cast(environmentHandle); + env->odbcVersion = odbcVersion; + logMessage(nullptr, "SQLSetEnvAttr: ODBC version set to 2.0", LOG_LEVEL_INFO); + return SQL_SUCCESS; + } + + logMessage(nullptr, "SQLSetEnvAttr: Empty value for ODBC version", LOG_LEVEL_ERROR); + env->addDiagnostic("HY009", "Invalid use of null pointer"); + return SQL_ERROR; + } + logMessage("Empty Value\n"); + return SQL_ERROR; + + case SQL_ATTR_CONNECTION_POOLING: + logMessage(nullptr, "SQLSetEnvAttr: Processing SQL_ATTR_CONNECTION_POOLING", LOG_LEVEL_DEBUG); + logMessage(nullptr, "SQLSetEnvAttr: Connection pooling is not supported", LOG_LEVEL_INFO); + return SQL_SUCCESS; + + case SQL_ATTR_OUTPUT_NTS: + if (reinterpret_cast(value) != SQL_TRUE) { + env->addDiagnostic("HYC00", "Null-terminated output is required"); + return SQL_ERROR; + } + return SQL_SUCCESS; + + default: + logMessage(nullptr, "SQLSetEnvAttr: Unsupported attribute: " + std::to_string(attribute), + LOG_LEVEL_WARN); + env->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLSetParam(SQLHSTMT statementHandle, SQLUSMALLINT parameterNumber, + SQLSMALLINT valueType, SQLSMALLINT parameterType, + SQLULEN lengthPrecision, SQLSMALLINT parameterScale, + SQLPOINTER parameterValue, SQLLEN* strLen_or_Ind) { + return SQLBindParameter(statementHandle, parameterNumber, SQL_PARAM_INPUT, valueType, + parameterType, lengthPrecision, parameterScale, parameterValue, + static_cast(lengthPrecision), strLen_or_Ind); +} + +SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, SQLPOINTER value, + SQLINTEGER stringLength) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLSetStmtAttr: Entering", LOG_LEVEL_TRACE); + + // Only build parameter log message if TRACE level is enabled + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLSetStmtAttr parameters: " + << "statementHandle = " << handleToString(statementHandle) + << ", attribute = " << attribute << ", value = " << valueToString(value) + << ", stringLength = " << stringLength; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + // In SQLSetStmtAttr we're allocating a NULL_Handle, so I disabled this. + if (!statementHandle) { + logMessage(cnct, "SQLSetStmtAttr: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + stmt->clearDiagnostics(); + if (stmt->needsParameterData) { + stmt->addDiagnostic("HY010", "Function sequence error"); + return SQL_ERROR; + } + + switch (attribute) { + case SQL_QUERY_TIMEOUT: + if (reinterpret_cast(value) != 0) { + stmt->addDiagnostic("HYC00", "Query timeout is not supported"); + return SQL_ERROR; + } + break; + + case SQL_ATTR_APP_ROW_DESC: + if (!value) { + stmt->appRowDesc = stmt->implicitAppRowDesc; + break; + } + if (static_cast(value)->getHandleType() != SQL_HANDLE_DESC || + static_cast(value)->connection != cnct) { + stmt->addDiagnostic("HY024", "Invalid attribute value"); + return SQL_ERROR; + } + if (static_cast(value)->implicit && value != stmt->implicitAppRowDesc) { + stmt->addDiagnostic("HY017", "Invalid use of an automatically allocated descriptor"); + return SQL_ERROR; + } + stmt->appRowDesc = value; + break; + + case SQL_ATTR_APP_PARAM_DESC: + if (!value) { + stmt->appParamDesc = stmt->implicitAppParamDesc; + break; + } + if (static_cast(value)->getHandleType() != SQL_HANDLE_DESC || + static_cast(value)->connection != cnct) { + stmt->addDiagnostic("HY024", "Invalid attribute value"); + return SQL_ERROR; + } + if (static_cast(value)->implicit && value != stmt->implicitAppParamDesc) { + stmt->addDiagnostic("HY017", "Invalid use of an automatically allocated descriptor"); + return SQL_ERROR; + } + stmt->appParamDesc = value; + break; + + case SQL_ATTR_ROWS_FETCHED_PTR: { + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_ROWS_FETCHED_PTR", LOG_LEVEL_DEBUG); + if (value == nullptr) { + // Power BI passes value==nullptr for unknown reasons. + // We can't modify Power BI's code, so we ignore this error condition. + logMessage(cnct, "SQLSetStmtAttr: value is nullptr (Power BI compatibility)", + LOG_LEVEL_DEBUG); + break; + } + if (stringLength != sizeof(SQLULEN)) { + logMessage(cnct, "SQLSetStmtAttr: Warning - stringLength != sizeof(SQLULEN)", LOG_LEVEL_WARN); + } + // Store the pointer to the rows fetched counter + stmt->rowsFetchedPtr = static_cast(value); + stmt->implicitImpRowDesc->rowsProcessedPtr = stmt->rowsFetchedPtr; + logMessage(cnct, "SQLSetStmtAttr: Rows fetched pointer set", LOG_LEVEL_DEBUG); + break; + } + + case SQL_ATTR_ROW_STATUS_PTR: + stmt->rowStatusPtr = static_cast(value); + stmt->implicitImpRowDesc->arrayStatusPtr = static_cast(value); + break; + + case SQL_ATTR_CURSOR_TYPE: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_CURSOR_TYPE", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_CURSOR_FORWARD_ONLY)) { + stmt->addDiagnostic("HYC00", "Only forward-only cursors are supported"); + return SQL_ERROR; + } + stmt->cursorType = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Cursor type set successfully", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_CONCURRENCY: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_CONCURRENCY", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_CONCUR_READ_ONLY)) { + stmt->addDiagnostic("HYC00", "Only read-only concurrency is supported"); + return SQL_ERROR; + } + stmt->concurrency = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Concurrency type set successfully", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_CURSOR_SCROLLABLE: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_CURSOR_SCROLLABLE", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_NONSCROLLABLE)) { + stmt->addDiagnostic("HYC00", "Scrollable cursors are not supported"); + return SQL_ERROR; + } + stmt->scrollable = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Cursor scrollable setting applied", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_CURSOR_SENSITIVITY: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_CURSOR_SENSITIVITY", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_UNSPECIFIED)) { + stmt->addDiagnostic("HYC00", "Cursor sensitivity is not supported"); + return SQL_ERROR; + } + stmt->sensitivity = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Cursor sensitivity set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_USE_BOOKMARKS: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_USE_BOOKMARKS", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_UB_OFF)) { + stmt->addDiagnostic("HYC00", "Bookmarks are not supported"); + return SQL_ERROR; + } + stmt->useBookmarks = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Bookmark usage setting applied", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_FETCH_BOOKMARK_PTR: + if (value) { + stmt->addDiagnostic("HYC00", "Bookmarks are not supported"); + return SQL_ERROR; + } + stmt->bookmarkPtr = nullptr; + break; + + case SQL_ATTR_ROW_ARRAY_SIZE: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_ROW_ARRAY_SIZE", LOG_LEVEL_DEBUG); + if (reinterpret_cast(value) == 0) { + stmt->addDiagnostic("HY024", "Row array size must be greater than zero"); + return SQL_ERROR; + } + stmt->rowArraySize = reinterpret_cast(value); + static_cast(stmt->appRowDesc)->arraySize = stmt->rowArraySize; + logMessage(cnct, "SQLSetStmtAttr: Row array size set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_ROW_BIND_TYPE: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_ROW_BIND_TYPE", LOG_LEVEL_DEBUG); + stmt->rowBindType = reinterpret_cast(value); + static_cast(stmt->appRowDesc)->bindType = stmt->rowBindType; + logMessage(cnct, "SQLSetStmtAttr: Row bind type set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_PARAMSET_SIZE: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_PARAMSET_SIZE", LOG_LEVEL_DEBUG); + if (reinterpret_cast(value) == 0) { + stmt->addDiagnostic("HY024", "Parameter set size must be greater than zero"); + return SQL_ERROR; + } + stmt->paramSetSize = reinterpret_cast(value); + static_cast(stmt->appParamDesc)->arraySize = stmt->paramSetSize; + logMessage(cnct, "SQLSetStmtAttr: Parameter set size set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_PARAM_BIND_TYPE: + stmt->paramBindType = reinterpret_cast(value); + static_cast(stmt->appParamDesc)->bindType = stmt->paramBindType; + break; + + case SQL_ATTR_PARAM_BIND_OFFSET_PTR: + stmt->paramBindOffsetPtr = static_cast(value); + static_cast(stmt->appParamDesc)->bindOffsetPtr = + reinterpret_cast(value); + break; + + case SQL_ATTR_PARAMS_PROCESSED_PTR: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_PARAMS_PROCESSED_PTR", LOG_LEVEL_DEBUG); + // Store processed parameter pointer + stmt->paramsProcessedPtr = static_cast(value); + stmt->implicitImpParamDesc->rowsProcessedPtr = stmt->paramsProcessedPtr; + logMessage(cnct, "SQLSetStmtAttr: Params processed pointer set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_PARAM_STATUS_PTR: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_PARAM_STATUS_PTR", LOG_LEVEL_DEBUG); + // Store parameter status pointer + stmt->paramStatusPtr = static_cast(value); + stmt->implicitImpParamDesc->arrayStatusPtr = static_cast(value); + logMessage(cnct, "SQLSetStmtAttr: Parameter status pointer set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_MAX_ROWS: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_MAX_ROWS", LOG_LEVEL_DEBUG); + // Set maximum return row count + stmt->maxRows = (SQLULEN)value; + logMessage(cnct, "SQLSetStmtAttr: Max rows set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_NOSCAN: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_NOSCAN", LOG_LEVEL_DEBUG); + // Set whether to scan + if (value != reinterpret_cast(SQL_NOSCAN_OFF) && + value != reinterpret_cast(SQL_NOSCAN_ON)) { + logMessage(cnct, "SQLSetStmtAttr: Invalid noscan option specified", LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY024", "Invalid noscan option"); + return SQL_ERROR; + } + stmt->noScan = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: NoScan option set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_SIMULATE_CURSOR: + stmt->addDiagnostic("HYC00", "Cursor simulation is not supported"); + return SQL_ERROR; + + case SQL_ATTR_RETRIEVE_DATA: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_RETRIEVE_DATA", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_RD_ON)) { + stmt->addDiagnostic("HYC00", "Disabling data retrieval is not supported"); + return SQL_ERROR; + } + stmt->retrieveData = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Retrieve data option set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_METADATA_ID: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_METADATA_ID", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_FALSE)) { + stmt->addDiagnostic("HYC00", "Identifier-based catalog arguments are not supported"); + return SQL_ERROR; + } + stmt->metadataId = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Metadata ID mode set", LOG_LEVEL_DEBUG); + break; + + case SQL_ATTR_ASYNC_STMT_EVENT: + stmt->addDiagnostic("HYC00", "Asynchronous statements are not supported"); + return SQL_ERROR; + + case SQL_ATTR_ENABLE_AUTO_IPD: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_ENABLE_AUTO_IPD", LOG_LEVEL_DEBUG); + if (value != reinterpret_cast(SQL_FALSE)) { + stmt->addDiagnostic("HYC00", "Automatic parameter descriptors are not supported"); + return SQL_ERROR; + } + stmt->enableAutoIPD = static_cast(reinterpret_cast(value)); + logMessage(cnct, "SQLSetStmtAttr: Auto IPD setting applied", LOG_LEVEL_DEBUG); + break; + + // Note: The following attributes are not fully implemented as their exact behavior + // may vary by database system or require additional context + case SQL_ATTR_IMP_ROW_DESC: + case SQL_ATTR_IMP_PARAM_DESC: + stmt->addDiagnostic("HY092", "Implementation descriptor attributes are read-only"); + return SQL_ERROR; + + case SQL_ATTR_ROW_BIND_OFFSET_PTR: + stmt->rowBindOffsetPtr = static_cast(value); + static_cast(stmt->appRowDesc)->bindOffsetPtr = + reinterpret_cast(value); + break; + + case SQL_ATTR_MAX_LENGTH: + logMessage(cnct, "SQLSetStmtAttr: Processing SQL_ATTR_MAX_LENGTH", LOG_LEVEL_DEBUG); + // Set maximum length for data retrieval + stmt->maxLength = (SQLULEN)value; + logMessage(cnct, "SQLSetStmtAttr: Max length set", LOG_LEVEL_DEBUG); + break; + + default: + logMessage(cnct, "SQLSetStmtAttr: Unsupported attribute: " + std::to_string(attribute), + LOG_LEVEL_WARN); + stmt->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } + + logMessage(cnct, "SQLSetStmtAttr: Exiting successfully", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSetStmtOption(SQLHSTMT statementHandle, SQLUSMALLINT option, SQLULEN value) { + if (!statementHandle) { + logMessage(nullptr, "SQLSetStmtOption: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + SQLINTEGER attribute = 0; + switch (option) { + case SQL_QUERY_TIMEOUT: + attribute = SQL_ATTR_QUERY_TIMEOUT; + break; + case SQL_MAX_ROWS: + attribute = SQL_ATTR_MAX_ROWS; + break; + case SQL_NOSCAN: + attribute = SQL_ATTR_NOSCAN; + break; + case SQL_MAX_LENGTH: + attribute = SQL_ATTR_MAX_LENGTH; + break; + case SQL_BIND_TYPE: + attribute = SQL_ATTR_ROW_BIND_TYPE; + break; + case SQL_CURSOR_TYPE: + attribute = SQL_ATTR_CURSOR_TYPE; + break; + case SQL_CONCURRENCY: + attribute = SQL_ATTR_CONCURRENCY; + break; + case SQL_ROWSET_SIZE: + attribute = SQL_ATTR_ROW_ARRAY_SIZE; + break; + case SQL_RETRIEVE_DATA: + attribute = SQL_ATTR_RETRIEVE_DATA; + break; + case SQL_USE_BOOKMARKS: + attribute = SQL_ATTR_USE_BOOKMARKS; + break; + default: + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + stmt->addDiagnostic("HY092", "Invalid attribute/option identifier"); + return SQL_ERROR; + } + return SQLSetStmtAttr(statementHandle, attribute, reinterpret_cast(value), 0); +} + +static SQLRETURN SetEmptyCatalogResult(StatementHandle* stmt, + const std::vector& columnNames, + const std::vector& columnTypes) { + stmt->ClearResultSet(); + stmt->AllocateSessionResultSet(); + if (!stmt->resultSetPtr) { + stmt->addDiagnostic("HY001", "Unable to allocate catalog result set"); + return SQL_ERROR; + } + stmt->resultSetPtr->clear(); + stmt->resultSetPtr->columnNames = columnNames; + stmt->resultSetPtr->columnTypes = columnTypes; + stmt->resultSetPtr->numColumns = static_cast(columnNames.size()); + stmt->resultSetPtr->numRows = 0; + stmt->resultSetPtr->isMetaData = true; + stmt->curRow = -1; + stmt->rowsReturned = 0; + stmt->isQuery = true; + PopulateImplementationRowDescriptor(stmt); + return SQL_SUCCESS; +} + +SQLRETURN SQL_API SQLSpecialColumns(SQLHSTMT statementHandle, SQLUSMALLINT identifierType, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLUSMALLINT scope, + SQLUSMALLINT nullable) { + if (!statementHandle) { + logMessage(nullptr, "SQLSpecialColumns: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (identifierType != SQL_BEST_ROWID && identifierType != SQL_ROWVER) { + stmt->addDiagnostic("HY097", "Column type out of range"); + return SQL_ERROR; + } + if (scope != SQL_SCOPE_CURROW && scope != SQL_SCOPE_TRANSACTION && scope != SQL_SCOPE_SESSION) { + stmt->addDiagnostic("HY098", "Scope type out of range"); + return SQL_ERROR; + } + if (nullable != SQL_NO_NULLS && nullable != SQL_NULLABLE) { + stmt->addDiagnostic("HY099", "Nullable type out of range"); + return SQL_ERROR; + } + (void)catalogName; + (void)nameLength1; + (void)schemaName; + (void)nameLength2; + (void)tableName; + (void)nameLength3; + return SetEmptyCatalogResult( + stmt, + {"SCOPE", "COLUMN_NAME", "DATA_TYPE", "TYPE_NAME", "COLUMN_SIZE", "BUFFER_LENGTH", + "DECIMAL_DIGITS", "PSEUDO_COLUMN"}, + {"INT16", "TEXT", "INT16", "TEXT", "INT32", "INT32", "INT16", "INT16"}); +} + +SQLRETURN SQL_API SQLStatistics(SQLHSTMT statementHandle, SQLCHAR* catalogName, + SQLSMALLINT nameLength1, SQLCHAR* schemaName, + SQLSMALLINT nameLength2, SQLCHAR* tableName, + SQLSMALLINT nameLength3, SQLUSMALLINT unique, + SQLUSMALLINT reserved) { + if (!statementHandle) { + logMessage(nullptr, "SQLStatistics: Invalid statement handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + auto* stmt = static_cast(statementHandle); + stmt->clearDiagnostics(); + if (unique != SQL_INDEX_UNIQUE && unique != SQL_INDEX_ALL) { + stmt->addDiagnostic("HY100", "Uniqueness option type out of range"); + return SQL_ERROR; + } + if (reserved != SQL_ENSURE && reserved != SQL_QUICK) { + stmt->addDiagnostic("HY101", "Accuracy option type out of range"); + return SQL_ERROR; + } + (void)catalogName; + (void)nameLength1; + (void)schemaName; + (void)nameLength2; + (void)tableName; + (void)nameLength3; + // IoTDB exposes no relational indexes through ODBC, so the conformant + // catalog result has the required schema and zero rows. + return SetEmptyCatalogResult(stmt, + {"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "NON_UNIQUE", + "INDEX_QUALIFIER", "INDEX_NAME", "TYPE", "ORDINAL_POSITION", + "COLUMN_NAME", "ASC_OR_DESC", "CARDINALITY", "PAGES", + "FILTER_CONDITION"}, + {"TEXT", "TEXT", "TEXT", "INT16", "TEXT", "TEXT", "INT16", "INT16", + "TEXT", "TEXT", "INT32", "INT32", "TEXT"}); +} + +/* +when name is nullptr, return "" +dont deal with the % case here + */ +SQLRETURN SQL_API SQLTransact(SQLHENV environmentHandle, SQLHDBC connectionHandle, + SQLUSMALLINT completionType) { + if (!environmentHandle && !connectionHandle) { + logMessage(nullptr, "SQLTransact: Invalid environment/connection handle", LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + if (connectionHandle) + return SQLEndTran(SQL_HANDLE_DBC, connectionHandle, completionType); + return SQLEndTran(SQL_HANDLE_ENV, environmentHandle, completionType); +} + +// sqlext.h + +SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, SQLHWND windowHandle, + SQLCHAR* inConnectionString, + SQLSMALLINT inConnectionStringLength, + SQLCHAR* outConnectionString, SQLSMALLINT bufferLength, + SQLSMALLINT* outConnectionStringLengthPtr, + SQLUSMALLINT driverCompletion) { + ConnectionHandle* cnct = static_cast(connectionHandle); + if (!cnct) + return SQL_INVALID_HANDLE; + cnct->clearDiagnostics(); + logMessage(cnct, "SQLDriverConnect: Entering", LOG_LEVEL_TRACE); + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::stringstream logStream; + logStream << "SQLDriverConnect parameters: " + << "connectionHandle = " << handleToString(connectionHandle) + << ", windowHandle = " << handleToString(windowHandle) + << ", inConnectionStringLength = " << inConnectionStringLength + << ", bufferLength = " << bufferLength << ", driverCompletion = " << driverCompletion; + logMessage(cnct, logStream.str(), LOG_LEVEL_TRACE); + } + + if (!inConnectionString || inConnectionStringLength == 0) { + logMessage(cnct, "SQLDriverConnect: Connection string is empty or null", LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "Invalid connection string"); + return SQL_ERROR; + } + + if (inConnectionStringLength < 0 && inConnectionStringLength != SQL_NTS) { + cnct->addDiagnostic("HY090", "Invalid connection string length"); + return SQL_ERROR; + } + if (bufferLength < 0) { + cnct->addDiagnostic("HY090", "Invalid output buffer length"); + return SQL_ERROR; + } + std::string connectionString((const char*)inConnectionString, + inConnectionStringLength == SQL_NTS + ? strlen((const char*)inConnectionString) + : static_cast(inConnectionStringLength)); + try { + + // --- Phase 1: first pass to extract DSN name and detect DRIVER keyword --- + auto attributes = ParseConnectionString(connectionString); + std::string dsnName; + bool hasDriverKeyword = false; + { + for (const auto& attribute : attributes) { + std::string key = attribute.first; + std::string keyLower = key; + std::transform(keyLower.begin(), keyLower.end(), keyLower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (keyLower == "dsn") { + dsnName = attribute.second; + } else if (keyLower == "driver") { + hasDriverKeyword = true; + } + } + } + logMessage(cnct, + "SQLDriverConnect: Extracted DSN='" + dsnName + + "', hasDriver=" + (hasDriverKeyword ? "true" : "false"), + LOG_LEVEL_DEBUG); + cnct->dataSourceName = dsnName; + + // --- Phase 2: if DSN is specified and no DRIVER keyword, load from ODBC.INI --- + if (!dsnName.empty() && !hasDriverKeyword) { + logMessage(cnct, + "SQLDriverConnect: Loading DSN '" + dsnName + "' from ODBC.INI as base config", + LOG_LEVEL_INFO); + cnct->LoadDsnFromOdbcIni(dsnName); + } + + // --- Phase 3: parse connection string again to override DSN values --- + logMessage(cnct, "SQLDriverConnect: Applying connection string overrides", LOG_LEVEL_TRACE); + { + for (const auto& attribute : attributes) + SetConnectionHandle(cnct, attribute.first, attribute.second); + } + + if (isLogLevelEnabled(cnct, LOG_LEVEL_INFO)) { + std::ostringstream oss; + oss << "SQLDriverConnect: Final config - Server=" << cnct->serverHostName + << ", Port=" << cnct->serverPort << ", UID=" << cnct->userName + << ", Database=" << cnct->database + << ", TableModel=" << (cnct->isTableModel ? "true" : "false") + << ", LogLevel=" << cnct->logLevel << ", SessionTimeout=" << cnct->sessionTimeoutMs + << ", BatchSize=" << cnct->batchSize; + logMessage(cnct, oss.str(), LOG_LEVEL_INFO); + } + + bool outputTruncated = false; + if (outConnectionStringLengthPtr) { + *outConnectionStringLengthPtr = static_cast(std::min( + connectionString.size(), static_cast(std::numeric_limits::max()))); + } + if (outConnectionString && bufferLength > 0) { + const size_t copyLength = + std::min(connectionString.size(), static_cast(bufferLength - 1)); + std::memcpy(outConnectionString, connectionString.data(), copyLength); + outConnectionString[copyLength] = '\0'; + outputTruncated = copyLength < connectionString.size(); + logMessage(cnct, "SQLDriverConnect: Copied connection string to output buffer", + LOG_LEVEL_DEBUG); + } + + if (cnct->isTableModel && cnct->database.empty()) { + logMessage(cnct, "SQLDriverConnect: Database not set, using 'information_schema' as default", + LOG_LEVEL_INFO); + cnct->database = "information_schema"; + } + + cnct->ValidateTransport(); + SQLRETURN returnCode; + if (cnct->useRestful) { + logMessage(cnct, "SQLDriverConnect: Connecting using RESTful API", LOG_LEVEL_DEBUG); + returnCode = IoTDB_DriverConnect_Rest(cnct); + } else { + logMessage(cnct, "SQLDriverConnect: Connecting using Session API", LOG_LEVEL_DEBUG); + returnCode = IoTDB_DriverConnect_Session(cnct); + } + + logMessage(cnct, "SQLDriverConnect: Exiting with return code: " + std::to_string(returnCode), + LOG_LEVEL_TRACE); + if (SQL_SUCCEEDED(returnCode) && outputTruncated) { + cnct->addDiagnostic("01004", "Connection string data was truncated"); + return SQL_SUCCESS_WITH_INFO; + } + return returnCode; + } catch (const std::exception& e) { + cnct->addDiagnostic("08001", e.what()); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLMoreResults(SQLHSTMT statementHandle) { + const auto stmt = static_cast(statementHandle); + ConnectionHandle* cnct = stmt ? stmt->getConnection() : nullptr; + + logMessage(cnct, "SQLMoreResults: Entering", LOG_LEVEL_TRACE); + + if (!statementHandle) { + logMessage(cnct, "SQLMoreResults: Invalid statement handle, returning SQL_INVALID_HANDLE", + LOG_LEVEL_ERROR); + return SQL_INVALID_HANDLE; + } + + logMessage(cnct, "SQLMoreResults: Only support one result set. Returning SQL_NO_DATA", + LOG_LEVEL_DEBUG); + return SQL_NO_DATA; +} + +#ifdef WIN32 +#include "setup/setup_dialog.h" + +extern "C" BOOL APIENTRY ConfigDSN(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, + LPCSTR lpszAttributes) { + logMessage(nullptr, "ConfigDSN: Entering", LOG_LEVEL_INFO); + return ShowDSNDialog(hwndParent, fRequest, lpszDriver, lpszAttributes); +} +#endif diff --git a/iotdb-client/odbc/src/driver.def b/iotdb-client/odbc/src/driver.def new file mode 100644 index 0000000000000..eb59df1fcec51 --- /dev/null +++ b/iotdb-client/odbc/src/driver.def @@ -0,0 +1,104 @@ +; 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. +EXPORTS + ;odbcinst.h functions + ConfigDSN + ;sql.h functions + SQLAllocConnect + SQLAllocEnv + SQLAllocHandle + SQLAllocStmt + SQLBindCol + SQLBindParam + SQLBindParameter + SQLCancel + SQLCancelHandle + SQLCloseCursor + SQLColAttribute + SQLColumns + SQLConnect + SQLCopyDesc + SQLDataSources + SQLDescribeCol + SQLDisconnect + SQLEndTran + SQLError + SQLExecDirect + SQLExecute + SQLFetch + SQLFetchScroll + SQLFreeConnect + SQLFreeEnv + SQLFreeHandle + SQLFreeStmt + SQLGetConnectAttr + SQLGetConnectOption + SQLGetCursorName + SQLGetData + SQLGetDescField + SQLGetDescRec + SQLGetDiagField + SQLGetDiagRec + SQLGetEnvAttr + SQLGetFunctions + SQLGetInfo + SQLGetStmtAttr + SQLGetStmtOption + SQLGetTypeInfo + SQLNativeSql + SQLNumResultCols + SQLNumParams + SQLParamData + SQLPrepare + SQLPutData + SQLRowCount + SQLSetConnectAttr + SQLSetConnectOption + SQLSetCursorName + SQLSetDescField + SQLSetDescRec + SQLSetEnvAttr + SQLSetParam + SQLSetStmtAttr + SQLSetStmtOption + SQLSpecialColumns + SQLStatistics + SQLTables + SQLTransact + ;sqlext.h functions + SQLDriverConnect + SQLMoreResults + SQLExecDirectW + SQLPrepareW + SQLDriverConnectW + SQLConnectW + SQLTablesW + SQLColumnsW + SQLDescribeColW + SQLColAttributeW + SQLGetInfoW + SQLGetDiagRecW + SQLGetTypeInfoW + SQLGetStmtAttrW + SQLSetStmtAttrW + SQLGetDiagFieldW + SQLGetConnectAttrW + SQLSetConnectAttrW + SQLNativeSqlW + SQLGetDescFieldW + SQLGetDescRecW + SQLSetDescFieldW diff --git a/iotdb-client/odbc/src/driver.h b/iotdb-client/odbc/src/driver.h new file mode 100644 index 0000000000000..d78204b641853 --- /dev/null +++ b/iotdb-client/odbc/src/driver.h @@ -0,0 +1,184 @@ +/** + * 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. + */ +// +// Created by christoferdutz on 05.11.2024. +// + +#ifndef DRIVER_H +#define DRIVER_H + +#include "Pch.h" +#include "DiagnosticManager.h" +#include "DriverConfig.h" + +// IoTDB Session API includes +#include + +// Forward declarations to avoid circular dependencies +struct BindColInfo; + +#include "ODBCHandle.h" +#include "EnvironmentHandle.h" +#include "ConnectionHandle.h" +#include "StatementHandle.h" + +extern "C" { +SQLRETURN SQL_API SQLAllocConnect(SQLHENV environmentHandle, SQLHDBC* connectionHandle); +SQLRETURN SQL_API SQLAllocEnv(SQLHENV* environmentHandle); +SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, SQLHANDLE inputHandle, + SQLHANDLE* outputHandle); +SQLRETURN SQL_API SQLAllocStmt(SQLHDBC ConnectionHandle, SQLHSTMT* StatementHandle); +SQLRETURN SQL_API SQLBindCol(SQLHSTMT StatementHandle, SQLUSMALLINT columnNumber, + SQLSMALLINT TargetType, SQLPOINTER TargetValue, SQLLEN BufferLength, + SQLLEN* StrLen_or_Ind); +SQLRETURN SQL_API SQLBindParam(SQLHSTMT StatementHandle, SQLUSMALLINT ParameterNumber, + SQLSMALLINT ValueType, SQLSMALLINT ParameterType, + SQLULEN LengthPrecision, SQLSMALLINT ParameterScale, + SQLPOINTER ParameterValue, SQLLEN* StrLen_or_Ind); +SQLRETURN SQL_API SQLBindParameter(SQLHSTMT StatementHandle, SQLUSMALLINT ParameterNumber, + SQLSMALLINT InputOutputType, SQLSMALLINT ValueType, + SQLSMALLINT ParameterType, SQLULEN ColumnSize, + SQLSMALLINT DecimalDigits, SQLPOINTER ParameterValuePtr, + SQLLEN BufferLength, SQLLEN* StrLen_or_IndPtr); +SQLRETURN SQL_API SQLCancel(SQLHSTMT StatementHandle); +SQLRETURN SQL_API SQLCancelHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle); +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT StatementHandle); +SQLRETURN SQL_API SQLColAttribute(SQLHSTMT StatementHandle, SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttribute, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLength, + SQLLEN* numericAttribute); +SQLRETURN SQL_API SQLColumns(SQLHSTMT StatementHandle, SQLCHAR* CatalogName, + SQLSMALLINT NameLength1, SQLCHAR* SchemaName, SQLSMALLINT NameLength2, + SQLCHAR* TableName, SQLSMALLINT NameLength3, SQLCHAR* ColumnName, + SQLSMALLINT NameLength4); +SQLRETURN SQL_API SQLConnect(SQLHDBC ConnectionHandle, SQLCHAR* ServerName, SQLSMALLINT NameLength1, + SQLCHAR* UserName, SQLSMALLINT NameLength2, SQLCHAR* Password, + SQLSMALLINT NameLength3); +SQLRETURN SQL_API SQLCopyDesc(SQLHDESC SourceDescHandle, SQLHDESC TargetDescHandle); +SQLRETURN SQL_API SQLDataSources(SQLHENV EnvironmentHandle, SQLUSMALLINT Direction, + SQLCHAR* ServerName, SQLSMALLINT BufferLength1, + SQLSMALLINT* NameLength1Ptr, SQLCHAR* Description, + SQLSMALLINT BufferLength2, SQLSMALLINT* NameLength2Ptr); +SQLRETURN SQL_API SQLDescribeCol(SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, + SQLCHAR* ColumnName, SQLSMALLINT BufferLength, + SQLSMALLINT* NameLength, SQLSMALLINT* DataType, + SQLULEN* ColumnSize, SQLSMALLINT* DecimalDigits, + SQLSMALLINT* Nullable); +SQLRETURN SQL_API SQLDisconnect(SQLHDBC ConnectionHandle); +SQLRETURN SQL_API SQLEndTran(SQLSMALLINT HandleType, SQLHANDLE Handle, SQLSMALLINT CompletionType); +SQLRETURN SQL_API SQLError(SQLHENV EnvironmentHandle, SQLHDBC ConnectionHandle, + SQLHSTMT StatementHandle, SQLCHAR* Sqlstate, SQLINTEGER* NativeError, + SQLCHAR* MessageText, SQLSMALLINT BufferLength, SQLSMALLINT* TextLength); +SQLRETURN SQL_API SQLExecDirect(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength); +SQLRETURN SQL_API SQLExecute(SQLHSTMT StatementHandle); +SQLRETURN SQL_API SQLFetch(SQLHSTMT StatementHandle); +SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT StatementHandle, SQLSMALLINT FetchOrientation, + SQLLEN FetchOffset); +SQLRETURN SQL_API SQLFreeConnect(SQLHDBC ConnectionHandle); +SQLRETURN SQL_API SQLFreeEnv(SQLHENV EnvironmentHandle); +SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle); +SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT StatementHandle, SQLUSMALLINT Option); +SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC ConnectionHandle, SQLINTEGER Attribute, + SQLPOINTER Value, SQLINTEGER BufferLength, + SQLINTEGER* StringLengthPtr); +SQLRETURN SQL_API SQLGetConnectOption(SQLHDBC ConnectionHandle, SQLUSMALLINT Option, + SQLPOINTER Value); +SQLRETURN SQL_API SQLGetCursorName(SQLHSTMT StatementHandle, SQLCHAR* CursorName, + SQLSMALLINT BufferLength, SQLSMALLINT* NameLengthPtr); +SQLRETURN SQL_API SQLGetData(SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, + SQLSMALLINT TargetType, SQLPOINTER TargetValue, SQLLEN BufferLength, + SQLLEN* StrLen_or_IndPtr); +SQLRETURN SQL_API SQLGetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength, SQLINTEGER* StringLength); +SQLRETURN SQL_API SQLGetDescRec(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, SQLCHAR* Name, + SQLSMALLINT BufferLength, SQLSMALLINT* StringLengthPtr, + SQLSMALLINT* TypePtr, SQLSMALLINT* SubTypePtr, SQLLEN* LengthPtr, + SQLSMALLINT* PrecisionPtr, SQLSMALLINT* ScalePtr, + SQLSMALLINT* NullablePtr); +SQLRETURN SQL_API SQLGetDiagField(SQLSMALLINT HandleType, SQLHANDLE Handle, SQLSMALLINT RecNumber, + SQLSMALLINT DiagIdentifier, SQLPOINTER DiagInfoPtr, + SQLSMALLINT BufferLength, SQLSMALLINT* StringLengthPtr); +SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT HandleType, SQLHANDLE Handle, SQLSMALLINT RecNumber, + SQLCHAR* Sqlstate, SQLINTEGER* NativeError, SQLCHAR* MessageText, + SQLSMALLINT BufferLength, SQLSMALLINT* TextLength); +SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV EnvironmentHandle, SQLINTEGER Attribute, SQLPOINTER Value, + SQLINTEGER BufferLength, SQLINTEGER* StringLength); +SQLRETURN SQL_API SQLGetFunctions(SQLHDBC ConnectionHandle, SQLUSMALLINT FunctionId, + SQLUSMALLINT* Supported); +SQLRETURN SQL_API SQLGetInfo(SQLHDBC ConnectionHandle, SQLUSMALLINT InfoType, SQLPOINTER InfoValue, + SQLSMALLINT BufferLength, SQLSMALLINT* StringLengthPtr); +SQLRETURN SQL_API SQLGetStmtAttr(SQLHSTMT StatementHandle, SQLINTEGER Attribute, SQLPOINTER Value, + SQLINTEGER BufferLength, SQLINTEGER* StringLength); +SQLRETURN SQL_API SQLGetStmtOption(SQLHSTMT StatementHandle, SQLUSMALLINT Option, SQLPOINTER Value); +SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT StatementHandle, SQLSMALLINT DataType); +SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT StatementHandle, SQLSMALLINT* ColumnCount); +SQLRETURN SQL_API SQLNumParams(SQLHSTMT StatementHandle, SQLSMALLINT* ParameterCountPtr); +SQLRETURN SQL_API SQLNativeSql(SQLHDBC ConnectionHandle, SQLCHAR* InStatementText, + SQLINTEGER TextLength1, SQLCHAR* OutStatementText, + SQLINTEGER BufferLength, SQLINTEGER* TextLength2Ptr); +SQLRETURN SQL_API SQLParamData(SQLHSTMT StatementHandle, SQLPOINTER* Value); +SQLRETURN SQL_API SQLPrepare(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength); +SQLRETURN SQL_API SQLPutData(SQLHSTMT StatementHandle, SQLPOINTER Data, SQLLEN StrLen_or_Ind); +SQLRETURN SQL_API SQLRowCount(SQLHSTMT StatementHandle, SQLLEN* RowCount); +SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC ConnectionHandle, SQLINTEGER Attribute, + SQLPOINTER Value, SQLINTEGER StringLength); +SQLRETURN SQL_API SQLSetConnectOption(SQLHDBC ConnectionHandle, SQLUSMALLINT Option, SQLULEN Value); +SQLRETURN SQL_API SQLSetCursorName(SQLHSTMT StatementHandle, SQLCHAR* CursorName, + SQLSMALLINT NameLength); +SQLRETURN SQL_API SQLSetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength); +SQLRETURN SQL_API SQLSetDescRec(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, SQLSMALLINT Type, + SQLSMALLINT SubType, SQLLEN Length, SQLSMALLINT Precision, + SQLSMALLINT Scale, SQLPOINTER Data, SQLLEN* StringLength, + SQLLEN* Indicator); +SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV EnvironmentHandle, SQLINTEGER Attribute, SQLPOINTER Value, + SQLINTEGER StringLength); +SQLRETURN SQL_API SQLSetParam(SQLHSTMT StatementHandle, SQLUSMALLINT ParameterNumber, + SQLSMALLINT ValueType, SQLSMALLINT ParameterType, + SQLULEN LengthPrecision, SQLSMALLINT ParameterScale, + SQLPOINTER ParameterValue, SQLLEN* StrLen_or_Ind); +SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT StatementHandle, SQLINTEGER Attribute, SQLPOINTER Value, + SQLINTEGER StringLength); +SQLRETURN SQL_API SQLSetStmtOption(SQLHSTMT StatementHandle, SQLUSMALLINT Option, SQLULEN Value); +SQLRETURN SQL_API SQLSpecialColumns(SQLHSTMT StatementHandle, SQLUSMALLINT IdentifierType, + SQLCHAR* CatalogName, SQLSMALLINT NameLength1, + SQLCHAR* SchemaName, SQLSMALLINT NameLength2, + SQLCHAR* TableName, SQLSMALLINT NameLength3, SQLUSMALLINT Scope, + SQLUSMALLINT Nullable); +SQLRETURN SQL_API SQLStatistics(SQLHSTMT StatementHandle, SQLCHAR* CatalogName, + SQLSMALLINT NameLength1, SQLCHAR* SchemaName, + SQLSMALLINT NameLength2, SQLCHAR* TableName, + SQLSMALLINT NameLength3, SQLUSMALLINT Unique, + SQLUSMALLINT Reserved); +SQLRETURN SQL_API SQLTables(SQLHSTMT StatementHandle, SQLCHAR* CatalogName, SQLSMALLINT NameLength1, + SQLCHAR* SchemaName, SQLSMALLINT NameLength2, SQLCHAR* TableName, + SQLSMALLINT NameLength3, SQLCHAR* TableType, SQLSMALLINT NameLength4); +SQLRETURN SQL_API SQLTransact(SQLHENV EnvironmentHandle, SQLHDBC ConnectionHandle, + SQLUSMALLINT CompletionType); + +// IoTDB functions +} +SQLRETURN IoTDB_ExecDirect(StatementHandle* const stmt, const std::string& statementText, + bool saveStatementText = true); + +#endif //DRIVER_H diff --git a/iotdb-client/odbc/src/install_driver.cpp b/iotdb-client/odbc/src/install_driver.cpp new file mode 100644 index 0000000000000..d9b5a4b77cc85 --- /dev/null +++ b/iotdb-client/odbc/src/install_driver.cpp @@ -0,0 +1,152 @@ +/** + * 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 + +#ifdef _WIN32 +#include +#endif + +#include + +static const char* DRIVER_NAME = "Apache IoTDB ODBC Driver"; +static const char* DRIVER_DESCRIPTION = "Apache IoTDB ODBC Driver"; + +static void PrintInstallerError() { + DWORD errCode = 0; + char errMsg[512] = {}; + SQLInstallerError(1, &errCode, errMsg, sizeof(errMsg), nullptr); + fprintf(stderr, " ODBC Installer error [%lu]: %s\n", errCode, errMsg); +} + +static int InstallDriver(const char* dllPath) { + DWORD usageCount = 0; + +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesA(dllPath); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY)) { + fprintf(stderr, "Driver DLL does not exist: %s\n", dllPath); + return 1; + } +#endif + + printf("Removing previous registration of '%s' (if any)...\n", DRIVER_NAME); + SQLRemoveDriver(DRIVER_NAME, FALSE, &usageCount); + + std::string driverDir; + std::string fullPath(dllPath); +#ifdef _WIN32 + size_t lastSep = fullPath.find_last_of("\\/"); +#else + size_t lastSep = fullPath.find_last_of('/'); +#endif + if (lastSep != std::string::npos) + driverDir = fullPath.substr(0, lastSep); + else + driverDir = "."; + + std::string driverDescr; + auto appendEntry = [&](const std::string& entry) { + driverDescr.append(entry); + driverDescr.push_back('\0'); + }; + appendEntry(DRIVER_NAME); + appendEntry("Driver=" + fullPath); + appendEntry("Setup=" + fullPath); + appendEntry(std::string("Description=") + DRIVER_DESCRIPTION); + appendEntry("DriverODBCVer=03.80"); + appendEntry("ConnectFunctions=YYN"); + appendEntry("APILevel=1"); + appendEntry("SQLLevel=1"); + appendEntry("FileUsage=0"); + driverDescr.push_back('\0'); + + char outPath[512] = {}; + printf("Installing driver '%s'\n", DRIVER_NAME); + printf(" DLL: %s\n", dllPath); + printf(" Dir: %s\n", driverDir.c_str()); + + if (!SQLInstallDriverEx(driverDescr.c_str(), driverDir.c_str(), outPath, sizeof(outPath), nullptr, + ODBC_INSTALL_COMPLETE, nullptr)) { + fprintf(stderr, "Failed to install driver.\n"); + PrintInstallerError(); + return 1; + } + + printf("Driver installed successfully.\n"); + printf(" Registered at: %s\n", outPath); + return 0; +} + +static int UninstallDriver(bool removeDSNs) { + DWORD usageCount = 0; + + printf("Removing driver '%s'%s...\n", DRIVER_NAME, removeDSNs ? " and associated DSNs" : ""); + + if (!SQLRemoveDriver(DRIVER_NAME, removeDSNs ? TRUE : FALSE, &usageCount)) { + fprintf(stderr, "Failed to remove driver.\n"); + PrintInstallerError(); + return 1; + } + + printf("Driver removed successfully. Remaining usage count: %lu\n", usageCount); + if (usageCount > 0) + printf(" Note: Driver entry still exists (usage count > 0).\n"); + + return 0; +} + +static void PrintUsage(const char* progName) { + printf("Apache IoTDB ODBC Driver Installer\n\n"); + printf("Usage:\n"); + printf(" %s install \n", progName); + printf(" Register the ODBC driver using the specified DLL path.\n\n"); + printf(" %s uninstall\n", progName); + printf(" Unregister the ODBC driver.\n\n"); + printf(" %s uninstall --remove-dsn\n", progName); + printf(" Unregister the driver and remove all associated DSNs.\n\n"); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + PrintUsage(argv[0]); + return 1; + } + + std::string action(argv[1]); + + if (action == "install") { + if (argc < 3) { + fprintf(stderr, "Error: DLL path is required for install.\n\n"); + PrintUsage(argv[0]); + return 1; + } + return InstallDriver(argv[2]); + } else if (action == "uninstall") { + bool removeDSNs = false; + if (argc >= 3 && strcmp(argv[2], "--remove-dsn") == 0) + removeDSNs = true; + return UninstallDriver(removeDSNs); + } else { + fprintf(stderr, "Error: Unknown action '%s'.\n\n", argv[1]); + PrintUsage(argv[0]); + return 1; + } +} diff --git a/iotdb-client/odbc/src/rest_api_client.cpp b/iotdb-client/odbc/src/rest_api_client.cpp new file mode 100644 index 0000000000000..4f3c2b9bd01ea --- /dev/null +++ b/iotdb-client/odbc/src/rest_api_client.cpp @@ -0,0 +1,385 @@ +/** + * 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 "Pch.h" +#include "rest_api_client.h" + +// Additional third-party includes not in Pch.h +#include + +// Project includes +#include "Log.h" +#include "driver.h" +#include "DiagnosticManager.h" +#include "StatementUtils.h" + +namespace { +class CurlGlobalState { +public: + CurlGlobalState() : result(curl_global_init(CURL_GLOBAL_ALL)) {} + ~CurlGlobalState() { + if (result == CURLE_OK) + curl_global_cleanup(); + } + + CURLcode result; +}; + +struct CurlEasyDeleter { + void operator()(CURL* handle) const { + if (handle) + curl_easy_cleanup(handle); + } +}; + +struct CurlHeadersDeleter { + void operator()(curl_slist* headers) const { + if (headers) + curl_slist_free_all(headers); + } +}; +} // namespace + +// Callback function for writing received data +size_t WriteCallback(void* contents, const size_t size, const size_t nmemb, std::string* response) { + const size_t totalSize = size * nmemb; + response->append(static_cast(contents), totalSize); + return totalSize; +} + +SQLRETURN executeRestCall(ConnectionHandle* cnct, const std::string& relativeUrl, + const nlohmann::json& jsonPayload, nlohmann::json* jsonResponse) { + logMessage(cnct, " Entering executeRestCall", LOG_LEVEL_TRACE); + + static CurlGlobalState curlGlobal; + if (curlGlobal.result != CURLE_OK) { + logMessage(cnct, "Curl global initialization failed", LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Couldn't initialize the REST client"); + return SQL_ERROR; + } + + std::unique_ptr curl(curl_easy_init()); + if (!curl) { + logMessage(cnct, "Curl initialization failed", LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Couldn't initialize the REST client"); + return SQL_ERROR; + } + + try { + const std::string fullUrl = + "http://" + cnct->serverHostName + ":" + cnct->serverPort + relativeUrl; + logMessage(cnct, "Connecting to: " + fullUrl, LOG_LEVEL_DEBUG); + curl_easy_setopt(curl.get(), CURLOPT_URL, fullUrl.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl.get(), CURLOPT_HTTPAUTH, static_cast(CURLAUTH_BASIC)); + curl_easy_setopt(curl.get(), CURLOPT_USERNAME, cnct->userName.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_PASSWORD, cnct->password.c_str()); + + std::unique_ptr headers( + curl_slist_append(nullptr, "Content-Type: application/json")); + if (!headers) { + cnct->addDiagnostic("HY001", "Couldn't allocate REST request headers"); + return SQL_ERROR; + } + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers.get()); + + const std::string jsonString = jsonPayload.dump(); + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, jsonString.c_str()); + + std::string response; + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); + + const CURLcode result = curl_easy_perform(curl.get()); + if (result != CURLE_OK) { + logMessage(cnct, curl_easy_strerror(result), LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "The specified server URL could not be reached."); + return SQL_ERROR; + } + + long httpCode = 0; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &httpCode); + logMessage(cnct, "Http response code: " + std::to_string(httpCode), LOG_LEVEL_DEBUG); + if (httpCode != 200) { + logMessage(cnct, "Got an invalid response code from the server", LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "Invalid response"); + return SQL_ERROR; + } + + try { + *jsonResponse = nlohmann::json::parse(response); + } catch (const nlohmann::json::parse_error& e) { + logMessage(cnct, "Failed to parse JSON response: " + std::string(e.what()), LOG_LEVEL_ERROR); + cnct->addDiagnostic("22018", "Failed to parse JSON response: " + std::string(e.what())); + return SQL_ERROR; + } + } catch (const std::exception& e) { + logMessage(cnct, "Unexpected error: " + std::string(e.what()), LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Unexpected error: " + std::string(e.what())); + return SQL_ERROR; + } + + logMessage(cnct, "Exiting executeRestCall", LOG_LEVEL_TRACE); + return SQL_SUCCESS; +} + +// Execute a simple query or modification statement directly through connection handle and return json data, no statement handle involved. +SQLRETURN executeQuery(ConnectionHandle* cnct, std::string statementText, + nlohmann::json* jsonResponse) { + logMessage(cnct, "executeQuery: Entering", LOG_LEVEL_TRACE); + bool isQuery = IsQueryStatement(statementText); + + std::string logMessageText = + "executeQuery: is " + std::string(isQuery ? "query" : "nonQuery") + "."; + logMessage(cnct, logMessageText, LOG_LEVEL_DEBUG); + + nlohmann::json jsonPayload; + if (cnct->isTableModel) { + if (cnct->database == "") { + logMessage(cnct, "executeQuery: In table model mode, but no database is set", + LOG_LEVEL_ERROR); + return SQL_ERROR; + } + jsonPayload = {{"database", cnct->database}, {"sql", statementText}}; + } else { + jsonPayload = {{"sql", statementText}}; + } + const std::string relativeUrl = std::string(cnct->isTableModel ? "/rest/table/v1" : "/rest/v2") + + std::string(isQuery ? "/query" : "/nonQuery"); + logMessage(cnct, "Target URL: " + relativeUrl, LOG_LEVEL_DEBUG); + const SQLRETURN returnCode = executeRestCall(cnct, relativeUrl, jsonPayload, jsonResponse); + logMessage(cnct, "Exiting executeQuery", LOG_LEVEL_TRACE); + return returnCode; +} + +// Test RESTful API connection and return connection result +SQLRETURN IoTDB_DriverConnect_Rest(ConnectionHandle* cnct) { + try { + cnct->ValidateTransport(); + } catch (const std::exception& e) { + cnct->addDiagnostic("08001", e.what()); + return SQL_ERROR; + } + logMessage(cnct, "IoTDB_DriverConnect_Rest: Testing connection with SHOW VERSION query", + LOG_LEVEL_TRACE); + nlohmann::json jsonPayload; + if (cnct->isTableModel) { + jsonPayload = {{"database", cnct->database}, {"sql", "SHOW VERSION"}}; + } else { + jsonPayload = {{"sql", "SHOW VERSION"}}; // Use SHOW VERSION to test connection + } + nlohmann::json jsonResponse; + std::string relativeUrl = cnct->isTableModel ? "/rest/table/v1/query" : "/rest/v2/query"; + SQLRETURN returnCode = executeRestCall(cnct, relativeUrl, jsonPayload, &jsonResponse); + + if (returnCode == SQL_SUCCESS) { + logMessage(cnct, "IoTDB_DriverConnect_Rest: Received successful HTTP response", + LOG_LEVEL_DEBUG); + try { + auto responseCode = jsonResponse.value("code", 200); + switch (responseCode) { + case 200: + logMessage(cnct, "IoTDB_DriverConnect_Rest: Connection test successful", LOG_LEVEL_INFO); + returnCode = SQL_SUCCESS; + break; + + case 708: + logMessage(cnct, "IoTDB_DriverConnect_Rest: Response too large", LOG_LEVEL_ERROR); + cnct->addDiagnostic("28000", "Response too large"); + returnCode = SQL_ERROR; + break; + + case 411: + logMessage(cnct, "IoTDB_DriverConnect_Rest: Data requires streaming transmission", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("28000", "Data requires streaming transmission for connection test"); + returnCode = SQL_ERROR; + break; + + case 801: + logMessage(cnct, "IoTDB_DriverConnect_Rest: Invalid credentials", LOG_LEVEL_ERROR); + cnct->addDiagnostic("28000", "Invalid username or password"); + returnCode = SQL_ERROR; + break; + + case 700: + logMessage(cnct, "IoTDB_DriverConnect_Rest: SQL syntax error in connection test", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("42000", "SQL syntax error in connection test query"); + returnCode = SQL_ERROR; + break; + + default: + if (responseCode >= 200 && responseCode < 300) { + logMessage(cnct, + "IoTDB_DriverConnect_Rest: Connection test successful with response code " + + std::to_string(responseCode), + LOG_LEVEL_INFO); + returnCode = SQL_SUCCESS; + } else { + logMessage(cnct, + "IoTDB_DriverConnect_Rest: Unhandled response code: " + + std::to_string(responseCode), + LOG_LEVEL_ERROR); + cnct->addDiagnostic("28000", "Unexpected return code " + std::to_string(responseCode)); + returnCode = SQL_ERROR; + } + break; + } + } catch (const std::exception& e) { + logMessage( + cnct, "IoTDB_DriverConnect_Rest: Exception processing response: " + std::string(e.what()), + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Exception processing response: " + std::string(e.what())); + returnCode = SQL_ERROR; + } catch (...) { + logMessage(cnct, "IoTDB_DriverConnect_Rest: Unknown exception processing response", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Unknown exception processing response"); + returnCode = SQL_ERROR; + } + } else { + logMessage(cnct, + "IoTDB_DriverConnect_Rest: Connection test failed with code: " + + std::to_string(returnCode), + LOG_LEVEL_ERROR); + } + + logMessage(cnct, + "IoTDB_DriverConnect_Rest: Exiting with return code: " + std::to_string(returnCode), + LOG_LEVEL_TRACE); + return returnCode; +} + +// Execute direct statement execution for RESTful API +SQLRETURN IoTDB_ExecDirect_Rest(StatementHandle* stmt, const std::string& statementText) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "IoTDB_ExecDirect_Rest: Entering", LOG_LEVEL_TRACE); + + bool isQuery = IsQueryStatement(statementText); + // Do not log SQL text: statements can contain credentials or user data. + if (isLogLevelEnabled(cnct, LOG_LEVEL_DEBUG)) { + const std::string logMessageText = + "IoTDB_ExecDirect_Rest: Statement type = " + std::string(isQuery ? "query" : "nonQuery"); + logMessage(cnct, logMessageText, LOG_LEVEL_DEBUG); + } + + nlohmann::json jsonPayload; + if (stmt->getConnection()->isTableModel) { + if (stmt->getConnection()->database == "") { + logMessage(cnct, "IoTDB_ExecDirect_Rest: Table model requires database name but none is set", + LOG_LEVEL_ERROR); + return SQL_ERROR; + } + jsonPayload = {{"database", stmt->getConnection()->database}, {"sql", statementText}}; + } else { + jsonPayload = {{"sql", statementText}}; + } + + nlohmann::json jsonResponse; + const std::string relativeUrl = + std::string(stmt->getConnection()->isTableModel ? "/rest/table/v1" : "/rest/v2") + + std::string(isQuery ? "/query" : "/nonQuery"); + + std::string fullServerUrl = + "http://" + stmt->getConnection()->serverHostName + ":" + stmt->getConnection()->serverPort; + logMessage(cnct, + "IoTDB_ExecDirect_Rest: Calling executeRestCall with serverHostName: " + + fullServerUrl + "; relative URL: " + relativeUrl, + LOG_LEVEL_DEBUG); + const SQLRETURN returnCode = + executeRestCall(stmt->getConnection(), relativeUrl, jsonPayload, &jsonResponse); + if (returnCode == SQL_SUCCESS) { + logMessage(cnct, "IoTDB_ExecDirect_Rest: Received successful HTTP response", LOG_LEVEL_DEBUG); + try { + const auto responseCode = jsonResponse.value("code", 200); + logMessage(cnct, "IoTDB_ExecDirect_Rest: Response code = " + std::to_string(responseCode), + LOG_LEVEL_DEBUG); + switch (responseCode) { + case 200: { + logMessage(cnct, "IoTDB_ExecDirect_Rest: Request completed successfully", LOG_LEVEL_INFO); + stmt->isQuery = isQuery; + if (!isQuery) { + stmt->ClearResultSet(); + return SQL_SUCCESS; + } + stmt->AllocateRestResultSet(); + stmt->resultSetPtr->loadData(jsonResponse); + if (stmt->isStream && stmt->resultSetPtr->getNumRows() == 0) { + logMessage(cnct, "IoTDB_ExecDirect_Rest: Streaming completed (empty result set)", + LOG_LEVEL_INFO); + stmt->isStream = false; + // receiving an empty set means streaming is end. + return SQL_NO_DATA; + } + logMessage(cnct, "IoTDB_ExecDirect_Rest: Exiting with success", LOG_LEVEL_TRACE); + return SQL_SUCCESS; + } + case 801: + logMessage(cnct, "IoTDB_ExecDirect_Rest: Invalid credentials", LOG_LEVEL_ERROR); + + stmt->addDiagnostic("28000", "Invalid username or password"); + return SQL_ERROR; + + case 708: + case 411: + logMessage(cnct, "IoTDB_ExecDirect_Rest: Data requires streaming transmission", + LOG_LEVEL_INFO); + stmt->isQuery = true; + stmt->AllocateRestResultSet(); + stmt->resultSetPtr->loadData(jsonResponse); + stmt->isStream = true; + stmt->streamCurRow = 0; + return streamNextBatch(stmt); + logMessage(cnct, "IoTDB_ExecDirect_Rest: Exiting with streaming setup", LOG_LEVEL_TRACE); + return SQL_SUCCESS; + + case 700: + logMessage(cnct, "IoTDB_ExecDirect_Rest: Mismatched input in SQL statement", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("42000", "Mismatched input."); + return SQL_ERROR; + + default: + logMessage( + cnct, "IoTDB_ExecDirect_Rest: Unhandled response code: " + std::to_string(responseCode), + LOG_LEVEL_ERROR); + + stmt->addDiagnostic("28000", "Unexpected return code " + std::to_string(returnCode)); + return SQL_ERROR; + } + } catch (const std::exception& e) { + logMessage(cnct, + "IoTDB_ExecDirect_Rest: Exception processing response: " + std::string(e.what()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "Exception processing response: " + std::string(e.what())); + return SQL_ERROR; + } catch (...) { + logMessage(cnct, "IoTDB_ExecDirect_Rest: Unknown exception processing response", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "Unknown exception processing response"); + return SQL_ERROR; + } + } + + logMessage(cnct, + "IoTDB_ExecDirect_Rest: HTTP request failed with code: " + std::to_string(returnCode), + LOG_LEVEL_ERROR); + logMessage(cnct, "IoTDB_ExecDirect_Rest: Exiting with error", LOG_LEVEL_TRACE); + return returnCode; +} diff --git a/iotdb-client/odbc/src/rest_api_client.h b/iotdb-client/odbc/src/rest_api_client.h new file mode 100644 index 0000000000000..f26a731806de4 --- /dev/null +++ b/iotdb-client/odbc/src/rest_api_client.h @@ -0,0 +1,74 @@ +/** + * 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. + */ +#ifndef REST_API_CLIENT_H +#define REST_API_CLIENT_H + +#include "Pch.h" + +// Forward declarations to avoid circular dependencies +class ConnectionHandle; +class StatementHandle; + +// CURL callback function for writing response data +size_t WriteCallback(void* contents, const size_t size, const size_t nmemb, std::string* response); + +// Execute RESTful API call +// @param cnct connection handle +// @param relativeUrl relative URL path +// @param jsonPayload JSON request payload +// @param jsonResponse output parameter for storing JSON response +// @return SQLRETURN return code +SQLRETURN executeRestCall(ConnectionHandle* cnct, const std::string& relativeUrl, + const nlohmann::json& jsonPayload, nlohmann::json* jsonResponse); + +// Execute a simple query or modification statement directly through connection handle and return json data, no statement handle involved +// @param cnct connection handle +// @param statementText SQL statement text +// @param jsonResponse output parameter for storing JSON response +// @return SQLRETURN return code +SQLRETURN executeQuery(ConnectionHandle* cnct, std::string statementText, + nlohmann::json* jsonResponse); + +// Test RESTful API connection and return connection result +// @param cnct connection handle +// @return SQLRETURN return code +SQLRETURN IoTDB_DriverConnect_Rest(ConnectionHandle* cnct); + +// Execute direct statement execution for RESTful API +// @param stmt statement handle +// @param statementText SQL statement text +// @return SQLRETURN return code +SQLRETURN IoTDB_ExecDirect_Rest(StatementHandle* stmt, const std::string& statementText); + +// Save curl response data for tree model +// @param stmt statement handle +// @param jsonResponse JSON response data +void saveCurlResponseTree(StatementHandle* stmt, const nlohmann::json& jsonResponse); + +// Save curl response data for table model +// @param stmt statement handle +// @param jsonResponse JSON response data +void saveCurlResponseTable(StatementHandle* stmt, const nlohmann::json& jsonResponse); + +// Save curl response data (automatically select based on model type) +// @param stmt statement handle +// @param jsonResponse JSON response data +void saveCurlResponse(StatementHandle* stmt, const nlohmann::json& jsonResponse); + +#endif // REST_API_CLIENT_H diff --git a/iotdb-client/odbc/src/session_api_client.cpp b/iotdb-client/odbc/src/session_api_client.cpp new file mode 100644 index 0000000000000..ad656cdeb3551 --- /dev/null +++ b/iotdb-client/odbc/src/session_api_client.cpp @@ -0,0 +1,238 @@ +/** + * 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 "Pch.h" +#include "session_api_client.h" + +// Project includes +#include "Log.h" +#include "driver.h" +#include "DiagnosticManager.h" +#include "ODBCResultSet.h" +#include "StatementUtils.h" + +// IoTDB Session API includes +#include +#include +#include +#include +#include + +// Forward declarations for functions defined in driver.cpp +SQLRETURN IoTDB_ExecDirect_Session(StatementHandle* stmt, const std::string& statementText); + +// Test Session API connection and return connection result +SQLRETURN IoTDB_DriverConnect_Session(ConnectionHandle* cnct) { + logMessage(cnct, "IoTDB_DriverConnect_Session: Testing connection with SHOW VERSION query", + LOG_LEVEL_TRACE); + + try { + // Parse server port from string to int + int rpcPort = 6667; // Default port + try { + rpcPort = std::stoi(cnct->serverPort); + logMessage(cnct, + "IoTDB_DriverConnect_Session: Using configured port: " + std::to_string(rpcPort), + LOG_LEVEL_DEBUG); + } catch (...) { + logMessage(cnct, + "IoTDB_DriverConnect_Session: Invalid port in serverPort (" + cnct->serverPort + + "), using default 6667", + LOG_LEVEL_WARN); + } + + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) { + std::string tmp; + tmp = "Server URL: " + cnct->serverHostName + ", Port: " + std::to_string(rpcPort) + + ", User: " + cnct->userName + ", Database: " + cnct->database + + ", Table Model: " + (cnct->isTableModel ? "true" : "false"); + logMessage(cnct, "IoTDB_DriverConnect_Session: All connection paras: " + tmp, + LOG_LEVEL_TRACE); + } + + // Create session + std::shared_ptr tableSession = nullptr; + std::shared_ptr session = nullptr; + if (cnct->isTableModel) { + if (cnct->database.empty()) { + logMessage( + cnct, "IoTDB_DriverConnect_Session: Table model requires database name but none is set", + LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "Database not specified for table model"); + return SQL_ERROR; + } + logMessage(cnct, "IoTDB_DriverConnect_Session: Creating table session", LOG_LEVEL_TRACE); + cnct->OpenSession(); + tableSession = cnct->tableSessionPtr; + } else { + logMessage(cnct, "IoTDB_DriverConnect_Session: Creating tree session", LOG_LEVEL_TRACE); + cnct->OpenSession(); + session = cnct->sessionPtr; + } + + // Test connection with SHOW VERSION query + std::unique_ptr dataSet = + cnct->isTableModel + ? tableSession->executeQueryStatement("SHOW VERSION", cnct->sessionTimeoutMs) + : session->executeQueryStatement("SHOW VERSION", cnct->sessionTimeoutMs); + + if (dataSet && dataSet->hasNext()) { + logMessage(cnct, "IoTDB_DriverConnect_Session: Connection test successful", LOG_LEVEL_INFO); + dataSet->closeOperationHandle(); + return SQL_SUCCESS; + } else { + logMessage(cnct, "IoTDB_DriverConnect_Session: Connection test failed - no data returned", + LOG_LEVEL_ERROR); + cnct->CloseSession(); + cnct->addDiagnostic("08001", "Connection test failed"); + return SQL_ERROR; + } + + } catch (const IoTDBConnectionException& e) { + logMessage(cnct, "IoTDB_DriverConnect_Session: Connection exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "Connection failed: " + std::string(e.what())); + return SQL_ERROR; + } catch (const IoTDBException& e) { + logMessage(cnct, "IoTDB_DriverConnect_Session: IoTDB exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + cnct->addDiagnostic("08001", "Query execution failed: " + std::string(e.what())); + return SQL_ERROR; + } catch (const std::exception& e) { + logMessage(cnct, "IoTDB_DriverConnect_Session: Unexpected exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + cnct->addDiagnostic("HY000", "Unexpected error: " + std::string(e.what())); + return SQL_ERROR; + } + + logMessage(cnct, "IoTDB_DriverConnect_Session: Exiting with error", LOG_LEVEL_TRACE); + return SQL_ERROR; +} + +// Execute direct statement execution for Session API +SQLRETURN IoTDB_ExecDirect_Session(StatementHandle* stmt, const std::string& statementText) { + ConnectionHandle* cnct = stmt->getConnection(); + logMessage(cnct, "IoTDB_ExecDirect_Session: Entering", LOG_LEVEL_TRACE); + + bool isQuery = IsQueryStatement(statementText); + // Do not log SQL text: statements can contain credentials or user data. + if (isLogLevelEnabled(cnct, LOG_LEVEL_DEBUG)) { + const std::string logMessageText = + "IoTDB_ExecDirect_Session: Statement type = " + std::string(isQuery ? "query" : "nonQuery"); + logMessage(cnct, logMessageText, LOG_LEVEL_DEBUG); + } + + try { + // Parse server port from string to int + int rpcPort = 6667; // Default port + try { + rpcPort = std::stoi(cnct->serverPort); + logMessage(cnct, + "IoTDB_ExecDirect_Session: Using configured port: " + std::to_string(rpcPort), + LOG_LEVEL_DEBUG); + } catch (...) { + logMessage(cnct, + "IoTDB_ExecDirect_Session: Invalid port in serverPort (" + cnct->serverPort + + "), using default 6667", + LOG_LEVEL_WARN); + } + + // Ensure session is created + stmt->ClearResultSet(); + if (cnct->isTableModel) { + if (cnct->database.empty()) { + logMessage(cnct, + "IoTDB_ExecDirect_Session: Table model requires database name but none is set", + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "Database not specified for table model"); + return SQL_ERROR; + } + if (!cnct->tableSessionPtr) { + logMessage(cnct, "IoTDB_ExecDirect_Session: Creating table session", LOG_LEVEL_TRACE); + cnct->OpenSession(); + } + } else { + if (!cnct->sessionPtr) { + logMessage(cnct, "IoTDB_ExecDirect_Session: Creating tree session", LOG_LEVEL_TRACE); + cnct->OpenSession(); + } + } + + if (isQuery) { + // Execute query statement + std::unique_ptr dataSet = + cnct->isTableModel + ? cnct->tableSessionPtr->executeQueryStatement(statementText, cnct->sessionTimeoutMs) + : cnct->sessionPtr->executeQueryStatement(statementText, cnct->sessionTimeoutMs); + // Process query results + if (dataSet) { + stmt->isQuery = true; + stmt->AllocateSessionResultSet(); + stmt->resultSetPtr->loadData(std::move(dataSet)); + logMessage(cnct, + "IoTDB_ExecDirect_Session: Query executed successfully with batch processing", + LOG_LEVEL_INFO); + if (isLogLevelEnabled(cnct, LOG_LEVEL_TRACE)) + stmt->resultSetPtr->outputTable(); + return SQL_SUCCESS; + } else { + logMessage(cnct, "IoTDB_ExecDirect_Session: Query returned no data", LOG_LEVEL_WARN); + stmt->isQuery = false; + stmt->ClearResultSet(); + return SQL_NO_DATA; + } + } else { + // Execute non-query statement + if (cnct->isTableModel) { + cnct->tableSessionPtr->executeNonQueryStatement(statementText); + } else { + cnct->sessionPtr->executeNonQueryStatement(statementText); + } + stmt->ClearResultSet(); // Clear any existing data + logMessage(cnct, "IoTDB_ExecDirect_Session: Non-query statement executed successfully", + LOG_LEVEL_INFO); + return SQL_SUCCESS; + } + + } catch (const IoTDBConnectionException& e) { + logMessage(cnct, "IoTDB_ExecDirect_Session: Connection exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("08001", "Connection failed: " + std::string(e.what())); + return SQL_ERROR; + } catch (const IoTDBException& e) { + logMessage(cnct, "IoTDB_ExecDirect_Session: IoTDB exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + + // Handle specific error codes if possible + std::string errorMsg = e.what(); + if (errorMsg.find("mismatched input") != std::string::npos) { + stmt->addDiagnostic("42000", "SQL syntax error: " + errorMsg); + } else { + stmt->addDiagnostic("HY000", "Query execution failed: " + errorMsg); + } + return SQL_ERROR; + } catch (const std::exception& e) { + logMessage(cnct, "IoTDB_ExecDirect_Session: Unexpected exception: " + std::string(e.what()), + LOG_LEVEL_ERROR); + stmt->addDiagnostic("HY000", "Unexpected error: " + std::string(e.what())); + return SQL_ERROR; + } + + logMessage(cnct, "IoTDB_ExecDirect_Session: Exiting with error", LOG_LEVEL_TRACE); + return SQL_ERROR; +} diff --git a/iotdb-client/odbc/src/session_api_client.h b/iotdb-client/odbc/src/session_api_client.h new file mode 100644 index 0000000000000..9912a8ec7f830 --- /dev/null +++ b/iotdb-client/odbc/src/session_api_client.h @@ -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. + */ +#ifndef SESSION_API_CLIENT_H +#define SESSION_API_CLIENT_H + +#include +#include +#include "Pch.h" + +// Forward declarations +class ConnectionHandle; +class StatementHandle; +class TableSession; +class SessionDataSet; + +// Test Session API connection and return connection result +SQLRETURN IoTDB_DriverConnect_Session(ConnectionHandle* cnct); + +// Execute direct statement execution for Session API +SQLRETURN IoTDB_ExecDirect_Session(StatementHandle* stmt, const std::string& statementText); + +#endif // SESSION_API_CLIENT_H diff --git a/iotdb-client/odbc/src/setup/resource.h b/iotdb-client/odbc/src/setup/resource.h new file mode 100644 index 0000000000000..a778b7a92dfeb --- /dev/null +++ b/iotdb-client/odbc/src/setup/resource.h @@ -0,0 +1,45 @@ +/** + * 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. + */ +#ifndef SETUP_RESOURCE_H +#define SETUP_RESOURCE_H + +#define IDD_DSN_DIALOG 101 + +#define IDC_STATIC -1 +#define IDC_DSN_NAME 1001 +#define IDC_DSN_DESCRIPTION 1002 +#define IDC_SERVER 1003 +#define IDC_PORT 1004 +#define IDC_UID 1005 +#define IDC_PWD 1006 +#define IDC_DATABASE 1007 +#define IDC_TABLE_MODEL 1008 +#define IDC_LOG_LEVEL 1009 +#define IDC_SESSION_TIMEOUT 1010 +#define IDC_BATCH_SIZE 1011 +#define IDC_BTN_TEST 1012 +#define IDC_DATABASE_HINT 1013 +#define IDC_SSL 1014 +#define IDC_SSLCA 1015 +#define IDC_SSLCERT 1016 +#define IDC_SSLKEY 1017 +#define IDC_BTN_OK IDOK +#define IDC_BTN_CANCEL IDCANCEL + +#endif // SETUP_RESOURCE_H diff --git a/iotdb-client/odbc/src/setup/setup.rc b/iotdb-client/odbc/src/setup/setup.rc new file mode 100644 index 0000000000000..9f4dd3ee01689 --- /dev/null +++ b/iotdb-client/odbc/src/setup/setup.rc @@ -0,0 +1,78 @@ +/** + * 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 "resource.h" + +IDD_DSN_DIALOG DIALOGEX 0, 0, 300, 440 +STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Apache IoTDB ODBC Data Source Configuration" +FONT 9, "Segoe UI", 400, 0, 0x0 +BEGIN + GROUPBOX "Data Source", IDC_STATIC, 7, 7, 286, 48 + LTEXT "DSN Name:", IDC_STATIC, 14, 22, 60, 8 + EDITTEXT IDC_DSN_NAME, 80, 20, 206, 14, ES_AUTOHSCROLL + LTEXT "Description:", IDC_STATIC, 14, 38, 60, 8 + EDITTEXT IDC_DSN_DESCRIPTION, 80, 36, 206, 14, ES_AUTOHSCROLL + + GROUPBOX "Connection", IDC_STATIC, 7, 60, 286, 68 + LTEXT "Server:", IDC_STATIC, 14, 76, 60, 8 + EDITTEXT IDC_SERVER, 80, 74, 140, 14, ES_AUTOHSCROLL + LTEXT "Port:", IDC_STATIC, 228, 76, 18, 8 + EDITTEXT IDC_PORT, 248, 74, 38, 14, ES_AUTOHSCROLL | ES_NUMBER + LTEXT "User:", IDC_STATIC, 14, 94, 60, 8 + EDITTEXT IDC_UID, 80, 92, 206, 14, ES_AUTOHSCROLL + LTEXT "Password:", IDC_STATIC, 14, 112, 60, 8 + EDITTEXT IDC_PWD, 80, 110, 206, 14, ES_PASSWORD | ES_AUTOHSCROLL + + GROUPBOX "Options", IDC_STATIC, 7, 134, 286, 178 + AUTOCHECKBOX "Table Model (uncheck for Tree Model)", IDC_TABLE_MODEL, 14, 150, 200, 10 + LTEXT "Database:", IDC_STATIC, 14, 168, 60, 8 + EDITTEXT IDC_DATABASE, 80, 166, 140, 14, ES_AUTOHSCROLL + LTEXT "(Table Model only)", IDC_DATABASE_HINT, 224, 168, 66, 8 + + LTEXT "Log Level:", IDC_STATIC, 14, 190, 60, 8 + EDITTEXT IDC_LOG_LEVEL, 80, 188, 28, 14, ES_AUTOHSCROLL | ES_NUMBER + LTEXT "0=ERROR 1=WARN 2=INFO 3=DEBUG 4=TRACE", IDC_STATIC, 114, 190, 170, 8 + + LTEXT "Session Timeout:", IDC_STATIC, 14, 212, 62, 8 + EDITTEXT IDC_SESSION_TIMEOUT, 80, 210, 60, 14, ES_AUTOHSCROLL | ES_NUMBER + LTEXT "ms (0 = no timeout; server default 60000ms)", IDC_STATIC, 144, 212, 148, 8 + + LTEXT "Batch Size:", IDC_STATIC, 14, 234, 60, 8 + EDITTEXT IDC_BATCH_SIZE, 80, 232, 60, 14, ES_AUTOHSCROLL | ES_NUMBER + LTEXT "rows per fetch (default: 1000)", IDC_STATIC, 144, 234, 120, 8 + + LTEXT "* Log Level controls the verbosity of driver log output.", IDC_STATIC, 14, 256, 270, 8 + LTEXT "* Session Timeout: server-side queryTimeoutThreshold", IDC_STATIC, 14, 268, 270, 8 + LTEXT " may also limit effective timeout.", IDC_STATIC, 14, 278, 270, 8 + LTEXT "* Batch Size: number of rows fetched per request.", IDC_STATIC, 14, 290, 270, 8 + LTEXT " 0 resets to default (1000).", IDC_STATIC, 14, 300, 270, 8 + + GROUPBOX "Session TLS (PEM files)", IDC_STATIC, 7, 316, 286, 90 + AUTOCHECKBOX "Enable TLS", IDC_SSL, 14, 330, 120, 10 + LTEXT "CA file:", IDC_STATIC, 14, 348, 60, 8 + EDITTEXT IDC_SSLCA, 80, 346, 206, 14, ES_AUTOHSCROLL + LTEXT "Client certificate:", IDC_STATIC, 14, 366, 65, 8 + EDITTEXT IDC_SSLCERT, 80, 364, 206, 14, ES_AUTOHSCROLL + LTEXT "Client key:", IDC_STATIC, 14, 384, 60, 8 + EDITTEXT IDC_SSLKEY, 80, 382, 206, 14, ES_AUTOHSCROLL + PUSHBUTTON "Test Connection", IDC_BTN_TEST, 7, 416, 70, 14 + DEFPUSHBUTTON "OK", IDOK, 176, 416, 50, 14 + PUSHBUTTON "Cancel", IDCANCEL, 233, 416, 50, 14 +END diff --git a/iotdb-client/odbc/src/setup/setup_dialog.cpp b/iotdb-client/odbc/src/setup/setup_dialog.cpp new file mode 100644 index 0000000000000..58541b2525a2c --- /dev/null +++ b/iotdb-client/odbc/src/setup/setup_dialog.cpp @@ -0,0 +1,464 @@ +/** + * 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 "setup_dialog.h" +#include "../ConnectionHandle.h" +#include "resource.h" +#include "../Log.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +static HINSTANCE GetCurrentModuleHandle() { + HMODULE hModule = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&GetCurrentModuleHandle), &hModule); + return static_cast(hModule); +} + +static std::string ParseDSNFromAttributes(LPCSTR lpszAttributes) { + if (!lpszAttributes) + return ""; + const char* p = lpszAttributes; + while (*p) { + std::string entry(p); + size_t eq = entry.find('='); + if (eq != std::string::npos) { + std::string key = entry.substr(0, eq); + std::string keyLower = key; + for (auto& c : keyLower) + c = static_cast(tolower(static_cast(c))); + if (keyLower == "dsn") + return entry.substr(eq + 1); + } + p += entry.length() + 1; + } + return ""; +} + +static std::string ReadOdbcIniString(const std::string& dsn, const char* key, const char* def) { + char buf[1024] = {}; + SQLGetPrivateProfileString(dsn.c_str(), key, def, buf, sizeof(buf), "ODBC.INI"); + return buf; +} + +// ─── DSN read / write via ODBC Installer API ─────────────────────────────── + +void ReadDsnFromOdbcIni(const std::string& dsnName, DsnInfo& info) { + logMessage(nullptr, "ReadDsnFromOdbcIni: Reading DSN '" + dsnName + "'", LOG_LEVEL_INFO); + + info.dsnName = dsnName; + info.description = ReadOdbcIniString(dsnName, "Description", ""); + info.server = ReadOdbcIniString(dsnName, "SERVER", "127.0.0.1"); + info.port = ReadOdbcIniString(dsnName, "PORT", "6667"); + info.uid = ReadOdbcIniString(dsnName, "UID", "root"); + info.pwd = ReadOdbcIniString(dsnName, "PWD", "root"); + info.database = ReadOdbcIniString(dsnName, "DATABASE", ""); + info.isTableModel = ReadOdbcIniString(dsnName, "ISTABLEMODEL", "1"); + info.logLevel = ReadOdbcIniString(dsnName, "LOGLEVEL", "0"); + info.sessionTimeoutMs = ReadOdbcIniString(dsnName, "SESSIONTIMEOUTMS", "0"); + info.batchSize = ReadOdbcIniString(dsnName, "BATCHSIZE", "1000"); + info.ssl = ReadOdbcIniString(dsnName, "SSL", "0"); + info.sslca = ReadOdbcIniString(dsnName, "SSLCA", ""); + info.sslcert = ReadOdbcIniString(dsnName, "SSLCERT", ""); + info.sslkey = ReadOdbcIniString(dsnName, "SSLKEY", ""); + + std::ostringstream oss; + oss << "ReadDsnFromOdbcIni: SERVER=" << info.server << ", PORT=" << info.port + << ", UID=" << info.uid << ", DATABASE=" << info.database + << ", ISTABLEMODEL=" << info.isTableModel << ", LOGLEVEL=" << info.logLevel + << ", SESSIONTIMEOUTMS=" << info.sessionTimeoutMs << ", BATCHSIZE=" << info.batchSize; + logMessage(nullptr, oss.str(), LOG_LEVEL_DEBUG); +} + +BOOL WriteDsnToOdbcIni(const DsnInfo& info, const std::string& driverName) { + logMessage(nullptr, "WriteDsnToOdbcIni: Writing DSN '" + info.dsnName + "'", LOG_LEVEL_INFO); + + if (info.dsnName.empty()) { + logMessage(nullptr, "WriteDsnToOdbcIni: DSN name is empty, aborting", LOG_LEVEL_ERROR); + return FALSE; + } + + if (!SQLValidDSN(info.dsnName.c_str())) { + logMessage(nullptr, "WriteDsnToOdbcIni: Invalid DSN name '" + info.dsnName + "'", + LOG_LEVEL_ERROR); + return FALSE; + } + + if (!SQLWriteDSNToIni(info.dsnName.c_str(), driverName.c_str())) { + logMessage(nullptr, "WriteDsnToOdbcIni: SQLWriteDSNToIni failed", LOG_LEVEL_ERROR); + return FALSE; + } + + const char* dsn = info.dsnName.c_str(); + bool success = true; + auto w = [&](const char* key, const std::string& val) { + if (!SQLWritePrivateProfileString(dsn, key, val.c_str(), "ODBC.INI")) + success = false; + }; + + w("Description", info.description); + w("SERVER", info.server); + w("PORT", info.port); + w("UID", info.uid); + w("PWD", info.pwd); + w("DATABASE", info.database); + w("ISTABLEMODEL", info.isTableModel); + w("LOGLEVEL", info.logLevel); + w("SESSIONTIMEOUTMS", info.sessionTimeoutMs); + w("BATCHSIZE", info.batchSize); + w("SSL", info.ssl); + w("SSLCA", info.sslca); + w("SSLCERT", info.sslcert); + w("SSLKEY", info.sslkey); + + if (!success) { + logMessage(nullptr, "WriteDsnToOdbcIni: Failed to write one or more DSN attributes", + LOG_LEVEL_ERROR); + return FALSE; + } + + std::ostringstream oss; + oss << "WriteDsnToOdbcIni: Written DSN '" << info.dsnName << "' with SERVER=" << info.server + << ", PORT=" << info.port << ", UID=" << info.uid << ", DATABASE=" << info.database + << ", ISTABLEMODEL=" << info.isTableModel << ", LOGLEVEL=" << info.logLevel + << ", SESSIONTIMEOUTMS=" << info.sessionTimeoutMs << ", BATCHSIZE=" << info.batchSize; + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + return TRUE; +} + +// ─── Dialog context passed via lParam ─────────────────────────────────────── + +struct DialogContext { + DsnInfo info; + std::string driverName; + BOOL isNewDsn; +}; + +// ─── Dialog Proc ──────────────────────────────────────────────────────────── + +static void UpdateDatabaseEnabled(HWND hDlg) { + BOOL isTable = (IsDlgButtonChecked(hDlg, IDC_TABLE_MODEL) == BST_CHECKED); + EnableWindow(GetDlgItem(hDlg, IDC_DATABASE), isTable); +} + +static void SetDialogFields(HWND hDlg, const DsnInfo& info, BOOL isNew) { + SetDlgItemTextA(hDlg, IDC_DSN_NAME, info.dsnName.c_str()); + SetDlgItemTextA(hDlg, IDC_DSN_DESCRIPTION, info.description.c_str()); + SetDlgItemTextA(hDlg, IDC_SERVER, info.server.c_str()); + SetDlgItemTextA(hDlg, IDC_PORT, info.port.c_str()); + SetDlgItemTextA(hDlg, IDC_UID, info.uid.c_str()); + SetDlgItemTextA(hDlg, IDC_PWD, info.pwd.c_str()); + SetDlgItemTextA(hDlg, IDC_DATABASE, info.database.c_str()); + CheckDlgButton(hDlg, IDC_TABLE_MODEL, + (info.isTableModel == "1" || info.isTableModel == "true") ? BST_CHECKED + : BST_UNCHECKED); + SetDlgItemTextA(hDlg, IDC_LOG_LEVEL, info.logLevel.c_str()); + SetDlgItemTextA(hDlg, IDC_SESSION_TIMEOUT, info.sessionTimeoutMs.c_str()); + SetDlgItemTextA(hDlg, IDC_BATCH_SIZE, info.batchSize.c_str()); + CheckDlgButton(hDlg, IDC_SSL, + (info.ssl == "1" || info.ssl == "true") ? BST_CHECKED : BST_UNCHECKED); + SetDlgItemTextA(hDlg, IDC_SSLCA, info.sslca.c_str()); + SetDlgItemTextA(hDlg, IDC_SSLCERT, info.sslcert.c_str()); + SetDlgItemTextA(hDlg, IDC_SSLKEY, info.sslkey.c_str()); + + EnableWindow(GetDlgItem(hDlg, IDC_DSN_NAME), isNew); + UpdateDatabaseEnabled(hDlg); +} + +static void GetDialogFields(HWND hDlg, DsnInfo& info) { + auto getText = [&](int id) -> std::string { + char buf[1024] = {}; + GetDlgItemTextA(hDlg, id, buf, sizeof(buf)); + return buf; + }; + + info.dsnName = getText(IDC_DSN_NAME); + info.description = getText(IDC_DSN_DESCRIPTION); + info.server = getText(IDC_SERVER); + info.port = getText(IDC_PORT); + info.uid = getText(IDC_UID); + info.pwd = getText(IDC_PWD); + info.database = getText(IDC_DATABASE); + info.isTableModel = (IsDlgButtonChecked(hDlg, IDC_TABLE_MODEL) == BST_CHECKED) ? "1" : "0"; + info.logLevel = getText(IDC_LOG_LEVEL); + info.sessionTimeoutMs = getText(IDC_SESSION_TIMEOUT); + info.batchSize = getText(IDC_BATCH_SIZE); + info.ssl = IsDlgButtonChecked(hDlg, IDC_SSL) == BST_CHECKED ? "1" : "0"; + info.sslca = getText(IDC_SSLCA); + info.sslcert = getText(IDC_SSLCERT); + info.sslkey = getText(IDC_SSLKEY); +} + +static INT_PTR CALLBACK DSNDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { + DialogContext* ctx = reinterpret_cast(GetWindowLongPtrA(hDlg, GWLP_USERDATA)); + + switch (message) { + case WM_INITDIALOG: { + ctx = reinterpret_cast(lParam); + SetWindowLongPtrA(hDlg, GWLP_USERDATA, reinterpret_cast(ctx)); + + logMessage(nullptr, + "DSNDialogProc: WM_INITDIALOG, DSN='" + ctx->info.dsnName + + "', isNew=" + (ctx->isNewDsn ? "true" : "false"), + LOG_LEVEL_INFO); + + SetDialogFields(hDlg, ctx->info, ctx->isNewDsn); + + logMessage(nullptr, "DSNDialogProc: Dialog fields populated", LOG_LEVEL_DEBUG); + return TRUE; + } + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: { + logMessage(nullptr, "DSNDialogProc: OK clicked", LOG_LEVEL_INFO); + + GetDialogFields(hDlg, ctx->info); + + if (ctx->info.dsnName.empty()) { + logMessage(nullptr, "DSNDialogProc: DSN name is empty", LOG_LEVEL_WARN); + MessageBoxA(hDlg, "DSN Name cannot be empty.", "Validation Error", MB_OK | MB_ICONWARNING); + SetFocus(GetDlgItem(hDlg, IDC_DSN_NAME)); + return TRUE; + } + + std::ostringstream oss; + oss << "DSNDialogProc: Saving DSN '" << ctx->info.dsnName << "', driver='" << ctx->driverName + << "'"; + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + if (!WriteDsnToOdbcIni(ctx->info, ctx->driverName)) { + logMessage(nullptr, "DSNDialogProc: WriteDsnToOdbcIni failed", LOG_LEVEL_ERROR); + MessageBoxA(hDlg, "Failed to save DSN configuration.", "Error", MB_OK | MB_ICONERROR); + return TRUE; + } + + logMessage(nullptr, "DSNDialogProc: DSN saved successfully", LOG_LEVEL_INFO); + EndDialog(hDlg, IDOK); + return TRUE; + } + + case IDCANCEL: + logMessage(nullptr, "DSNDialogProc: Cancel clicked", LOG_LEVEL_INFO); + EndDialog(hDlg, IDCANCEL); + return TRUE; + + case IDC_TABLE_MODEL: + UpdateDatabaseEnabled(hDlg); + return TRUE; + + case IDC_BTN_TEST: { + logMessage(nullptr, "DSNDialogProc: Test Connection clicked", LOG_LEVEL_INFO); + + DsnInfo tmp; + GetDialogFields(hDlg, tmp); + + bool isTable = (tmp.isTableModel == "1" || tmp.isTableModel == "true"); + + std::ostringstream oss; + oss << "DSNDialogProc: Test params - Server=" << tmp.server << ", Port=" << tmp.port + << ", UID=" << tmp.uid << ", Database=" << tmp.database + << ", TableModel=" << (isTable ? "true" : "false"); + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + if (isTable && tmp.database.empty()) { + MessageBoxA(hDlg, "Table Model requires a database name.", "Test Connection", + MB_OK | MB_ICONWARNING); + SetFocus(GetDlgItem(hDlg, IDC_DATABASE)); + return TRUE; + } + + SetCursor(LoadCursor(nullptr, IDC_WAIT)); + + try { + std::string version; + ConnectionHandle connection(nullptr); + connection.serverHostName = tmp.server; + connection.serverPort = tmp.port; + connection.userName = tmp.uid; + connection.password = tmp.pwd; + connection.database = tmp.database; + connection.isTableModel = isTable; + SetConnectionHandle(&connection, "SSL", tmp.ssl); + SetConnectionHandle(&connection, "SSLCA", tmp.sslca); + SetConnectionHandle(&connection, "SSLCERT", tmp.sslcert); + SetConnectionHandle(&connection, "SSLKEY", tmp.sslkey); + connection.OpenSession(); + if (isTable) { + auto session = connection.tableSessionPtr; + auto dataSet = session->executeQueryStatement("SHOW VERSION"); + if (dataSet && dataSet->hasNext()) + version = dataSet->next()->toString(); + session->close(); + } else { + auto session = connection.sessionPtr; + auto dataSet = session->executeQueryStatement("SHOW VERSION"); + if (dataSet && dataSet->hasNext()) + version = dataSet->next()->toString(); + session->close(); + } + + SetCursor(LoadCursor(nullptr, IDC_ARROW)); + + logMessage(nullptr, "DSNDialogProc: Test connection succeeded, version: " + version, + LOG_LEVEL_INFO); + + std::string msg = "Connection successful!"; + if (!version.empty()) + msg += "\n\nServer response:\n" + version; + MessageBoxA(hDlg, msg.c_str(), "Test Connection", MB_OK | MB_ICONINFORMATION); + } catch (const std::exception& e) { + SetCursor(LoadCursor(nullptr, IDC_ARROW)); + + std::string errMsg = e.what(); + logMessage(nullptr, "DSNDialogProc: Test connection failed: " + errMsg, LOG_LEVEL_ERROR); + + std::string msg = "Connection failed!\n\n" + errMsg; + MessageBoxA(hDlg, msg.c_str(), "Test Connection", MB_OK | MB_ICONERROR); + } + return TRUE; + } + } + break; + + case WM_CLOSE: + logMessage(nullptr, "DSNDialogProc: WM_CLOSE", LOG_LEVEL_INFO); + EndDialog(hDlg, IDCANCEL); + return TRUE; + } + + return FALSE; +} + +// ─── Entry point called from ConfigDSN ────────────────────────────────────── + +BOOL ShowDSNDialog(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, LPCSTR lpszAttributes) { + std::ostringstream oss; + oss << "ShowDSNDialog: fRequest=" << fRequest + << ", driver=" << (lpszDriver ? lpszDriver : "(null)") + << ", hwndParent=" << (hwndParent ? "non-null" : "null"); + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + switch (fRequest) { + case ODBC_ADD_DSN: { + logMessage(nullptr, "ShowDSNDialog: ODBC_ADD_DSN", LOG_LEVEL_INFO); + + if (!hwndParent) { + logMessage(nullptr, "ShowDSNDialog: No parent window for ADD, aborting", LOG_LEVEL_WARN); + return FALSE; + } + + DialogContext ctx; + ctx.driverName = lpszDriver ? lpszDriver : ""; + ctx.isNewDsn = TRUE; + ctx.info.server = "127.0.0.1"; + ctx.info.port = "6667"; + ctx.info.uid = "root"; + ctx.info.pwd = "root"; + ctx.info.database = ""; + ctx.info.isTableModel = "1"; + ctx.info.logLevel = "0"; + ctx.info.sessionTimeoutMs = "0"; + ctx.info.batchSize = "1000"; + + HINSTANCE hInst = GetCurrentModuleHandle(); + logMessage(nullptr, "ShowDSNDialog: Showing ADD dialog", LOG_LEVEL_DEBUG); + + INT_PTR result = DialogBoxParamA(hInst, MAKEINTRESOURCEA(IDD_DSN_DIALOG), hwndParent, + DSNDialogProc, reinterpret_cast(&ctx)); + + oss.str(""); + oss.clear(); + oss << "ShowDSNDialog: ADD dialog returned " << result; + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + return (result == IDOK) ? TRUE : FALSE; + } + + case ODBC_CONFIG_DSN: { + std::string dsnName = ParseDSNFromAttributes(lpszAttributes); + logMessage(nullptr, "ShowDSNDialog: ODBC_CONFIG_DSN, DSN='" + dsnName + "'", LOG_LEVEL_INFO); + + if (!hwndParent) { + logMessage(nullptr, "ShowDSNDialog: No parent window for CONFIG, aborting", LOG_LEVEL_WARN); + return FALSE; + } + + DialogContext ctx; + ctx.driverName = lpszDriver ? lpszDriver : ""; + ctx.isNewDsn = FALSE; + + if (!dsnName.empty()) { + ReadDsnFromOdbcIni(dsnName, ctx.info); + } + + HINSTANCE hInst = GetCurrentModuleHandle(); + logMessage(nullptr, "ShowDSNDialog: Showing CONFIG dialog", LOG_LEVEL_DEBUG); + + INT_PTR result = DialogBoxParamA(hInst, MAKEINTRESOURCEA(IDD_DSN_DIALOG), hwndParent, + DSNDialogProc, reinterpret_cast(&ctx)); + + oss.str(""); + oss.clear(); + oss << "ShowDSNDialog: CONFIG dialog returned " << result; + logMessage(nullptr, oss.str(), LOG_LEVEL_INFO); + + return (result == IDOK) ? TRUE : FALSE; + } + + case ODBC_REMOVE_DSN: { + std::string dsnName = ParseDSNFromAttributes(lpszAttributes); + logMessage(nullptr, "ShowDSNDialog: ODBC_REMOVE_DSN, DSN='" + dsnName + "'", LOG_LEVEL_INFO); + + if (dsnName.empty()) { + logMessage(nullptr, "ShowDSNDialog: DSN name empty, cannot remove", LOG_LEVEL_ERROR); + return FALSE; + } + + BOOL removed = SQLRemoveDSNFromIni(dsnName.c_str()); + logMessage(nullptr, + "ShowDSNDialog: SQLRemoveDSNFromIni returned " + + std::string(removed ? "TRUE" : "FALSE"), + LOG_LEVEL_INFO); + + return removed; + } + + default: + logMessage(nullptr, "ShowDSNDialog: Unknown fRequest=" + std::to_string(fRequest), + LOG_LEVEL_ERROR); + return FALSE; + } +} + +#endif // WIN32 diff --git a/iotdb-client/odbc/src/setup/setup_dialog.h b/iotdb-client/odbc/src/setup/setup_dialog.h new file mode 100644 index 0000000000000..89f142685d2c7 --- /dev/null +++ b/iotdb-client/odbc/src/setup/setup_dialog.h @@ -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. + */ +#ifndef SETUP_DIALOG_H +#define SETUP_DIALOG_H + +#ifdef WIN32 + +#include +#include +#include +#include +#include + +BOOL ShowDSNDialog(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, LPCSTR lpszAttributes); + +struct DsnInfo { + std::string dsnName; + std::string description; + std::string server; + std::string port; + std::string uid; + std::string pwd; + std::string database; + std::string isTableModel; + std::string logLevel; + std::string sessionTimeoutMs; + std::string batchSize; + std::string ssl = "0"; + std::string sslca; + std::string sslcert; + std::string sslkey; +}; + +void ReadDsnFromOdbcIni(const std::string& dsnName, DsnInfo& info); +BOOL WriteDsnToOdbcIni(const DsnInfo& info, const std::string& driverName); + +#endif // WIN32 +#endif // SETUP_DIALOG_H diff --git a/iotdb-client/odbc/test/CMakeLists.txt b/iotdb-client/odbc/test/CMakeLists.txt new file mode 100644 index 0000000000000..319ddf27306ea --- /dev/null +++ b/iotdb-client/odbc/test/CMakeLists.txt @@ -0,0 +1,50 @@ +# 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_executable(odbc_unit unit.cpp) +target_link_libraries(odbc_unit PRIVATE odbc_objects) +if(WIN32) + target_sources(odbc_unit PRIVATE ../src/setup/setup.rc) +endif() +add_test(NAME odbc_unit COMMAND odbc_unit) +if(NOT WIN32) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/odbc.ini" + "[ODBC_TLS_TEST]\nSERVER=localhost\nSSL=1\nSSLCA=dsn-ca.pem\nSSLCERT=dsn-client.pem\nSSLKEY=dsn-client.key\n") + set_tests_properties(odbc_unit PROPERTIES + ENVIRONMENT "ODBCINI=${CMAKE_CURRENT_BINARY_DIR}/odbc.ini") +endif() + +add_executable(odbc_manager manager.cpp) +if(WIN32) + target_link_libraries(odbc_manager PRIVATE odbc32 advapi32) +else() + find_library(ODBC_MANAGER_LIBRARY odbc REQUIRED) + target_include_directories(odbc_manager PRIVATE "${ODBC_INCLUDE_DIR}") + target_link_libraries(odbc_manager PRIVATE "${ODBC_MANAGER_LIBRARY}") + target_link_options(odbc_manager PRIVATE -static-libstdc++ -static-libgcc) +endif() +add_test(NAME odbc_manager_load COMMAND odbc_manager $) +set_tests_properties(odbc_unit odbc_manager_load PROPERTIES TIMEOUT 30) +find_package(Python3 COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) + add_test(NAME odbc_parameters COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/parameters.py" $) + set_tests_properties(odbc_parameters PROPERTIES TIMEOUT 30) + add_test(NAME odbc_compatibility COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/compatibility.py" $) + set_tests_properties(odbc_compatibility PROPERTIES TIMEOUT 30) +endif() +install(TARGETS odbc_manager RUNTIME DESTINATION test COMPONENT Tests) diff --git a/iotdb-client/odbc/test/check-windows-dependencies.ps1 b/iotdb-client/odbc/test/check-windows-dependencies.ps1 new file mode 100644 index 0000000000000..81e360e7d6e99 --- /dev/null +++ b/iotdb-client/odbc/test/check-windows-dependencies.ps1 @@ -0,0 +1,35 @@ +# 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. +param([Parameter(Mandatory=$true)][string]$Driver) +$ErrorActionPreference = 'Stop' +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +$vs = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +$dumpbin = Get-ChildItem "$vs\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" | Sort-Object FullName | Select-Object -Last 1 +if (-not $dumpbin) { throw 'dumpbin not found' } +$imports = & $dumpbin.FullName /dependents $Driver +if ($LASTEXITCODE) { throw 'Cannot inspect driver imports' } +$allowed = '^(KERNEL32|ADVAPI32|USER32|GDI32|SHELL32|OLE32|OLEAUT32|WS2_32|CRYPT32|BCRYPT|SECUR32|NORMALIZ|NTDLL|ODBC32|ODBCCP32|COMDLG32|COMCTL32|RPCRT4|VERSION)\.dll$' +$dependencies = $imports | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^[A-Za-z0-9_.-]+\.dll$' } +if (-not $dependencies) { throw 'Cannot determine driver imports' } +foreach ($dependency in $dependencies) { + if ($dependency -notmatch $allowed -and $dependency -notmatch '^api-ms-win-.*\.dll$') { + throw "Unexpected runtime dependency: $dependency" + } +} +$headers = & $dumpbin.FullName /headers $Driver +if ($LASTEXITCODE -or -not ($headers -match '8664 machine')) { throw 'Expected an x64 driver' } +Write-Host 'Windows x64 single-library dependency check passed' diff --git a/iotdb-client/odbc/test/compatibility.py b/iotdb-client/odbc/test/compatibility.py new file mode 100644 index 0000000000000..59f430cdbff33 --- /dev/null +++ b/iotdb-client/odbc/test/compatibility.py @@ -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. +"""Public-ABI regressions for catalog browsing, Unicode, and REST result types.""" + +import ctypes as c +import json +import os +import sys +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + + +def wide(value): + return (c.c_ushort * (len(value.encode("utf-16-le")) // 2 + 1)).from_buffer_copy( + value.encode("utf-16-le") + b"\0\0" + ) + + +def main(): + requests = [] + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + payload = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + sql = payload["sql"] + requests.append(sql) + names, types, rows = ["Version"], ["TEXT"], [["2.0.11-SNAPSHOT"]] + if "information_schema.tables" in sql: + names, types = ["database", "table_name", "comment"], ["STRING"] * 3 + rows = [ + ["db_one", "samples", None], + ["db_one", "samplex", None], + ["db_two", "中文", None], + ] + elif "information_schema.columns" in sql: + names = [ + "database", + "table_name", + "column_name", + "datatype", + "category", + "comment", + ] + types = ["STRING"] * 6 + rows = [ + ["db_one", "samples", "time", "TIMESTAMP", "TIME", None], + ["db_one", "samples", "flag", "BOOLEAN", "FIELD", None], + ["db_one", "samples", "day", "DATE", "FIELD", None], + ] + elif sql == "SHOW DATABASES": + names, types, rows = ["Database"], ["STRING"], [["db_one"], ["db_two"]] + elif sql.startswith("SELECT time"): + names, types = ["中文", "day", "bytes", "empty"], [ + "STRING", + "DATE", + "BLOB", + "STRING", + ] + rows = [["中文😀", "2024-02-29", "0x0001ff", ""]] + elif sql == "SELECT reviewed": + names = ["long_label", "flag", "minimum", "floating"] + types = ["STRING", "BOOLEAN", "INT64", "DOUBLE"] + rows = [["sample", True, -(2**63), 1.5]] + body = {"column_names": names, "data_types": types, "values": rows} + if "/rest/v2/" in self.path: + body["expressions"] = [] + body["timestamps"] = [] + body["values"] = [list(column) for column in zip(*rows)] + response = json.dumps(body).encode() + self.send_response(200) + self.send_header("Content-Length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def log_message(self, *_): + pass + + server = HTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + lib = (c.WinDLL if os.name == "nt" else c.CDLL)(os.path.abspath(sys.argv[1])) + h, short, ushort, integer, length = ( + c.c_void_p, + c.c_short, + c.c_ushort, + c.c_int, + c.c_ssize_t, + ) + + def api(name, *arguments): + fn = getattr(lib, name) + fn.argtypes, fn.restype = arguments, short + return fn + + alloc = api("SQLAllocHandle", short, h, c.POINTER(h)) + free = api("SQLFreeHandle", short, h) + connect = api("SQLDriverConnectW", h, h, h, short, h, short, h, ushort) + direct = api("SQLExecDirectW", h, h, integer) + tables = api("SQLTablesW", h, h, short, h, short, h, short, h, short) + columns = api("SQLColumnsW", h, h, short, h, short, h, short, h, short) + fetch = api("SQLFetch", h) + get = api("SQLGetData", h, ushort, short, h, length, c.POINTER(length)) + count = api("SQLNumResultCols", h, c.POINTER(short)) + describe = api( + "SQLDescribeColW", + h, + ushort, + h, + short, + c.POINTER(short), + c.POINTER(short), + h, + h, + h, + ) + close = api("SQLCloseCursor", h) + env, dbc, stmt = h(), h(), h() + + def read_rows(expected_columns): + n = short() + assert count(stmt, c.byref(n)) == 0 and n.value == expected_columns + result = [] + while True: + rc = fetch(stmt) + if rc == 100: + break + assert rc == 0, rc + row = [] + for col in range(1, n.value + 1): + value, size = c.create_string_buffer(4096), length() + assert get(stmt, col, 1, value, len(value), c.byref(size)) == 0 + row.append(None if size.value == -1 else value.value.decode()) + result.append(row) + assert close(stmt) == 0 + return result + + try: + assert alloc(1, None, c.byref(env)) == 0 + assert alloc(2, env, c.byref(dbc)) == 0 + connection = wide( + f"SERVER=127.0.0.1;PORT={server.server_port};RESTFUL=1;DATABASE=db_one" + ) + assert connect(dbc, None, connection, -3, None, 0, None, 0) == 0 + assert alloc(3, dbc, c.byref(stmt)) == 0 + # SQL_NTS catalog enumeration, standard null padding, and fixed schemas. + assert tables(stmt, wide("%"), -3, wide(""), -3, wide(""), -3, None, 0) == 0 + rows = read_rows(5) + assert [row[0] for row in rows] == ["db_one", "db_two"] + assert all(row[1:] == [None] * 4 for row in rows) + assert tables(stmt, wide(""), -3, wide("%"), -3, wide(""), -3, None, 0) == 0 + assert read_rows(5) == [] + assert ( + tables(stmt, wide(""), -3, wide(""), -3, wide(""), -3, wide("%"), -3) == 0 + ) + assert read_rows(5) == [[None, None, None, "TABLE", None]] + assert tables(stmt, None, 0, None, 0, None, 0, wide("'VIEW'"), -3) == 0 + assert read_rows(5) == [] + assert ( + tables(stmt, wide(r"db\_one"), -3, None, 0, wide("sample_"), -3, None, 0) + == 0 + ) + assert len(read_rows(5)) == 2 + assert tables(stmt, None, 0, None, 0, wide("中_"), -3, None, 0) == 0 + assert read_rows(5)[0][2] == "中文" + before = len(requests) + assert tables(stmt, wide("x' OR 1=1 --"), -3, None, 0, None, 0, None, 0) == 0 + assert read_rows(5) == [] + assert all("OR 1=1" not in sql for sql in requests[before:]) + assert columns(stmt, None, 0, None, 0, None, 0, None, 0) == 0 + rows = read_rows(18) + assert len(rows) == 3 and [row[16] for row in rows] == ["1", "2", "3"] + assert rows[1][4] == "-7" and rows[2][4] == "91" + assert rows[0][10] == "0" and rows[0][17] == "NO" + assert columns(stmt, None, 0, None, 0, None, 0, wide("day"), -3) == 0 + assert read_rows(18)[0][16] == "3" + assert columns(stmt, None, 0, None, 0, wide("missing"), -3, None, 0) == 0 + assert read_rows(18) == [] + sql = "SELECT time, '中文😀' FROM samples" + assert direct(stmt, wide(sql), -3) == 0 and requests[-1] == sql + name, name_length, data_type = (ushort * 3)(), short(), short() + assert ( + describe( + stmt, + 1, + name, + 3, + c.byref(name_length), + c.byref(data_type), + None, + None, + None, + ) + == 0 + ) + assert name_length.value == 2 and list(name) == [0x4E2D, 0x6587, 0] + assert data_type.value == -9 # SQL_WVARCHAR makes ADO request UTF-16 data. + assert fetch(stmt) == 0 + + class Date(c.Structure): + _fields_ = [("year", short), ("month", ushort), ("day", ushort)] + + day, size = Date(), length() + assert get(stmt, 2, 91, c.byref(day), c.sizeof(day), c.byref(size)) == 0 + assert (day.year, day.month, day.day) == (2024, 2, 29) + blob = (c.c_ubyte * 3)() + assert get(stmt, 3, -2, blob, 3, c.byref(size)) == 0 and list(blob) == [ + 0, + 1, + 255, + ] + empty = c.create_string_buffer(1) + assert get(stmt, 4, 1, empty, 1, c.byref(size)) == 0 and size.value == 0 + assert get(stmt, 4, 1, empty, 1, c.byref(size)) == 100 + assert fetch(stmt) == 100 + + # Review regressions exercise exported entry points through the loaded + # library, including cleanup calls after SQLDisconnect dropped children. + assert direct(stmt, wide("SELECT reviewed"), -3) == 0 + describe_a = api("SQLDescribeCol", *describe.argtypes) + attribute = api("SQLColAttribute", h, ushort, ushort, h, short, h, h) + diagnostic = api("SQLGetDiagRec", short, h, short, h, h, h, short, h) + + def state(): + text = c.create_string_buffer(6) + assert diagnostic(3, stmt, 1, text, None, None, 0, None) == 0 + return text.value.decode() + + column_size = c.c_size_t() + assert ( + describe_a(stmt, 1, None, 0, None, None, c.byref(column_size), None, None) + == 0 + ) + assert column_size.value == 2147483647 + for col, expected in ((1, -9), (2, -7), (3, -5)): + numeric = length(0x12345678) + assert attribute(stmt, col, 2, None, 0, None, c.byref(numeric)) == 0 + assert numeric.value == expected + assert attribute(stmt, 1, 18, None, 0, c.byref(name_length), None) == 0 + assert name_length.value == 10 # SQL_DESC_LABEL length-only request. + sentinel = c.create_string_buffer(b"XXXXXXXX") + assert attribute(stmt, 1, 18, sentinel, -1, None, None) == -1 + assert state() == "HY090" and sentinel.raw == b"XXXXXXXX\0" + assert ( + describe_a( + stmt, + 1, + sentinel, + 4, + c.byref(name_length), + None, + c.byref(column_size), + None, + None, + ) + == 1 + ) + assert sentinel.value == b"lon" and name_length.value == 10 + assert column_size.value == 2147483647 and state() == "01004" + assert attribute(stmt, 1, 18, sentinel, 1, None, None) == 1 + assert state() == "01004" + assert fetch(stmt) == 0 + + class Numeric(c.Structure): + _fields_ = [ + ("precision", c.c_ubyte), + ("scale", c.c_byte), + ("sign", c.c_ubyte), + ("val", c.c_ubyte * 16), + ] + + number = Numeric() + assert get(stmt, 3, 2, c.byref(number), c.sizeof(number), c.byref(size)) == 0 + assert number.sign == 0 and number.scale == 0 + assert int.from_bytes(number.val, "little") == 2**63 + c.memset(c.byref(number), 0x5A, c.sizeof(number)) + before = bytes(number) + assert get(stmt, 4, 2, c.byref(number), c.sizeof(number), c.byref(size)) == -1 + assert state() == "07006" and bytes(number) == before + descriptor = h() + assert alloc(4, dbc, c.byref(descriptor)) == 0 + assert api("SQLDisconnect", h)(dbc) == 0 + assert free(2, dbc) == 0 + assert free(3, stmt) == -2 and free(4, descriptor) == -2 + dbc.value = stmt.value = None + print("ODBC catalog, Unicode, numeric, handle and REST checks passed") + finally: + for kind, value in ((3, stmt), (2, dbc), (1, env)): + if value: + free(kind, value) + server.shutdown() + server.server_close() + thread.join() + + +if __name__ == "__main__": + main() diff --git a/iotdb-client/odbc/test/manager.cpp b/iotdb-client/odbc/test/manager.cpp new file mode 100644 index 0000000000000..68f5a9fd7ed02 --- /dev/null +++ b/iotdb-client/odbc/test/manager.cpp @@ -0,0 +1,250 @@ +/** + * 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 +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +// Use a disposable user DSN so the test exercises the Windows Driver Manager +// without changing machine-wide driver registration or redirecting HKLM. +class UserDsnSandbox { + std::string path_; + std::string name_; + +public: + explicit UserDsnSandbox(std::string dll) { + std::replace(dll.begin(), dll.end(), '/', '\\'); + name_ = + "IoTDB-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64()); + path_ = "Software\\ODBC\\ODBC.INI\\" + name_; + auto write = [&](const std::string& path, const char* name, const std::string& value) { + HKEY key = nullptr; + // Creating a volatile path also makes previously absent ODBC parent keys + // volatile, preventing normal applications from creating persistent DSNs. + // The sandbox explicitly deletes its uniquely named DSN on destruction. + LONG rc = RegCreateKeyExA(HKEY_CURRENT_USER, path.c_str(), 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &key, nullptr); + // Temporary/sandboxed user hives can require volatile descendants. + if (rc == ERROR_CHILD_MUST_BE_VOLATILE) + rc = RegCreateKeyExA(HKEY_CURRENT_USER, path.c_str(), 0, nullptr, REG_OPTION_VOLATILE, + KEY_ALL_ACCESS, nullptr, &key, nullptr); + if (rc == ERROR_SUCCESS) { + rc = RegSetValueExA(key, name, 0, REG_SZ, reinterpret_cast(value.c_str()), + static_cast(value.size() + 1)); + RegCloseKey(key); + } + return rc == ERROR_SUCCESS; + }; + if (!write(path_, "Driver", dll) || !write("Software\\ODBC\\ODBC.INI\\ODBC Data Sources", + name_.c_str(), "Apache IoTDB ODBC Driver")) { + RegDeleteTreeA(HKEY_CURRENT_USER, path_.c_str()); + throw std::runtime_error("Cannot configure temporary user DSN"); + } + } + const std::string& name() const { + return name_; + } + ~UserDsnSandbox() { + RegDeleteTreeA(HKEY_CURRENT_USER, path_.c_str()); + HKEY sources = nullptr; + if (RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\ODBC\\ODBC.INI\\ODBC Data Sources", 0, + KEY_SET_VALUE, &sources) == ERROR_SUCCESS) { + RegDeleteValueA(sources, name_.c_str()); + RegCloseKey(sources); + } + } +}; +#endif + +static std::string utf8Environment(const char* name) { +#ifdef _WIN32 + std::string key(name); + std::wstring wideKey(key.begin(), key.end()); + const wchar_t* value = _wgetenv(wideKey.c_str()); + return value ? std::wstring_convert>().to_bytes(value) : ""; +#else + const char* value = std::getenv(name); + return value ? value : ""; +#endif +} + +static std::basic_string sqlWide(const std::string& input) { + auto utf16 = + std::wstring_convert, char16_t>().from_bytes(input); + std::basic_string result(utf16.size(), 0); + std::copy(utf16.begin(), utf16.end(), result.begin()); + return result; +} + +// Without IOTDB_ODBC_CONNECTION, reject a TLS/REST mismatch without network I/O. +// With it, connect to a real server and execute IOTDB_ODBC_QUERY or SHOW VERSION. +int runTest(int argc, char** argv) { + if (argc != 2) + return 2; + std::string driver = argv[1]; +#ifdef _WIN32 + UserDsnSandbox dsn(driver); +#endif + SQLHENV env = nullptr; + SQLHDBC dbc = nullptr; + if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env))) + return 1; + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc))) + return 1; + std::string optionsText = utf8Environment("IOTDB_ODBC_CONNECTION"); + const char* options = optionsText.empty() ? nullptr : optionsText.c_str(); + const bool traceStages = !utf8Environment("IOTDB_ODBC_TRACE_STAGES").empty(); + auto stage = [&](const char* text) { + if (traceStages) + std::cerr << text << std::endl; + }; +#ifdef _WIN32 + std::string conn = + "DSN=" + dsn.name() + ";SSL=0;" + (options ? options : "RESTFUL=1;SSL=1;SSLCA=missing.pem"); +#else + std::string conn = std::string("DRIVER={") + driver + "};SSL=0;" + + (options ? options : "RESTFUL=1;SSL=1;SSLCA=missing.pem"); +#endif + stage("driver-connect"); + auto wideConnection = sqlWide(conn); + SQLRETURN rc = SQLDriverConnectW(dbc, nullptr, &wideConnection[0], SQL_NTS, nullptr, 0, nullptr, + SQL_DRIVER_NOPROMPT); + stage("driver-connect-returned"); + bool expectedFailure = !options || std::getenv("IOTDB_ODBC_EXPECT_FAILURE"); + bool ok = false; + if (expectedFailure) { + SQLCHAR state[6]{}, message[1024]{}; + SQLINTEGER native{}; + SQLSMALLINT length{}; + SQLRETURN diagnosticResult = + SQLGetDiagRec(SQL_HANDLE_DBC, dbc, 1, state, &native, message, sizeof(message), &length); + ok = rc == SQL_ERROR && std::string(reinterpret_cast(state)) == "08001"; + if (!ok) + std::cerr << "connect=" << rc << ", diagnostic=" << diagnosticResult << ", " << state << ": " + << message << '\n'; + } else if (SQL_SUCCEEDED(rc)) { + SQLHSTMT stmt = nullptr; + stage("allocate-statement"); + SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); + std::string queryText = utf8Environment("IOTDB_ODBC_QUERY"); + const char* query = queryText.empty() ? nullptr : queryText.c_str(); + std::string sql = query ? query : "SHOW VERSION"; + auto wideSql = sqlWide(sql); + std::string parameterText = utf8Environment("IOTDB_ODBC_PARAMETER_INTS"); + stage("execute"); + if (parameterText.empty()) { + rc = SQLExecDirectW(stmt, &wideSql[0], SQL_NTS); + } else { + std::vector parameters; + std::stringstream values(parameterText); + std::string item; + while (std::getline(values, item, ',')) + parameters.push_back(static_cast(std::stol(item))); + std::vector lengths(parameters.size(), sizeof(SQLINTEGER)); + std::vector statuses(parameters.size(), SQL_PARAM_UNUSED); + SQLULEN processed = 0; + SQLSetStmtAttr(stmt, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(parameters.size()), + 0); + SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_STATUS_PTR, statuses.data(), 0); + SQLSetStmtAttr(stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, 0); + rc = SQLPrepareW(stmt, &wideSql[0], SQL_NTS); + if (SQL_SUCCEEDED(rc)) + rc = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 10, 0, + parameters.data(), sizeof(SQLINTEGER), lengths.data()); + if (SQL_SUCCEEDED(rc)) + rc = SQLExecute(stmt); + if (SQL_SUCCEEDED(rc)) + rc = processed == parameters.size() && + std::all_of(statuses.begin(), statuses.end(), + [](SQLUSMALLINT status) { + return status == SQL_PARAM_SUCCESS || + status == SQL_PARAM_SUCCESS_WITH_INFO; + }) + ? rc + : SQL_ERROR; + } + stage("execute-returned"); + if (SQL_SUCCEEDED(rc)) { + SQLSMALLINT columns = 0; + SQLNumResultCols(stmt, &columns); + ok = columns == 0; + int rows = 0; + bool foundValue = false; + std::string expectedText = utf8Environment("IOTDB_ODBC_EXPECT_VALUE"); + const char* expectedValue = expectedText.empty() ? nullptr : expectedText.c_str(); + while (stage("fetch"), SQL_SUCCEEDED(rc = SQLFetch(stmt))) { + stage("fetch-returned-row"); + ++rows; + for (SQLUSMALLINT col = 1; col <= columns; ++col) { + SQLCHAR value[4096]{}; + SQLLEN length{}; + if (!SQL_SUCCEEDED(SQLGetData(stmt, col, SQL_C_CHAR, value, sizeof(value), &length))) + return 1; + if (length != SQL_NULL_DATA && expectedValue && + std::string(reinterpret_cast(value)) == expectedValue) + foundValue = true; + } + } + if (columns > 0) + ok = rc == SQL_NO_DATA && rows > 0; + if (const char* expectedRows = std::getenv("IOTDB_ODBC_EXPECT_ROWS")) + ok = ok && rows == std::stoi(expectedRows); + if (expectedValue) + ok = ok && foundValue; + } + stage("free-statement"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + stage("disconnect"); + SQLDisconnect(dbc); + stage("disconnect-returned"); + } else { + SQLCHAR state[6]{}, message[1024]{}; + SQLINTEGER native{}; + SQLSMALLINT length{}; + SQLGetDiagRec(SQL_HANDLE_DBC, dbc, 1, state, &native, message, sizeof(message), &length); + std::cerr << state << ": " << message << '\n'; + } + stage("free-connection"); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + stage("free-environment"); + SQLFreeHandle(SQL_HANDLE_ENV, env); + stage("done"); + return ok ? 0 : 1; +} + +int main(int argc, char** argv) { + try { + return runTest(argc, argv); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/iotdb-client/odbc/test/parameters.py b/iotdb-client/odbc/test/parameters.py new file mode 100644 index 0000000000000..835dae48311c6 --- /dev/null +++ b/iotdb-client/odbc/test/parameters.py @@ -0,0 +1,346 @@ +# 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. +"""Check parameter values at the REST boundary using the public ODBC ABI.""" + +import ctypes as c +from http.server import BaseHTTPRequestHandler, HTTPServer +import json +import os +import sys +import threading + + +class Timestamp(c.Structure): + _fields_ = [ + (name, c.c_short) + for name in ("year", "month", "day", "hour", "minute", "second") + ] + [("fraction", c.c_uint)] + + +def main(): + requests = [] + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + payload = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + requests.append((self.path, payload["sql"])) + response = b'{"code":200,"expressions":null,"column_names":[],"values":[]}' + if payload["sql"].startswith("SHOW DEVICES"): + response = json.dumps( + { + "code": 200, + "expressions": None, + "column_names": [ + "Device", + "Database", + "IsAligned", + "Template", + "TTL(ms)", + ], + "values": [["root.db.d"], ["root.db"], [False], [""], [None]], + } + ).encode() + self.send_response(200) + self.send_header("Content-Length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def log_message(self, *_): + pass + + server = HTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + lib = (c.WinDLL if os.name == "nt" else c.CDLL)(os.path.abspath(sys.argv[1])) + handle, small, integer, length = c.c_void_p, c.c_short, c.c_int, c.c_ssize_t + + def api(name, *args): + function = getattr(lib, name) + function.argtypes = args + function.restype = small + return function + + alloc = api("SQLAllocHandle", small, handle, c.POINTER(handle)) + free = api("SQLFreeHandle", small, handle) + connect = api( + "SQLDriverConnect", + handle, + handle, + c.c_char_p, + small, + handle, + small, + handle, + c.c_ushort, + ) + prepare = api("SQLPrepare", handle, c.c_char_p, integer) + bind = api( + "SQLBindParameter", + handle, + c.c_ushort, + small, + small, + small, + c.c_size_t, + small, + handle, + length, + c.POINTER(length), + ) + execute = api("SQLExecute", handle) + param_data = api("SQLParamData", handle, c.POINTER(handle)) + put = api("SQLPutData", handle, handle, length) + direct = api("SQLExecDirect", handle, c.c_char_p, integer) + get_attr = api("SQLGetStmtAttr", handle, integer, handle, integer, handle) + set_attr = api("SQLSetStmtAttr", handle, integer, handle, integer) + free_stmt = api("SQLFreeStmt", handle, c.c_ushort) + set_desc = api("SQLSetDescField", handle, small, small, handle, integer) + get_desc = api("SQLGetDescField", handle, small, small, handle, integer, handle) + get_type_info = api("SQLGetTypeInfo", handle, small) + get_desc_rec = api( + "SQLGetDescRec", + handle, + small, + handle, + small, + handle, + handle, + handle, + handle, + handle, + handle, + handle, + ) + copy_desc = api("SQLCopyDesc", handle, handle) + tables = api( + "SQLTables", handle, handle, small, handle, small, handle, small, handle, small + ) + env, dbc, stmt = handle(), handle(), handle() + try: + assert alloc(1, None, c.byref(env)) == 0 + assert alloc(2, env, c.byref(dbc)) == 0 + options = ( + f"SERVER=127.0.0.1;PORT={server.server_port};" + "RESTFUL=1;ISTABLEMODEL=0;UID=root;PWD=root;" + ).encode() + assert connect(dbc, None, options, -3, None, 0, None, 0) == 0 + assert alloc(3, dbc, c.byref(stmt)) == 0 + + def check( + value, + ctype, + sqltype, + expected, + streamed=False, + indicator=None, + direct_execution=False, + ): + assert prepare(stmt, b"insert into root.d(time, v) values(1, ?)", -3) == 0 + ind = ( + length(-2 if streamed else indicator) + if streamed or indicator is not None + else None + ) + assert ( + bind( + stmt, + 1, + 1, + ctype, + sqltype, + 64, + 0, + c.byref(value), + c.sizeof(value), + c.byref(ind) if ind is not None else None, + ) + == 0 + ) + result = ( + direct(stmt, b"insert into root.d(time, v) values(1, ?)", -3) + if direct_execution + else execute(stmt) + ) + if streamed: + assert result == 99 + assert prepare(stmt, b"select ?, ?, ?", -3) == -1 + assert direct(stmt, b"select 1", -3) == -1 + assert execute(stmt) == -1 + assert set_attr(stmt, 22, handle(2), 0) == -1 + assert ( + bind( + stmt, + 1, + 1, + ctype, + sqltype, + 64, + 0, + c.byref(value), + c.sizeof(value), + c.byref(ind), + ) + == -1 + ) + token = handle() + assert param_data(stmt, c.byref(token)) == 99 + assert token.value == c.addressof(value) + assert ( + put( + stmt, + c.byref(value), + indicator if indicator is not None else c.sizeof(value), + ) + == 0 + ) + assert param_data(stmt, c.byref(token)) == 0 + else: + assert result == 0 + assert requests[-1] == ( + "/rest/v2/nonQuery", + f"insert into root.d(time, v) values(1, {expected})", + ), requests[-1] + + check(c.c_double(1.2345678901234567), 8, 8, "1.2345678901234567") + + # Tail bytes make an incorrect wider read deterministic. + class SmallBuffer(c.Structure): + _fields_ = [("value", c.c_short), ("tail", c.c_short)] + + class TinyBuffer(c.Structure): + _fields_ = [("value", c.c_byte), ("tail", c.c_byte * 3)] + + check(SmallBuffer(42, 1234), 99, 5, "42") + check(TinyBuffer(42, (c.c_byte * 3)(1, 2, 3)), 99, -6, "42") + check(c.create_string_buffer(b"12.3456"), 99, 3, "'12.3456'") + check((c.c_ushort * 3)(0x4E2D, 0x6587, 0), 99, -9, "'中文'") + check( + Timestamp(2026, 9, 22, 1, 2, 3, 123456789), + 93, + 93, + "'2026-09-22 01:02:03.123456789'", + ) + check(c.create_string_buffer(b"it's text"), 99, 12, "'it''s text'") + check(c.c_int(42), 4, 4, "42") + check(c.c_int(42), 4, 4, "42", streamed=True) + check(c.c_int(42), 4, 4, "42", direct_execution=True) + check(c.c_int(42), 4, 4, "42", streamed=True, direct_execution=True) + check((c.c_ubyte * 3)(0, 255, 39), -2, -3, "X'00FF27'", streamed=True) + check( + (c.c_ushort * 3)(0x4E2D, 0x6587, 0), + -8, + -9, + "'中文'", + streamed=True, + indicator=-3, + ) + # Explicit descriptor length and NULL-indicator pointers are independent. + assert prepare(stmt, b"insert into root.d(time, v) values(1, ?)", -3) == 0 + value = c.create_string_buffer(b"abcdef") + size, indicator, apd = length(3), length(0), handle() + assert ( + bind(stmt, 1, 1, 1, 12, 64, 0, value, c.sizeof(value), c.byref(size)) == 0 + ) + assert get_attr(stmt, 10011, c.byref(apd), 0, None) == 0 + assert set_desc(apd, 1, 1009, c.byref(indicator), 0) == 0 + assert execute(stmt) == 0 + assert requests[-1][1].endswith("(1, 'abc')"), requests[-1] + assert direct(stmt, b"/* comment */ SELECT 1", -3) == 0 + assert requests[-1][0] == "/rest/v2/query" + + # Validate every marker before requesting streamed data from the caller. + assert free_stmt(stmt, 3) == 0 # SQL_RESET_PARAMS + value, indicator = c.c_int(42), length(-2) + assert bind(stmt, 1, 1, 4, 4, 0, 0, c.byref(value), 0, c.byref(indicator)) == 0 + sql = b"insert into root.d(time, v) values(?, ?)" + before = len(requests) + assert prepare(stmt, sql, -3) == 0 + assert execute(stmt) == -1 + assert direct(stmt, sql, -3) == -1 + token = handle() + assert param_data(stmt, c.byref(token)) == -1 + assert len(requests) == before + + # Arrays apply to parameterized SQL, not subsequent literal statements. + assert free_stmt(stmt, 3) == 0 + values = (c.c_int * 3)(11, 22, 33) + assert set_attr(stmt, 22, handle(3), 0) == 0 # SQL_ATTR_PARAMSET_SIZE + assert bind(stmt, 1, 1, 4, 4, 0, 0, values, c.sizeof(c.c_int), None) == 0 + sql = b"insert into root.d(time, v) values(1, ?)" + for direct_execution in (False, True): + before = len(requests) + if direct_execution: + assert direct(stmt, sql, -3) == 0 + else: + assert prepare(stmt, sql, -3) == 0 + assert execute(stmt) == 0 + assert [request[1] for request in requests[before:]] == [ + f"insert into root.d(time, v) values(1, {value})" for value in values + ] + literal = b"create database root.repro" + before = len(requests) + if direct_execution: + assert direct(stmt, literal, -3) == 0 + else: + assert prepare(stmt, literal, -3) == 0 + assert execute(stmt) == 0 + assert [request[1] for request in requests[before:]] == [literal.decode()] + # Read the IRD without SQLNumResultCols (which also refreshes it). + assert set_attr(stmt, 22, handle(1), 0) == 0 + assert get_type_info(stmt, 0) == 0 + ird, count = handle(), small() + assert get_attr(stmt, 10012, c.byref(ird), 0, None) == 0 + descriptor_copy = handle() + assert alloc(4, dbc, c.byref(descriptor_copy)) == 0 + try: + assert copy_desc(ird, descriptor_copy) == 0 + assert get_desc(descriptor_copy, 0, 1001, c.byref(count), 0, None) == 0 + assert count.value == 19, count.value + finally: + assert free(4, descriptor_copy) == 0 + assert direct(stmt, b"select 1", -3) == 0 + assert get_type_info(stmt, 0) == 0 + name = c.create_string_buffer(64) + assert ( + get_desc_rec( + ird, 19, name, len(name), None, None, None, None, None, None, None + ) + == 0 + ) + assert name.value == b"INTERVAL_PRECISION", name.value + assert get_desc(ird, 0, 1001, c.byref(count), 0, None) == 0 + assert count.value == 19, count.value + name = c.create_string_buffer(64) + assert get_desc(ird, 1, 1011, name, len(name), None) == 0 + assert name.value == b"TYPE_NAME", name.value + assert tables(stmt, None, 0, None, 0, None, 0, None, 0) == 0 + assert get_desc(ird, 0, 1001, c.byref(count), 0, None) == 0 + assert count.value == 5, count.value + assert get_desc(ird, 1, 1011, name, len(name), None) == 0 + assert name.value == b"TABLE_CAT", name.value + print("ODBC parameter serialization checks passed") + finally: + for kind, value in ((3, stmt), (2, dbc), (1, env)): + if value: + free(kind, value) + server.shutdown() + server.server_close() + thread.join() + + +if __name__ == "__main__": + main() diff --git a/iotdb-client/odbc/test/run_e2e.py b/iotdb-client/odbc/test/run_e2e.py new file mode 100644 index 0000000000000..04720785fff1b --- /dev/null +++ b/iotdb-client/odbc/test/run_e2e.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +# 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. +"""Run ODBC plain/TLS/mTLS checks against a disposable copy of a server distribution.""" + +import argparse +import importlib.util +import os +import re +import shutil +import socket +import subprocess +import tempfile +import time +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("manager", type=Path) + parser.add_argument("driver", type=Path) + parser.add_argument("distribution", type=Path) + parser.add_argument("--password-env", default="IOTDB_TEST_PASSWORD") + parser.add_argument( + "--windows-consumers", + action="store_true", + help="Also run .NET ODBC and MSDASQL/ADO on Windows", + ) + parser.add_argument( + "--modes", + nargs="+", + choices=["plain", "tls", "mtls"], + default=["plain", "tls", "mtls"], + ) + args = parser.parse_args() + if os.name != "nt" and not (shutil.which("lsof") or shutil.which("netstat")): + parser.error("Server stop scripts require lsof or netstat") + manager, driver = args.manager.resolve(), args.driver.resolve() + cpp = Path(__file__).resolve().parents[2] / "client-cpp" / "test" + spec = importlib.util.spec_from_file_location( + "cpp_phases", cpp / "scripts" / "run_cpp_it_phases.py" + ) + phases = importlib.util.module_from_spec(spec) + spec.loader.exec_module(phases) + fixtures = cpp / "fixtures" + password = os.environ.get(args.password_env) + if password is None: + parser.error(f"Set {args.password_env} to the test server password") + for port in [6667, 10710, 10720, 10730, 10740, 10750, 10760, 18080]: + with socket.socket() as probe: + if probe.connect_ex(("127.0.0.1", port)) == 0: + parser.error( + f"Port {port} is already in use; use an isolated test environment" + ) + env = os.environ.copy() + env["IOTDB_NO_PAUSE"] = "1" + processes = [] + launch_logs = [] + + def start_server(dist): + if os.name != "nt": + phases.start_server(dist, 120, env) + return + for node in ("confignode", "datanode"): + log = (dist / f"{node}-launcher.log").open("a", encoding="utf-8") + launch_logs.append(log) + processes.append( + subprocess.Popen( + [ + "cmd", + "/d", + "/c", + str(dist / "sbin" / "windows" / f"start-{node}.bat"), + "-f", + ], + cwd=dist, + env=env, + stdout=log, + stderr=subprocess.STDOUT, + creationflags=subprocess.CREATE_NO_WINDOW, + ) + ) + phases.wait_for_rpc_port(120) + + def stop_server(dist): + if os.name != "nt": + phases.stop_server(dist, env) + return + # Terminate only launchers and Java children created by this runner. + # Port-based stop scripts can target an unrelated process or PID 0. + for process in reversed(processes): + if process.poll() is None: + subprocess.run( + ["taskkill", "/PID", str(process.pid), "/T", "/F"], + check=True, + stdout=subprocess.DEVNULL, + ) + process.wait(timeout=30) + processes.clear() + for log in launch_logs: + log.close() + launch_logs.clear() + time.sleep(2) + + def check( + options, + sql="SHOW VERSION", + failure=False, + rows=None, + value=None, + parameter_ints=None, + ): + run_env = env.copy() + run_env["IOTDB_ODBC_CONNECTION"] = ( + "SERVER=127.0.0.1;PORT=6667;UID=root;PWD={" + + password.replace("}", "}}") + + "};" + "LOGLEVEL=0;BATCHSIZE=2;" + options + ) + run_env["IOTDB_ODBC_QUERY"] = sql + run_env.pop("IOTDB_ODBC_EXPECT_FAILURE", None) + run_env.pop("IOTDB_ODBC_EXPECT_ROWS", None) + run_env.pop("IOTDB_ODBC_EXPECT_VALUE", None) + run_env.pop("IOTDB_ODBC_PARAMETER_INTS", None) + if rows is not None: + run_env["IOTDB_ODBC_EXPECT_ROWS"] = str(rows) + if value is not None: + run_env["IOTDB_ODBC_EXPECT_VALUE"] = value + if parameter_ints is not None: + run_env["IOTDB_ODBC_PARAMETER_INTS"] = ",".join( + str(value) for value in parameter_ints + ) + if failure: + run_env["IOTDB_ODBC_EXPECT_FAILURE"] = "1" + subprocess.run([str(manager), str(driver)], env=run_env, check=True, timeout=90) + + with tempfile.TemporaryDirectory(prefix="iotdb-odbc-e2e-") as temporary: + dist = Path(temporary) / "server" + shutil.copytree( + args.distribution, dist, ignore=shutil.ignore_patterns("data", "logs") + ) + if os.name == "nt": + # Older distribution launchers pause after exit. Only adjust this + # disposable test copy so unattended tests can clean up all files. + for script in (dist / "sbin" / "windows").glob("*.bat"): + text = script.read_text(encoding="utf-8") + text = re.sub( + r"(?im)^\s*@?pause\s*$", + "rem pause disabled in ODBC test copy", + text, + ) + text = re.sub(r"(?im)^(\s*)start (?=cmd\b)", r"\1start /b ", text) + script.write_text(text, encoding="utf-8") + properties = dist / "conf" / "iotdb-system.properties" + with properties.open("a", encoding="utf-8") as config: + config.write("\nenable_rest_service=true\nrest_service_port=18080\n") + wrong_store = Path(temporary) / "wrong-identity.p12" + wrong_cert = Path(temporary) / "wrong-identity.crt" + if any(mode != "plain" for mode in args.modes): + subprocess.run( + [ + "keytool", + "-genkeypair", + "-alias", + "server", + "-keyalg", + "RSA", + "-keystore", + str(wrong_store), + "-storetype", + "PKCS12", + "-storepass", + "thrift", + "-keypass", + "thrift", + "-dname", + "CN=wrong.invalid", + "-ext", + "SAN=dns:wrong.invalid", + "-validity", + "3650", + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.run( + [ + "keytool", + "-exportcert", + "-rfc", + "-alias", + "server", + "-keystore", + str(wrong_store), + "-storepass", + "thrift", + "-file", + str(wrong_cert), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + for mode in args.modes: + phases.run( + [ + os.sys.executable, + str(cpp / "scripts" / "configure_iotdb_ssl_it.py"), + str(dist), + str(fixtures), + mode, + ], + dist, + ) + tls = "" + if mode != "plain": + tls = f"SSL=1;SSLCA={{{fixtures / 'tls' / 'ca.crt'}}};" + if mode == "mtls": + tls += f"SSLCERT={{{fixtures / 'tls' / 'client.crt'}}};SSLKEY={{{fixtures / 'tls' / 'client.key'}}};" + try: + start_server(dist) + check(tls + "ISTABLEMODEL=0;") + check(tls + "ISTABLEMODEL=1;DATABASE=information_schema;") + check(tls + "ISTABLEMODEL=0;", "CREATE DATABASE root.odbc_migration") + check( + tls + "ISTABLEMODEL=0;", + "INSERT INTO root.odbc_migration.d(time,v) VALUES(1,1),(2,2),(3,3)", + ) + check( + tls + "ISTABLEMODEL=0;", + "INSERT INTO root.odbc_migration.d(time,v) VALUES(?,42)", + parameter_ints=[4, 5], + ) + check( + tls + "ISTABLEMODEL=0;", + "SELECT v FROM root.odbc_migration.d", + rows=5, + ) + check(tls + "ISTABLEMODEL=0;", "DELETE DATABASE root.odbc_migration") + table = tls + "ISTABLEMODEL=1;DATABASE=information_schema;" + check(table, "CREATE DATABASE odbc_migration") + check( + table, + "CREATE TABLE odbc_migration.samples (device STRING TAG, val INT32 FIELD, txt STRING FIELD, day DATE FIELD)", + ) + check( + table, + "INSERT INTO odbc_migration.samples(time,device,val,txt,day) VALUES (1,'d',1,'中文','2024-02-29'),(2,'d',2,null,'2024-03-01'),(3,'d',3,'last','2024-03-02')", + ) + check( + table, + "SELECT val,txt,day FROM odbc_migration.samples ORDER BY time", + rows=3, + value="中文", + ) + if mode == "plain": + check("RESTFUL=1;PORT=18080;ISTABLEMODEL=0;") + check( + "RESTFUL=1;PORT=18080;ISTABLEMODEL=1;DATABASE=odbc_migration;", + "SELECT val,txt,day FROM samples ORDER BY time", + rows=3, + value="中文", + ) + if args.windows_consumers: + if os.name != "nt": + parser.error("--windows-consumers requires Windows") + consumer_env = env.copy() + consumer_env["IOTDB_ODBC_CONNECTION"] = ( + "SERVER=127.0.0.1;PORT=6667;UID=root;PWD={" + + password.replace("}", "}}") + + "};" + "ISTABLEMODEL=1;DATABASE=information_schema;BATCHSIZE=2;" + tls + ) + subprocess.run( + [ + "pwsh", + "-NoProfile", + "-File", + str(Path(__file__).with_name("windows-consumers.ps1")), + str(driver), + ], + env=consumer_env, + check=True, + timeout=120, + ) + check(table, "DROP DATABASE odbc_migration") + if mode != "plain": + check("ISTABLEMODEL=0;SSL=1;SSLCA=missing-ca.pem;", failure=True) + check(f"ISTABLEMODEL=0;SSL=1;SSLCA={{{wrong_cert}}};", failure=True) + check("ISTABLEMODEL=0;SSL=0;", failure=True) + if mode == "mtls": + check( + f"ISTABLEMODEL=0;SSL=1;SSLCA={{{fixtures / 'tls' / 'ca.crt'}}};", + failure=True, + ) + check(tls + f"SSLCERT={{{wrong_cert}}};", failure=True) + finally: + stop_server(dist) + print(f"ODBC {mode} checks passed", flush=True) + + if wrong_store.exists(): + # Trust the actual self-signed server certificate, but connect using + # localhost, which is absent from its SAN. This isolates identity + # verification from CA validation and connection-refused failures. + phases.run( + [ + os.sys.executable, + str(cpp / "scripts" / "configure_iotdb_ssl_it.py"), + str(dist), + str(fixtures), + "tls", + ], + dist, + ) + text = properties.read_text(encoding="utf-8") + text = re.sub( + r"^key_store_path=.*$", + "key_store_path=" + wrong_store.as_posix(), + text, + flags=re.MULTILINE, + ) + properties.write_text(text, encoding="utf-8") + try: + start_server(dist) + check(f"ISTABLEMODEL=0;SSL=1;SSLCA={{{wrong_cert}}};", failure=True) + finally: + stop_server(dist) + print("ODBC server identity rejection passed", flush=True) + + +if __name__ == "__main__": + main() diff --git a/iotdb-client/odbc/test/unit.cpp b/iotdb-client/odbc/test/unit.cpp new file mode 100644 index 0000000000000..ea3096a682e2f --- /dev/null +++ b/iotdb-client/odbc/test/unit.cpp @@ -0,0 +1,617 @@ +/** + * 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 "ConnectionHandle.h" +#include "ConnectionString.h" +#include "ODBCField.h" +#include "StatementUtils.h" +#include "driver.h" +#include +#include +#include + +static void checkAt(bool ok, int line) { + if (!ok) + throw std::runtime_error("ODBC unit assertion failed at line " + std::to_string(line)); +} +#define check(condition) checkAt((condition), __LINE__) + +static void checkDescriptorBindings() { + ConnectionHandle connection(nullptr); + StatementHandle statement(&connection); + statement.AllocateSessionResultSet(); + statement.resultSetPtr->columnNames = {"v"}; + statement.resultSetPtr->columnTypes = {"INT32"}; + statement.resultSetPtr->data = { + {ODBCField(int32_t(1))}, {ODBCField(int32_t(2))}, {ODBCField(int32_t(3))}}; + statement.resultSetPtr->numColumns = 1; + statement.resultSetPtr->numRows = 3; + SQLINTEGER values[4] = {-1, -1, -1, -1}; + SQLLEN lengths[2]{}; + SQLULEN fetched = 0; + SQLUSMALLINT status[2]{}; + check(SQLSetDescField(statement.impRowDesc, 0, SQL_DESC_ROWS_PROCESSED_PTR, &fetched, 0) == + SQL_SUCCESS); + check(SQLSetDescField(statement.impRowDesc, 0, SQL_DESC_ARRAY_STATUS_PTR, status, 0) == + SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 0, SQL_DESC_ARRAY_SIZE, + reinterpret_cast(2), 0) == SQL_SUCCESS); + SQLULEN arraySize = 0; + check(SQLGetStmtAttr(&statement, SQL_ATTR_ROW_ARRAY_SIZE, &arraySize, 0, nullptr) == + SQL_SUCCESS && + arraySize == 2); + // BufferLength is ignored for fixed-width C types, including SQL_C_DEFAULT. + check(SQLBindCol(&statement, 1, SQL_C_DEFAULT, values, sizeof(values), lengths) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS && fetched == 2 && values[0] == 1 && values[1] == 2 && + values[2] == -1 && status[1] == SQL_ROW_SUCCESS); + check(SQLFreeStmt(&statement, SQL_UNBIND) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS && fetched == 1 && values[0] == 1 && values[1] == 2); + + statement.curRow = -1; + check(SQLBindCol(&statement, 1, SQL_C_SLONG, values, 0, lengths) == SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 0, SQL_DESC_COUNT, nullptr, 0) == SQL_SUCCESS); + values[0] = -1; + check(SQLFetch(&statement) == SQL_SUCCESS && values[0] == -1); + statement.curRow = -1; + SQLLEN separateIndicators[2] = {-99, -99}; + check(SQLSetDescRec(statement.appRowDesc, 1, SQL_C_SLONG, 0, 0, 0, 0, values, lengths, + separateIndicators) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS && values[1] == 2 && lengths[1] == sizeof(SQLINTEGER) && + separateIndicators[1] == 0); + statement.curRow = -1; + statement.resultSetPtr->data[0][0] = ODBCField(std::string("not an integer")); + check(SQLFetch(&statement) == SQL_SUCCESS_WITH_INFO && fetched == 2 && + status[0] == SQL_ROW_ERROR && status[1] == SQL_ROW_SUCCESS && values[1] == 2); + check(SQLBindCol(&statement, 65535, SQL_C_CHAR, values, sizeof(values), nullptr) == SQL_ERROR); + check(SQLBindParameter(&statement, 65535, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 10, 0, + values, 0, nullptr) == SQL_ERROR); + + SQLHANDLE descriptor = nullptr; + check(SQLAllocHandle(SQL_HANDLE_DESC, &connection, &descriptor) == SQL_SUCCESS); + StatementHandle other(&connection); + check(SQLSetStmtAttr(&statement, SQL_ATTR_APP_ROW_DESC, descriptor, 0) == SQL_SUCCESS); + check(SQLSetStmtAttr(&other, SQL_ATTR_APP_PARAM_DESC, descriptor, 0) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_DESC, descriptor) == SQL_SUCCESS); + check(statement.appRowDesc == statement.implicitAppRowDesc && + other.appParamDesc == other.implicitAppParamDesc); + check(SQLSetStmtAttr(&statement, SQL_ATTR_APP_ROW_DESC, other.appRowDesc, 0) == SQL_ERROR); + check(SQLSetStmtAttr(&statement, SQL_ATTR_APP_PARAM_DESC, statement.impParamDesc, 0) == + SQL_ERROR); + + SQLCHAR sql[] = "insert into t values (?)"; + check(SQLPrepare(&statement, sql, SQL_NTS) == SQL_SUCCESS); + SQLLEN indicator = SQL_DATA_AT_EXEC; + check(SQLBindParameter(&statement, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 10, 0, values, + sizeof(values), &indicator) == SQL_SUCCESS); + check(SQLExecute(&statement) == SQL_NEED_DATA); + check(SQLCancel(&statement) == SQL_SUCCESS); + check(SQLSetDescField(statement.appParamDesc, 0, SQL_DESC_COUNT, nullptr, 0) == SQL_SUCCESS); + check(SQLExecute(&statement) == SQL_ERROR); + std::string state, message; + int native = 0; + check(statement.getDiagnostic(1, state, message, native) && state == "07002"); + indicator = -10; + check(SQLBindParameter(&statement, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 10, 0, values, + sizeof(values), &indicator) == SQL_SUCCESS); + check(SQLExecute(&statement) == SQL_ERROR); + check(statement.getDiagnostic(1, state, message, native) && state == "HY090"); +} + +static bool invalid(const ConnectionHandle& c) { + try { + c.ValidateTransport(); + return false; + } catch (const std::invalid_argument&) { + return true; + } +} + +static bool hasState(ODBCHandle& handle, const std::string& expected) { + std::string state, message; + int native = 0; + return handle.getDiagnostic(1, state, message, native) && state == expected; +} + +static void checkReviewedMetadata() { + ConnectionHandle connection(nullptr); + StatementHandle statement(&connection); + statement.AllocateSessionResultSet(); + statement.resultSetPtr->columnNames = {"long_label", "flag", "large"}; + statement.resultSetPtr->columnTypes = {"STRING", "BOOLEAN", "INT64"}; + statement.resultSetPtr->numColumns = 3; + SQLULEN size = 0; + check(SQLDescribeCol(&statement, 1, nullptr, 0, nullptr, nullptr, &size, nullptr, nullptr) == + SQL_SUCCESS && + size == 2147483647); + const SQLSMALLINT types[] = {SQL_WVARCHAR, SQL_BIT, SQL_BIGINT}; + for (SQLUSMALLINT col = 1; col <= 3; ++col) { + SQLLEN type = 0x12345678; + check(SQLColAttribute(&statement, col, SQL_DESC_CONCISE_TYPE, nullptr, 0, nullptr, &type) == + SQL_SUCCESS && + type == types[col - 1]); + check(SQLColAttribute(&statement, col, SQL_DESC_TYPE, nullptr, 0, nullptr, &type) == + SQL_SUCCESS && + type == types[col - 1]); + } + SQLSMALLINT length = 0; + check(SQLColAttribute(&statement, 1, SQL_DESC_LABEL, nullptr, 0, &length, nullptr) == + SQL_SUCCESS && + length == 10); + char sentinel[8]; + std::memset(sentinel, 'X', sizeof(sentinel)); + check(SQLColAttribute(&statement, 1, SQL_DESC_LABEL, sentinel, -1, &length, nullptr) == + SQL_ERROR && + hasState(statement, "HY090")); + for (char byte : sentinel) + check(byte == 'X'); + check(SQLDescribeCol(&statement, 1, reinterpret_cast(sentinel), -1, &length, nullptr, + nullptr, nullptr, nullptr) == SQL_ERROR && + hasState(statement, "HY090")); + for (char byte : sentinel) + check(byte == 'X'); + SQLSMALLINT type = 0; + check(SQLDescribeCol(&statement, 1, reinterpret_cast(sentinel), 4, &length, &type, + &size, nullptr, nullptr) == SQL_SUCCESS_WITH_INFO && + std::string(sentinel) == "lon" && length == 10 && type == SQL_WVARCHAR && + size == 2147483647 && hasState(statement, "01004")); + check(SQLColAttribute(&statement, 1, SQL_DESC_LABEL, sentinel, 1, &length, nullptr) == + SQL_SUCCESS_WITH_INFO && + sentinel[0] == 0 && length == 10 && hasState(statement, "01004")); + check(SQLColAttribute(&statement, 1, SQL_DESC_NAME, sentinel, 0, &length, nullptr) == + SQL_SUCCESS_WITH_INFO && + hasState(statement, "01004")); + SQLWCHAR wide[3]{}; + check(SQLDescribeColW(&statement, 1, wide, 3, &length, nullptr, &size, nullptr, nullptr) == + SQL_SUCCESS_WITH_INFO && + wide[0] == 'l' && wide[1] == 'o' && wide[2] == 0 && size == 2147483647 && length == 10 && + hasState(statement, "01004")); + check(SQLColAttributeW(&statement, 1, SQL_DESC_LABEL, nullptr, 0, &length, nullptr) == + SQL_SUCCESS && + length == 10 * sizeof(SQLWCHAR)); + char quote[4]{}; + connection.isTableModel = false; + check(SQLGetInfo(&connection, SQL_IDENTIFIER_QUOTE_CHAR, quote, sizeof(quote), &length) == + SQL_SUCCESS && + std::string(quote) == "`" && length == 1); + connection.isTableModel = true; + check(SQLGetInfo(&connection, SQL_IDENTIFIER_QUOTE_CHAR, quote, sizeof(quote), &length) == + SQL_SUCCESS && + std::string(quote) == "\""); +} + +static uint64_t numericMagnitude(const SQL_NUMERIC_STRUCT& value) { + uint64_t result = 0; + for (int i = 7; i >= 0; --i) + result = (result << 8) | value.val[i]; + for (int i = 8; i < SQL_MAX_NUMERIC_LEN; ++i) + check(value.val[i] == 0); + return result; +} + +static void checkReviewedNumeric() { + SQL_NUMERIC_STRUCT value{}; + check(ODBCField(std::numeric_limits::min()).toNumeric(&value) && value.sign == 0 && + numericMagnitude(value) == (uint64_t(1) << 63)); + check(ODBCField(std::numeric_limits::max()).toNumeric(&value) && value.sign == 1 && + numericMagnitude(value) == static_cast(std::numeric_limits::max())); + check(ODBCField(std::numeric_limits::min()).toNumeric(&value) && value.sign == 0 && + numericMagnitude(value) == (uint64_t(1) << 31)); + ConnectionHandle connection(nullptr); + StatementHandle statement(&connection); + statement.AllocateSessionResultSet(); + statement.resultSetPtr->columnNames = {"n"}; + statement.resultSetPtr->columnTypes = {"INT64"}; + statement.resultSetPtr->numColumns = 1; + statement.resultSetPtr->numRows = 1; + statement.resultSetPtr->data = {{ODBCField(int64_t(123))}}; + statement.curRow = 0; + SQLLEN length = 0; + check(SQLGetData(&statement, 1, SQL_C_NUMERIC, &value, sizeof(value), &length) == SQL_SUCCESS && + value.scale == 0 && value.precision == 38 && numericMagnitude(value) == 123 && + length == sizeof(value)); + check(SQLBindCol(&statement, 1, SQL_C_NUMERIC, &value, sizeof(value), &length) == SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_PRECISION, + reinterpret_cast(5), 0) == SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_SCALE, reinterpret_cast(2), + 0) == SQL_SUCCESS); + check(SQLGetData(&statement, 1, SQL_ARD_TYPE, &value, sizeof(value), &length) == SQL_SUCCESS && + value.scale == 2 && value.precision == 5 && numericMagnitude(value) == 12300); + check(SQLGetData(&statement, 1, SQL_C_NUMERIC, &value, sizeof(value), &length) == SQL_SUCCESS && + value.scale == 0 && numericMagnitude(value) == 123); + statement.curRow = -1; + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_DATA_PTR, &value, 0) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS && value.scale == 2 && + numericMagnitude(value) == 12300); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_SCALE, reinterpret_cast(-1), + 0) == SQL_SUCCESS); + check(SQLGetData(&statement, 1, SQL_ARD_TYPE, &value, sizeof(value), &length) == + SQL_SUCCESS_WITH_INFO && + value.scale == -1 && numericMagnitude(value) == 12 && hasState(statement, "01S07")); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_PRECISION, + reinterpret_cast(2), 0) == SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_SCALE, nullptr, 0) == SQL_SUCCESS); + check(SQLGetData(&statement, 1, SQL_ARD_TYPE, &value, sizeof(value), &length) == SQL_ERROR && + hasState(statement, "22003")); + check(SQLFreeStmt(&statement, SQL_UNBIND) == SQL_SUCCESS); + for (const auto& approximate : + {ODBCField(1.5), ODBCField(float(-1.5)), ODBCField(std::numeric_limits::infinity()), + ODBCField(std::numeric_limits::quiet_NaN())}) { + statement.resultSetPtr->data = {{approximate}}; + std::memset(&value, 0x5a, sizeof(value)); + auto unchanged = value; + check(SQLGetData(&statement, 1, SQL_C_NUMERIC, &value, sizeof(value), &length) == SQL_ERROR && + hasState(statement, "07006") && std::memcmp(&value, &unchanged, sizeof(value)) == 0); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_CONCISE_TYPE, + reinterpret_cast(SQL_C_NUMERIC), 0) == SQL_SUCCESS); + check(SQLSetDescField(statement.appRowDesc, 1, SQL_DESC_SCALE, reinterpret_cast(1), + 0) == SQL_SUCCESS); + check(SQLGetData(&statement, 1, SQL_ARD_TYPE, &value, sizeof(value), &length) == SQL_ERROR && + hasState(statement, "07006") && std::memcmp(&value, &unchanged, sizeof(value)) == 0); + } + check(!ODBCField::canConvert(TSDataType::DOUBLE, SQL_C_NUMERIC)); + check(!ODBCField::canConvert(TSDataType::FLOAT, SQL_C_NUMERIC)); +} + +static void checkReviewedHandleLifetime() { + SQLHANDLE env = nullptr, dbc = nullptr, first = nullptr, second = nullptr, descriptor = nullptr; + check(SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &first) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &second) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_DESC, dbc, &descriptor) == SQL_SUCCESS); + check(SQLSetStmtAttr(first, SQL_ATTR_APP_ROW_DESC, descriptor, 0) == SQL_SUCCESS); + check(SQLSetStmtAttr(second, SQL_ATTR_APP_PARAM_DESC, descriptor, 0) == SQL_SUCCESS); + auto implicit = static_cast(first)->impRowDesc; + auto* connection = static_cast(dbc); + check(connection->statements.size() == 2 && connection->descriptors.size() == 9); + std::weak_ptr result; + static_cast(first)->AllocateSessionResultSet(); + result = static_cast(first)->resultSetPtr; + check(SQLDisconnect(dbc) == SQL_SUCCESS && result.expired()); + check(connection->statements.empty() && connection->descriptors.empty()); + check(!ODBCHandle::isLiveHandle(implicit, SQL_HANDLE_DESC)); + check(SQLFreeHandle(SQL_HANDLE_DBC, dbc) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_STMT, first) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_STMT, second) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_DESC, descriptor) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_DESC, implicit) == SQL_INVALID_HANDLE); + check(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &first) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_DESC, dbc, &descriptor) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_DBC, dbc) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_STMT, first) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_DESC, descriptor) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_ENV, env) == SQL_SUCCESS); +} + +int main() { + try { + checkReviewedMetadata(); + checkReviewedNumeric(); + checkReviewedHandleLifetime(); + checkDescriptorBindings(); + ConnectionHandle c(nullptr); + check(!invalid(c)); + SetConnectionHandle(&c, "SSL", "true"); + check(invalid(c)); + SetConnectionHandle(&c, "sSlCa", "/certs/ca.pem"); + check(!invalid(c)); + SetConnectionHandle(&c, "SSLCERT", "/certs/client.pem"); + check(invalid(c)); + SetConnectionHandle(&c, "SSLKEY", "/certs/client.key"); + check(!invalid(c)); + SetConnectionHandle(&c, "RESTFUL", "1"); + check(invalid(c)); + SetConnectionHandle(&c, "RESTFUL", "0"); + SetConnectionHandle(&c, "PORT", "70000"); + check(invalid(c)); + SetConnectionHandle(&c, "PORT", "6667"); + SetConnectionHandle(&c, "SSL", "garbage"); + check(invalid(c)); + SetConnectionHandle(&c, "SSL", "0"); + SetConnectionHandle(&c, "RESTFUL", "garbage"); + check(invalid(c)); + SetConnectionHandle(&c, "RESTFUL", "0"); + check(IsQueryStatement(" SELECT * FROM t")); + check(IsQueryStatement("show version")); + check(IsQueryStatement("-- comment\n /* comment */ SELECT/* hint */1")); + check(!IsQueryStatement("/* comment only */")); + check(!IsQueryStatement("selection")); + check(!IsQueryStatement("insert into t values (1)")); + + SQLHANDLE env = nullptr; + check(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, nullptr) == SQL_ERROR); + check(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env) == SQL_SUCCESS && env); + SQLHANDLE dbc = nullptr; + check(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc) == SQL_SUCCESS && dbc); + check(SQLFreeHandle(SQL_HANDLE_ENV, dbc) == SQL_INVALID_HANDLE); + check(SQLFreeHandle(SQL_HANDLE_DBC, dbc) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_ENV, env) == SQL_SUCCESS); + + SQLUINTEGER functions = 0; + SQLSMALLINT infoLength = 0; + check(SQLGetInfo(&c, SQL_NUMERIC_FUNCTIONS, &functions, sizeof(functions), &infoLength) == + SQL_SUCCESS); + check(functions == 0 && infoLength == sizeof(SQLUINTEGER)); + functions = 0; + check(SQLGetInfo(&c, SQL_TIMEDATE_FUNCTIONS, &functions, sizeof(functions), &infoLength) == + SQL_SUCCESS); + check(functions == 0); + SQLUSMALLINT supported = SQL_FALSE; + check(SQLGetFunctions(&c, SQL_API_SQLNATIVESQL, &supported) == SQL_SUCCESS && + supported == SQL_TRUE); + check(SQLGetFunctions(&c, SQL_API_SQLBINDPARAM, &supported) == SQL_SUCCESS && + supported == SQL_TRUE); + check(SQLGetFunctions(&c, SQL_API_SQLBINDPARAMETER, &supported) == SQL_SUCCESS && + supported == SQL_TRUE); + check(SQLGetFunctions(&c, SQL_API_SQLDRIVERS, &supported) == SQL_SUCCESS && + supported == SQL_TRUE); +#ifdef SQL_API_SQLCOMPLETEASYNC + check(SQLGetFunctions(&c, SQL_API_SQLCOMPLETEASYNC, &supported) == SQL_SUCCESS && + supported == SQL_FALSE); +#endif +#ifdef SQL_API_SQLCANCELHANDLE + check(SQLGetFunctions(&c, SQL_API_SQLCANCELHANDLE, &supported) == SQL_SUCCESS && + supported == SQL_TRUE); +#endif + check(ParameterMarkerPositions("select '?', ? -- ?\n, \"?\", ? /* ? */").size() == 2); + check(ParameterMarkerPositions("select `?`, `a``?`, ?, 'it''s ?'").size() == 1); + SQLCHAR nativeSql[8]{}; + SQLINTEGER nativeSqlLength = 0; + SQLCHAR inputSql[] = "SELECT 1"; + check(SQLNativeSql(&c, inputSql, SQL_NTS, nativeSql, sizeof(nativeSql), &nativeSqlLength) == + SQL_SUCCESS_WITH_INFO); + check(nativeSqlLength == 8 && std::string(reinterpret_cast(nativeSql)) == "SELECT "); + SQLCHAR stateBuffer[6]{}; + SQLCHAR emptyMessageBuffer[1]{}; + check(SQLGetDiagRec(SQL_HANDLE_DBC, &c, 1, stateBuffer, nullptr, emptyMessageBuffer, 0, + nullptr) == SQL_SUCCESS_WITH_INFO); + check(SQLGetDiagRec(SQL_HANDLE_DBC, &c, 1, stateBuffer, nullptr, nullptr, 0, nullptr) == + SQL_SUCCESS); + check(std::string(reinterpret_cast(stateBuffer)) == "01004"); + SQLCHAR driverName[5]{}; + check(SQLGetInfo(&c, SQL_DRIVER_NAME, driverName, sizeof(driverName), &infoLength) == + SQL_SUCCESS_WITH_INFO); + check(infoLength > 4); + SQLUINTEGER optionValue = 99; + check(SQLGetConnectOption(&c, SQL_AUTOCOMMIT, &optionValue) == SQL_SUCCESS && + optionValue == SQL_AUTOCOMMIT_ON); + check(SQLSetConnectOption(&c, SQL_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF) == SQL_ERROR); + check(SQLSetConnectOption(&c, SQL_LOGIN_TIMEOUT, 7) == SQL_SUCCESS); + check(SQLGetConnectOption(&c, SQL_LOGIN_TIMEOUT, &optionValue) == SQL_SUCCESS && + optionValue == 7); + StatementHandle statement(&c); + check(SQLCancelHandle(SQL_HANDLE_STMT, &statement) == SQL_SUCCESS); + check(SQLCancelHandle(SQL_HANDLE_DBC, &c) == SQL_SUCCESS); + int optionNative = 0; + std::string statementState, statementMessage; + SQLHANDLE implicitArd = nullptr; + SQLHANDLE implicitApd = nullptr; + check(SQLGetStmtAttr(&statement, SQL_ATTR_APP_ROW_DESC, &implicitArd, 0, nullptr) == + SQL_SUCCESS && + implicitArd != nullptr); + check(SQLGetStmtAttr(&statement, SQL_ATTR_APP_PARAM_DESC, &implicitApd, 0, nullptr) == + SQL_SUCCESS && + implicitApd != nullptr && implicitApd != implicitArd); + check(SQLFreeHandle(SQL_HANDLE_DESC, implicitArd) == SQL_ERROR); + SQLHANDLE descriptor = nullptr; + SQLHANDLE descriptorCopy = nullptr; + check(SQLAllocHandle(SQL_HANDLE_DESC, &c, &descriptor) == SQL_SUCCESS); + check(SQLAllocHandle(SQL_HANDLE_DESC, &c, &descriptorCopy) == SQL_SUCCESS); + SQLLEN descriptorLength = 32; + SQLCHAR descriptorBuffer[32]{}; + check(SQLSetDescRec(descriptor, 1, SQL_C_CHAR, 0, descriptorLength, 0, 0, descriptorBuffer, + &descriptorLength, nullptr) == SQL_SUCCESS); + SQLSMALLINT descriptorCount = 0; + check(SQLGetDescField(descriptor, 0, SQL_DESC_COUNT, &descriptorCount, 0, nullptr) == + SQL_SUCCESS && + descriptorCount == 1); + check(SQLCopyDesc(descriptor, descriptorCopy) == SQL_SUCCESS); + check(SQLSetDescRec(descriptorCopy, 1, SQL_DATETIME, SQL_CODE_TIMESTAMP, + sizeof(SQL_TIMESTAMP_STRUCT), 0, 0, nullptr, nullptr, + nullptr) == SQL_SUCCESS); + SQLSMALLINT conciseType = 0; + check(SQLGetDescField(descriptorCopy, 1, SQL_DESC_CONCISE_TYPE, &conciseType, 0, nullptr) == + SQL_SUCCESS && + conciseType == SQL_C_TYPE_TIMESTAMP); + check(SQLSetStmtAttr(&statement, SQL_ATTR_APP_ROW_DESC, descriptor, 0) == SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_APP_ROW_DESC, nullptr, 0) == SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_ROW_ARRAY_SIZE, reinterpret_cast(1), 0) == + SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_ROW_ARRAY_SIZE, reinterpret_cast(2), 0) == + SQL_SUCCESS); + SQLULEN rowArraySize = 0; + check(SQLGetStmtAttr(&statement, SQL_ATTR_ROW_ARRAY_SIZE, &rowArraySize, 0, nullptr) == + SQL_SUCCESS && + rowArraySize == 2); + statement.AllocateSessionResultSet(); + statement.resultSetPtr->columnNames = {"v"}; + statement.resultSetPtr->columnTypes = {"INT32"}; + statement.resultSetPtr->data = { + {ODBCField(int32_t(1))}, {ODBCField(int32_t(2))}, {ODBCField(int32_t(3))}}; + statement.resultSetPtr->numColumns = 1; + statement.resultSetPtr->numRows = 3; + SQLINTEGER rowValues[2]{}; + SQLLEN rowLengths[2]{}; + SQLULEN rowsFetched = 0; + SQLUSMALLINT rowStatus[2]{}; + check(SQLBindCol(&statement, 1, SQL_C_SLONG, rowValues, sizeof(SQLINTEGER), rowLengths) == + SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_ROWS_FETCHED_PTR, &rowsFetched, 0) == SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_ROW_STATUS_PTR, rowStatus, 0) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS && rowsFetched == 2 && rowValues[0] == 1 && + rowValues[1] == 2 && rowStatus[0] == SQL_ROW_SUCCESS && rowStatus[1] == SQL_ROW_SUCCESS); + check(SQLCloseCursor(&statement) == SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_ROW_ARRAY_SIZE, reinterpret_cast(1), 0) == + SQL_SUCCESS); + check(SQLSetStmtAttr(&statement, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(2), 0) == + SQL_SUCCESS); + SQLULEN parameterSetSize = 0; + check(SQLGetStmtAttr(&statement, SQL_ATTR_PARAMSET_SIZE, ¶meterSetSize, 0, nullptr) == + SQL_SUCCESS && + parameterSetSize == 2); + check(SQLSetStmtAttr(&statement, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(1), 0) == + SQL_SUCCESS); + SQLCHAR preparedSql[] = "insert into t values (?, '?', ?)"; + check(SQLPrepare(&statement, preparedSql, SQL_NTS) == SQL_SUCCESS); + SQLSMALLINT parameterCount = 0; + check(SQLNumParams(&statement, ¶meterCount) == SQL_SUCCESS && parameterCount == 2); + SQLLEN dataAtExecution = SQL_DATA_AT_EXEC; + char parameterToken = 0; + check(SQLBindParameter(&statement, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 16, 0, + ¶meterToken, 16, &dataAtExecution) == SQL_SUCCESS); + SQLINTEGER integerParameter = 7; + check(SQLBindParameter(&statement, 2, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 10, 0, + &integerParameter, 0, nullptr) == SQL_SUCCESS); + check(SQLExecute(&statement) == SQL_NEED_DATA); + SQLPOINTER requestedParameter = nullptr; + check(SQLParamData(&statement, &requestedParameter) == SQL_NEED_DATA && + requestedParameter == ¶meterToken); + check(SQLPutData(&statement, const_cast("streamed"), SQL_NTS) == SQL_SUCCESS); + check(SQLCancel(&statement) == SQL_SUCCESS); + check(SQLParamData(&statement, &requestedParameter) == SQL_ERROR); + + SQLLEN arrayDataAtExecution[2] = {SQL_DATA_AT_EXEC, SQL_DATA_AT_EXEC}; + char parameterTokens[32]{}; + SQLINTEGER integerParameters[2] = {7, 8}; + check(SQLSetStmtAttr(&statement, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(2), 0) == + SQL_SUCCESS); + check(SQLBindParameter(&statement, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 16, 0, + parameterTokens, 16, arrayDataAtExecution) == SQL_SUCCESS); + check(SQLBindParameter(&statement, 2, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 10, 0, + integerParameters, 0, nullptr) == SQL_SUCCESS); + check(SQLExecute(&statement) == SQL_NEED_DATA); + check(SQLParamData(&statement, &requestedParameter) == SQL_NEED_DATA && + requestedParameter == parameterTokens); + check(SQLPutData(&statement, const_cast("first"), SQL_NTS) == SQL_SUCCESS); + check(SQLParamData(&statement, &requestedParameter) == SQL_NEED_DATA && + requestedParameter == parameterTokens + 16); + check(SQLPutData(&statement, const_cast("second"), SQL_NTS) == SQL_SUCCESS); + check(statement.parameterBindings[0].streamedData[0] == "first" && + statement.parameterBindings[0].streamedData[1] == "second"); + check(SQLCancel(&statement) == SQL_SUCCESS); + check(SQLFreeStmt(&statement, SQL_RESET_PARAMS) == SQL_SUCCESS); + check(SQLSpecialColumns(&statement, SQL_BEST_ROWID, nullptr, 0, nullptr, 0, nullptr, 0, + SQL_SCOPE_SESSION, SQL_NULLABLE) == SQL_SUCCESS); + SQLSMALLINT catalogColumns = 0; + check(SQLNumResultCols(&statement, &catalogColumns) == SQL_SUCCESS && catalogColumns == 8); + check(SQLStatistics(&statement, nullptr, 0, nullptr, 0, nullptr, 0, SQL_INDEX_ALL, SQL_QUICK) == + SQL_SUCCESS); + check(SQLNumResultCols(&statement, &catalogColumns) == SQL_SUCCESS && catalogColumns == 13); + SQLCHAR typeName[16]{}; + SQLSMALLINT typeNameLength = 0; + check(SQLColAttribute(&statement, 2, SQL_DESC_TYPE_NAME, nullptr, 0, &typeNameLength, + nullptr) == SQL_SUCCESS && + typeNameLength == 4); + check(SQLColAttribute(&statement, 2, SQL_DESC_TYPE_NAME, typeName, sizeof(typeName), nullptr, + nullptr) == SQL_SUCCESS && + std::string(reinterpret_cast(typeName)) == "TEXT"); + check(SQLFreeStmt(&statement, SQL_UNBIND) == SQL_SUCCESS); + check(SQLGetTypeInfo(&statement, SQL_ALL_TYPES) == SQL_SUCCESS); + SQLSMALLINT catalogType = 0; + check(SQLDescribeCol(&statement, 2, nullptr, 0, nullptr, &catalogType, nullptr, nullptr, + nullptr) == SQL_SUCCESS && + catalogType == SQL_SMALLINT); + check(SQLFetch(&statement) == SQL_SUCCESS); + SQLCHAR firstType[32]{}; + SQLLEN firstTypeLength = 0; + check(SQLGetData(&statement, 1, SQL_C_CHAR, firstType, sizeof(firstType), &firstTypeLength) == + SQL_SUCCESS); + check(SQLGetTypeInfo(&statement, SQL_ALL_TYPES) == SQL_SUCCESS); + check(SQLFetch(&statement) == SQL_SUCCESS); + SQLCHAR refreshedType[32]{}; + check(SQLGetData(&statement, 1, SQL_C_CHAR, refreshedType, sizeof(refreshedType), + &firstTypeLength) == SQL_SUCCESS && + std::string(reinterpret_cast(firstType)) == + std::string(reinterpret_cast(refreshedType))); + check(SQLSetStmtAttr(&statement, SQL_ATTR_CURSOR_TYPE, + reinterpret_cast(SQL_CURSOR_STATIC), 0) == SQL_ERROR); + check(SQLCloseCursor(&statement) == SQL_SUCCESS); + check(SQLCloseCursor(&statement) == SQL_ERROR); + check(statement.getDiagnostic(1, statementState, statementMessage, optionNative) && + statementState == "24000"); + check(SQLEndTran(SQL_HANDLE_DBC, &c, SQL_ROLLBACK) == SQL_ERROR); + check(c.getDiagnostic(1, statementState, statementMessage, optionNative) && + statementState == "HYC00"); + check(SQLFreeHandle(SQL_HANDLE_DESC, descriptorCopy) == SQL_SUCCESS); + check(SQLFreeHandle(SQL_HANDLE_DESC, descriptor) == SQL_SUCCESS); + auto parsed = ParseConnectionString( + " Driver={Apache IoTDB ODBC Driver};PWD={a;b}}c};SSLCA={C:\\certs\\ca.pem};SSL=1;"); + check(parsed.size() == 4 && parsed[1].second == "a;b}c"); + try { + ParseConnectionString("SSLCA={broken"); + check(false); + } catch (const std::invalid_argument&) { + } + SQLCHAR output[64]{}; + SQLLEN size = 0; + ODBCField date(IoTDBDate(2024, 2, 29)); + check(date.toChar(output, sizeof(output), &size)); + check(std::string(reinterpret_cast(output)) == "2024-02-29"); + SQL_DATE_STRUCT d{}; + check(date.toDate(&d) && d.year == 2024 && d.month == 2 && d.day == 29); + ODBCField null = ODBCField::null(); + check(null.toChar(output, sizeof(output), &size) && size == SQL_NULL_DATA); + ODBCField unicode(std::string("\xe4\xb8\xad\xe6\x96\x87")); + SQLWCHAR wide[16]{}; + check(unicode.toWChar(wide, sizeof(wide), &size)); + check(size == 4 && wide[0] == 0x4e2d && wide[1] == 0x6587 && wide[2] == 0); + Field timestamp; + timestamp.dataType = TSDataType::TIMESTAMP; + timestamp.longV = int64_t(1234); + SQL_TIMESTAMP_STRUCT ts{}; + check(ODBCField(timestamp).toTimestamp(&ts)); + check(ts.year == 1970 && ts.second == 1 && ts.fraction == 234000000); + timestamp.longV = int64_t(-1); + check(ODBCField(timestamp).toTimestamp(&ts)); + check(ts.year == 1969 && ts.month == 12 && ts.day == 31 && ts.hour == 23 && ts.minute == 59 && + ts.second == 59 && ts.fraction == 999000000); + // This fails before network I/O and exercises the public diagnostic path. + ConnectionHandle bad(nullptr); + bad.addDiagnostic("HY000", "Stale error from a previous connection attempt"); + std::string connection = "LOGLEVEL=4;SSL=1;SSLCA=x;RESTFUL=1;PWD=do-not-log"; + check(SQLDriverConnect(&bad, nullptr, reinterpret_cast(&connection[0]), + static_cast(connection.size()), nullptr, 0, nullptr, + SQL_DRIVER_NOPROMPT) == SQL_ERROR); + std::string state, message; + int native = 0; + check(bad.getDiagnostic(1, state, message, native) && state == "08001"); + SQLCHAR sqlState[6]{}, errorMessage[512]{}; + SQLINTEGER code = 0; + SQLSMALLINT errorLength = 0; + check(SQLError(nullptr, &bad, nullptr, sqlState, &code, errorMessage, sizeof(errorMessage), + &errorLength) == SQL_SUCCESS); + check(SQLError(nullptr, &bad, nullptr, sqlState, &code, errorMessage, sizeof(errorMessage), + &errorLength) == SQL_NO_DATA); +#ifndef WIN32 + ConnectionHandle dsn(nullptr); + std::string dsnString = "DSN=ODBC_TLS_TEST;SSLCA={override;ca.pem};RESTFUL=1;"; + check(SQLDriverConnect(&dsn, nullptr, reinterpret_cast(&dsnString[0]), SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT) == SQL_ERROR); + check(dsn.sslConfig.useSsl && dsn.sslConfig.trustCertFilePath == "override;ca.pem" && + dsn.sslConfig.clientCertificateFilePath == "dsn-client.pem"); +#endif + std::ifstream log("apache_iotdb_odbc.log"); + std::string logText((std::istreambuf_iterator(log)), std::istreambuf_iterator()); + check(logText.find("do-not-log") == std::string::npos); + std::cout << "ODBC configuration, conversion and diagnostic tests passed\n"; + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } +} diff --git a/iotdb-client/odbc/test/windows-consumers.ps1 b/iotdb-client/odbc/test/windows-consumers.ps1 new file mode 100644 index 0000000000000..f6b6c75e4617a --- /dev/null +++ b/iotdb-client/odbc/test/windows-consumers.ps1 @@ -0,0 +1,180 @@ +# 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. +param([Parameter(Mandatory = $true)][string]$Driver) +$ErrorActionPreference = 'Stop' +if (-not $env:IOTDB_ODBC_CONNECTION) { throw 'Set IOTDB_ODBC_CONNECTION to a disposable table-model server' } +if (-not [Environment]::Is64BitProcess) { throw 'The release driver requires a 64-bit consumer' } + +$dsn = 'IoTDB-' + $PID + '-' + [Guid]::NewGuid().ToString('N').Substring(0, 12) +$dsnPath = 'Software\ODBC\ODBC.INI\' + $dsn +try { + $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey( + $dsnPath, $true, [Microsoft.Win32.RegistryOptions]::None) +} catch { + # ERROR_CHILD_MUST_BE_VOLATILE: temporary user hives require volatile keys. + if (($_.Exception.InnerException.HResult -band 0xffff) -ne 1021) { throw } + $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey( + $dsnPath, $true, [Microsoft.Win32.RegistryOptions]::Volatile) +} +$key.SetValue('Driver', (Resolve-Path -LiteralPath $Driver).Path) +$key.Close() +$options = 'DSN=' + $dsn + ';' + $env:IOTDB_ODBC_CONNECTION +$connection = New-Object System.Data.Odbc.OdbcConnection($options) +$database = 'odbc_consumer_' + [Guid]::NewGuid().ToString('N') +$ado = $null +$recordset = $null +$created = $false + +function Assert-True($Condition, [string]$Message) { + if (-not $Condition) { throw $Message } +} +function Execute([string]$Sql) { + $command = $connection.CreateCommand() + try { $command.CommandText = $Sql; [void]$command.ExecuteNonQuery() } + finally { $command.Dispose() } +} +try { + $connection.Open() + Execute "CREATE DATABASE $database" + $created = $true + Execute "CREATE TABLE $database.samples (device STRING TAG, val INT32 FIELD, txt STRING FIELD, day DATE FIELD, active BOOLEAN FIELD)" + # ChangeDatabase calls SQLSetConnectAttrW; underscores must be accepted. + $connection.ChangeDatabase($database) + Execute "INSERT INTO samples(time,device,val,txt,day,active) VALUES (1,'设备',1,'中文','2024-02-29',true),(2,'设备',2,null,'2024-03-01',false),(3,'设备',3,'','2024-03-02',true),(4,'设备',4,'last','2024-03-03',true)" + $command = $connection.CreateCommand() + try { + $command.CommandText = 'INSERT INTO samples(time,device,val,txt,day,active) VALUES (?,?,?,?,?,?)' + [void]$command.Parameters.Add('time', [System.Data.Odbc.OdbcType]::BigInt) + [void]$command.Parameters.Add('device', [System.Data.Odbc.OdbcType]::NVarChar, 16) + [void]$command.Parameters.Add('val', [System.Data.Odbc.OdbcType]::Int) + [void]$command.Parameters.Add('txt', [System.Data.Odbc.OdbcType]::NVarChar, 12000) + [void]$command.Parameters.Add('day', [System.Data.Odbc.OdbcType]::Date) + [void]$command.Parameters.Add('active', [System.Data.Odbc.OdbcType]::Bit) + $longText = '中文😀' * 1500 + $values = [object[]]@(5L, '设备', 5, $longText, [DateTime]'2024-03-04', $true) + for ($i = 0; $i -lt $values.Length; $i++) { $command.Parameters[$i].Value = $values[$i] } + [void]$command.ExecuteNonQuery() + $command.Parameters.Clear() + $command.CommandText = 'SELECT time,device,val,txt,day,active FROM samples ORDER BY time' + $reader = $command.ExecuteReader() + try { + Assert-True ($reader.FieldCount -eq 6) 'The explicit time projection was lost' + $count = 0 + while ($reader.Read()) { + $count++ + Assert-True ($reader.GetInt64(0) -eq $count) 'Time or batch order mismatch' + Assert-True ($reader.GetString(1) -eq '设备') 'Unicode device mismatch' + Assert-True ($reader.GetInt32(2) -eq $count) 'Integer mismatch' + if ($count -eq 1) { Assert-True ($reader.GetString(3) -eq '中文') 'Unicode data mismatch' } + if ($count -eq 2) { Assert-True ($reader.IsDBNull(3)) 'NULL mismatch' } + if ($count -eq 3) { Assert-True ($reader.GetString(3) -eq '') 'Empty string mismatch' } + if ($count -eq 5) { Assert-True ($reader.GetString(3) -eq $longText) 'Chunked Unicode data mismatch' } + Assert-True ($reader.GetDateTime(4).Year -eq 2024) 'DATE conversion mismatch' + Assert-True ($reader.GetBoolean(5) -eq ($count -ne 2)) 'Boolean conversion mismatch' + } + Assert-True ($count -eq 5) 'Rows were lost across Session batches' + } finally { $reader.Dispose() } + $command.CommandText = 'SELECT txt FROM samples WHERE val < 0' + $reader = $command.ExecuteReader() + try { Assert-True (-not $reader.Read()) 'Empty query returned a row' } + finally { $reader.Dispose() } + } finally { $command.Dispose() } + + # Casting a PowerShell array containing $null to string[] changes nulls to + # empty strings, which have different ODBC catalog semantics. + $tableRestrictions = [string[]]::new(3) + $tableRestrictions[0] = $database + $tableRestrictions[2] = 'samples' + $tables = $connection.GetSchema('Tables', $tableRestrictions) + Assert-True ($tables.Rows.Count -eq 1) '.NET table browsing mismatch' + $columnRestrictions = [string[]]::new(4) + $columnRestrictions[0] = $database + $columnRestrictions[2] = 'samples' + $columns = $connection.GetSchema('Columns', $columnRestrictions) + Assert-True ($columns.Rows.Count -eq 6) ('.NET column browsing expected 6 rows, got ' + $columns.Rows.Count) + $columnRestrictions[2] = 'missing' + $none = $connection.GetSchema('Columns', $columnRestrictions) + Assert-True ($none.Rows.Count -eq 0) 'An empty catalog search must succeed' + Write-Output 'Windows .NET ODBC checks passed' + + # MSDASQL/ADO is also used by Office import workflows. In particular, it + # requests legacy column attributes and chooses C types from SQL metadata. + $ado = New-Object -ComObject ADODB.Connection + $ado.Open('Provider=MSDASQL;' + $options) + $recordset = $ado.Execute("SELECT time,txt,day FROM $database.samples WHERE val = 1") + Write-Output 'ADO query opened' + Assert-True (-not $recordset.EOF) 'ADO query returned no data' + Assert-True ($recordset.Fields.Item(1).Value -eq '中文') 'ADO Unicode import mismatch' + Assert-True ($recordset.Fields.Item(2).Value.Year -eq 2024) 'ADO DATE import mismatch' + $recordset.Close() + Write-Output 'ADO data checks passed' + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($recordset) + $recordset = $null + $recordset = $ado.OpenSchema(4, [object[]]@($database, [DBNull]::Value, 'samples', [DBNull]::Value)) + $count = 0 + while (-not $recordset.EOF) { $count++; $recordset.MoveNext() } + Assert-True ($count -eq 6) 'ADO column browsing mismatch' + $tree = New-Object System.Data.Odbc.OdbcConnection( + $options + ';ISTABLEMODEL=0;DATABASE=;') + $treeDatabase = 'root.' + $database + $treeCreated = $false + try { + $tree.Open() + $command = $tree.CreateCommand() + try { + $command.CommandText = "CREATE DATABASE $treeDatabase" + [void]$command.ExecuteNonQuery() + $treeCreated = $true + $command.CommandText = "INSERT INTO $treeDatabase.d(time,v,txt) VALUES(1,1,'中文'),(2,2,'two'),(3,3,'three')" + [void]$command.ExecuteNonQuery() + $tableRestrictions[0] = $treeDatabase + $tableRestrictions[2] = 'd' + Assert-True ($tree.GetSchema('Tables', $tableRestrictions).Rows.Count -eq 1) 'Tree table browsing mismatch' + $columnRestrictions[0] = $treeDatabase + $columnRestrictions[2] = 'd' + Assert-True ($tree.GetSchema('Columns', $columnRestrictions).Rows.Count -eq 3) 'Tree column browsing mismatch' + } finally { $command.Dispose() } + } finally { + if ($treeCreated) { + $command = $tree.CreateCommand() + try { + $command.CommandText = "DELETE DATABASE $treeDatabase" + [void]$command.ExecuteNonQuery() + } finally { $command.Dispose() } + } + $tree.Dispose() + } + Write-Output 'Windows .NET ODBC and MSDASQL/ADO consumer checks passed' +} finally { + if ($recordset) { + if ($recordset.State) { $recordset.Close() } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($recordset) + } + if ($ado) { + if ($ado.State) { $ado.Close() } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($ado) + } + try { + if ($created -and $connection.State -eq 'Open') { + $connection.ChangeDatabase('information_schema') + Execute "DROP DATABASE $database" + } + } finally { + $connection.Dispose() + [Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree($dsnPath, $false) + } +} diff --git a/iotdb-client/pom.xml b/iotdb-client/pom.xml index 1d0c8d42e2123..a198f8a641f8e 100644 --- a/iotdb-client/pom.xml +++ b/iotdb-client/pom.xml @@ -39,6 +39,12 @@ subscription + + with-odbc + + odbc + + with-cpp