diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml index 9b935a766c..6e2c31686e 100644 --- a/.github/workflows/release_publish.yaml +++ b/.github/workflows/release_publish.yaml @@ -61,7 +61,7 @@ jobs: id: deb-package-cache-key shell: bash run: | - echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" + echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'odbc/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" - name: Restore Debian package build cache uses: actions/cache/restore@v4 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 54e79c108d..0a3ad051f1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,6 +8,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] branches: - main + - odbc-driver-feature concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -54,9 +55,15 @@ jobs: with: compiler: ${{ matrix.compiler }} - name: Test + if: github.event_name != 'pull_request' || github.base_ref == 'main' shell: bash run: | ctest -j$(nproc) --preset unit + - name: Test ODBC + if: github.event_name == 'pull_request' && github.base_ref == 'odbc-driver-feature' + shell: bash + run: | + ctest --test-dir build/odbc/tests/unit -j$(nproc) --output-on-failure - name: Package integration build shell: bash run: | @@ -132,6 +139,7 @@ jobs: tar -C build -xzf "integration-build-${{ matrix.compiler }}.tar.gz" tar -C "$HOME" -xzf "integration-deps-${{ matrix.compiler }}.tar.gz" - name: Test + if: github.event_name != 'pull_request' || github.base_ref == 'main' shell: bash run: | IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' @@ -144,8 +152,15 @@ jobs: ./.github/scripts/run_iam_integration_tests.sh ;; esac + - name: Test ODBC + if: github.event_name == 'pull_request' && github.base_ref == 'odbc-driver-feature' + shell: bash + run: | + YDB_VERSION=${{ matrix.ydb-version }} \ + ctest --test-dir build/odbc/tests/integration -j2 --output-on-failure test-install: + if: github.event_name != 'pull_request' || github.base_ref == 'main' name: "Test CMake Install" concurrency: group: test-install-${{ github.ref }}-${{ matrix.compiler }} @@ -228,7 +243,7 @@ jobs: id: deb-package-cache-key shell: bash run: | - echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" + echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'odbc/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" - name: Validate dpkg-buildpackage shell: bash diff --git a/.github/workflows/warmup_cache.yaml b/.github/workflows/warmup_cache.yaml index e5864270b4..d0d31089b0 100644 --- a/.github/workflows/warmup_cache.yaml +++ b/.github/workflows/warmup_cache.yaml @@ -94,7 +94,7 @@ jobs: id: deb-package-cache-key shell: bash run: | - echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" + echo "prefix=ubuntu-24.04-deb-packages-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'contrib/**', 'include/**', 'library/**', 'odbc/**', 'plugins/**', 'scripts/build_cpack_deb_packages.sh', 'scripts/generate-debian-directory.sh', 'scripts/googleapis_deb/**', 'src/**', 'third_party/api-common-protos/**', 'tools/**', 'util/**') }}" >> "$GITHUB_OUTPUT" - name: Restore Debian package build cache id: deb-package-cache uses: actions/cache/restore@v4 diff --git a/README.md b/README.md index 4f78712790..2f1479633d 100644 --- a/README.md +++ b/README.md @@ -248,12 +248,17 @@ wget "${BASE}/libydb-cpp-dev_${TAG#v}_amd64.deb" wget "${BASE}/libydb-cpp-iam-dev_${TAG#v}_amd64.deb" wget "${BASE}/libydb-cpp-otel-metrics-dev_${TAG#v}_amd64.deb" wget "${BASE}/libydb-cpp-otel-tracing-dev_${TAG#v}_amd64.deb" +# Optional ODBC driver: +wget "${BASE}/ydb-odbc_${TAG#v}_amd64.deb" sudo apt-get update sudo apt-get install -y \ ./yandex-googleapis-api-common-protos-*.deb \ ./libydb-cpp-dev_*.deb ./libydb-cpp-iam-dev_*.deb \ - ./libydb-cpp-otel-metrics-dev_*.deb ./libydb-cpp-otel-tracing-dev_*.deb + ./libydb-cpp-otel-metrics-dev_*.deb ./libydb-cpp-otel-tracing-dev_*.deb \ + ./ydb-odbc_*.deb + +odbcinst -q -d -n YDB ``` After installation, use the SDK in your CMake project: diff --git a/cmake/PackSDK.cmake b/cmake/PackSDK.cmake index c204e7a49e..c93a48c988 100644 --- a/cmake/PackSDK.cmake +++ b/cmake/PackSDK.cmake @@ -18,6 +18,9 @@ set(CPACK_RESOURCE_FILE_LICENSE "${YDB_SDK_SOURCE_DIR}/LICENSE") set(CPACK_DEB_COMPONENT_INSTALL ON) set(CPACK_COMPONENTS_ALL libydb-cpp libydb-cpp-iam libydb-cpp-otel-metrics libydb-cpp-otel-tracing) +if (YDB_SDK_ODBC) + list(APPEND CPACK_COMPONENTS_ALL ydb-odbc) +endif() set(CPACK_DEBIAN_LIBYDB_CPP_PACKAGE_NAME "libydb-cpp-dev") set(CPACK_DEBIAN_LIBYDB_CPP_PACKAGE_DEPENDS @@ -34,6 +37,18 @@ set(CPACK_DEBIAN_LIBYDB_CPP_OTEL_TRACING_PACKAGE_NAME "libydb-cpp-otel-tracing-d set(CPACK_DEBIAN_LIBYDB_CPP_OTEL_TRACING_PACKAGE_DEPENDS "libydb-cpp-dev (= ${YDB_SDK_VERSION}), libydb-cpp-otel-metrics-dev (= ${YDB_SDK_VERSION})") +if (YDB_SDK_ODBC) + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_NAME" "ydb-odbc") + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_DEPENDS" "odbcinst") + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_SHLIBDEPS" ON) + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_CONTROL_EXTRA" + "${YDB_ODBC_DEBIAN_CONTROL_EXTRA}") + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_CONTROL_STRICT_PERMISSION" ON) + set("CPACK_DEBIAN_YDB-ODBC_PACKAGE_SECTION" "database") + set("CPACK_DEBIAN_YDB-ODBC_DESCRIPTION" + "YDB ODBC driver\n Shared ODBC driver and unixODBC registration for YDB.") +endif() + foreach(component IN ITEMS libydb-cpp libydb-cpp-iam libydb-cpp-otel-metrics libydb-cpp-otel-tracing) string(TOUPPER "${component}" component_upper) string(REPLACE "-" "_" component_var "${component_upper}") diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 16afdd5e70..c8c10f7d05 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -44,6 +44,25 @@ set_target_properties(ydb-odbc PROPERTIES include(GNUInstallDirs) +set(YDB_ODBC_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}" CACHE STRING + "Directory where the YDB ODBC shared library is installed") +set(YDB_ODBC_INSTALL_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}/ydb-odbc" CACHE STRING + "Directory where the YDB ODBC driver registration template is installed") + +if (IS_ABSOLUTE "${YDB_ODBC_INSTALL_LIBDIR}") + set(YDB_ODBC_DRIVER_INSTALL_DIR "${YDB_ODBC_INSTALL_LIBDIR}") +else() + set(YDB_ODBC_DRIVER_INSTALL_DIR + "${CMAKE_INSTALL_PREFIX}/${YDB_ODBC_INSTALL_LIBDIR}") +endif() + +if (IS_ABSOLUTE "${YDB_ODBC_INSTALL_DATADIR}") + set(YDB_ODBC_DRIVER_TEMPLATE_DIR "${YDB_ODBC_INSTALL_DATADIR}") +else() + set(YDB_ODBC_DRIVER_TEMPLATE_DIR + "${CMAKE_INSTALL_PREFIX}/${YDB_ODBC_INSTALL_DATADIR}") +endif() + file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/odbcinst.ini" CONTENT "[YDB] @@ -54,24 +73,45 @@ Setup=$ ) set(YDB_ODBC_DRIVER_PATH - "${CMAKE_INSTALL_FULL_LIBDIR}/libydb-odbc${CMAKE_SHARED_LIBRARY_SUFFIX}") + "${YDB_ODBC_DRIVER_INSTALL_DIR}/libydb-odbc${CMAKE_SHARED_LIBRARY_SUFFIX}") configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/odbcinst.ini.in" "${CMAKE_CURRENT_BINARY_DIR}/ydb-odbc-odbcinst.ini" @ONLY ) + +set(YDB_ODBC_DRIVER_TEMPLATE_PATH + "${YDB_ODBC_DRIVER_TEMPLATE_DIR}/odbcinst.ini") +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/debian") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/packaging/postinst.in" + "${CMAKE_CURRENT_BINARY_DIR}/debian/postinst" + @ONLY +) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/packaging/prerm.in" + "${CMAKE_CURRENT_BINARY_DIR}/debian/prerm" + @ONLY +) +set(YDB_ODBC_DEBIAN_CONTROL_EXTRA + "${CMAKE_CURRENT_BINARY_DIR}/debian/postinst;${CMAKE_CURRENT_BINARY_DIR}/debian/prerm" + CACHE INTERNAL "Debian control scripts for the ydb-odbc package") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ydb-odbc-odbcinst.ini" - DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d" - RENAME ydb-odbc.ini + DESTINATION "${YDB_ODBC_INSTALL_DATADIR}" + RENAME odbcinst.ini + COMPONENT ydb-odbc ) install(TARGETS ydb-odbc - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION "${YDB_ODBC_INSTALL_LIBDIR}" + COMPONENT ydb-odbc ) -install(DIRECTORY include/ - DESTINATION include/ydb-odbc -) +if (YDB_SDK_EXAMPLES) + add_subdirectory(examples) +endif() -add_subdirectory(examples) -add_subdirectory(tests) +if (YDB_SDK_TESTS) + add_subdirectory(tests) +endif() diff --git a/odbc/README.md b/odbc/README.md index 13ad517fd6..56219dbea3 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -26,17 +26,22 @@ The shared library is produced as `build/odbc/libydb-odbc.so`. ```bash cmake --install build --prefix /usr/local +sudo odbcinst -i -d -f /usr/local/share/ydb-odbc/odbcinst.ini ``` -Installs `libydb-odbc` and registers the driver in -`${prefix}/etc/odbcinst.d/ydb-odbc.ini`. -`odbc.ini` is not installed — create your own DSN (see below). +This installs `libydb-odbc` and its unixODBC registration template. The +`ydb-odbc` Debian package runs `odbcinst` automatically during installation +and unregisters the driver when the package is removed. `odbc.ini` is not +installed or modified — create your own DSN (see below). ## Configuration For `SQLConnect("YDB", ...)`, `isql -v YDB`, or `Driver=YDB`. -**`odbcinst.ini`** — driver registration (generated on build/install). Section `[YDB]` is the driver name used as `Driver=YDB` in connection strings and DSNs. `Driver` and `Setup` are the full path to `libydb-odbc.so`. Use `/etc/odbcinst.ini`, a file in `/etc/odbcinst.d/`, or set `ODBCSYSINI` to the directory that contains `odbcinst.ini`. +**`odbcinst.ini`** — driver registration template (generated on build/install). +Section `[YDB]` is the driver name used as `Driver=YDB` in connection strings +and DSNs. `Driver` and `Setup` are the full path to `libydb-odbc.so`. Register +the template with `odbcinst -i -d -f`; the Debian package does this for you. ```ini [YDB] diff --git a/odbc/packaging/postinst.in b/odbc/packaging/postinst.in new file mode 100644 index 0000000000..902817b642 --- /dev/null +++ b/odbc/packaging/postinst.in @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +case "${1:-}" in + configure|abort-upgrade|abort-remove|abort-deconfigure) + odbcinst -i -d -f "@YDB_ODBC_DRIVER_TEMPLATE_PATH@" + ;; +esac + +exit 0 diff --git a/odbc/packaging/prerm.in b/odbc/packaging/prerm.in new file mode 100644 index 0000000000..a91d015be1 --- /dev/null +++ b/odbc/packaging/prerm.in @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +case "${1:-}" in + remove|upgrade|deconfigure) + if odbcinst -q -d -n YDB 2>/dev/null \ + | grep -Fx "Driver=@YDB_ODBC_DRIVER_PATH@" >/dev/null; then + odbcinst -u -d -n YDB + fi + ;; +esac + +exit 0 diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 75f26c2bfc..ec0564688d 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -194,6 +194,18 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, }); } +SQLRETURN SQL_API SQLExecDirectW(SQLHSTMT statementHandle, + SQLWCHAR* statementText, + SQLINTEGER textLength) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + auto ret = stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); + if (ret != SQL_SUCCESS) { + return ret; + } + return stmt->Execute(); + }); +} + SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, SQLCHAR* statementText, SQLINTEGER textLength) { @@ -202,6 +214,14 @@ SQLRETURN SQL_API SQLPrepare(SQLHSTMT statementHandle, }); } +SQLRETURN SQL_API SQLPrepareW(SQLHSTMT statementHandle, + SQLWCHAR* statementText, + SQLINTEGER textLength) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); + }); +} + SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { return stmt->Execute(); diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp index f3d06c81d5..63d1da10cc 100644 --- a/odbc/src/utils/util.cpp +++ b/odbc/src/utils/util.cpp @@ -1,6 +1,7 @@ #include "util.h" #include +#include namespace NYdb::NOdbc { @@ -17,7 +18,7 @@ void TrimInPlace(std::string& value) { } // namespace -std::string GetString(SQLCHAR* str, SQLSMALLINT length) { +std::string GetString(SQLCHAR* str, SQLINTEGER length) { if (!str) { return {}; } @@ -27,7 +28,63 @@ std::string GetString(SQLCHAR* str, SQLSMALLINT length) { if (length <= 0) { return {}; } - return std::string(reinterpret_cast(str), length); + size_t size = static_cast(length); + if (str[size - 1] == 0) { + --size; + } + return std::string(reinterpret_cast(str), size); +} + +std::string GetString(SQLWCHAR* str, SQLINTEGER length) { + if (!str) { + return {}; + } + + size_t size = 0; + if (length == SQL_NTS) { + while (str[size] != 0) { + ++size; + } + } else if (length > 0) { + size = static_cast(length); + if (str[size - 1] == 0) { + --size; + } + } else { + return {}; + } + + std::string result; + result.reserve(size); + for (size_t i = 0; i < size; ++i) { + uint32_t codePoint = str[i]; + if (codePoint >= 0xd800 && codePoint <= 0xdbff) { + if (i + 1 < size && str[i + 1] >= 0xdc00 && str[i + 1] <= 0xdfff) { + codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (str[++i] - 0xdc00); + } else { + codePoint = 0xfffd; + } + } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) { + codePoint = 0xfffd; + } + + if (codePoint <= 0x7f) { + result.push_back(static_cast(codePoint)); + } else if (codePoint <= 0x7ff) { + result.push_back(static_cast(0xc0 | (codePoint >> 6))); + result.push_back(static_cast(0x80 | (codePoint & 0x3f))); + } else if (codePoint <= 0xffff) { + result.push_back(static_cast(0xe0 | (codePoint >> 12))); + result.push_back(static_cast(0x80 | ((codePoint >> 6) & 0x3f))); + result.push_back(static_cast(0x80 | (codePoint & 0x3f))); + } else { + result.push_back(static_cast(0xf0 | (codePoint >> 18))); + result.push_back(static_cast(0x80 | ((codePoint >> 12) & 0x3f))); + result.push_back(static_cast(0x80 | ((codePoint >> 6) & 0x3f))); + result.push_back(static_cast(0x80 | (codePoint & 0x3f))); + } + } + return result; } bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen) { diff --git a/odbc/src/utils/util.h b/odbc/src/utils/util.h index 942b20293e..9914ff9bac 100644 --- a/odbc/src/utils/util.h +++ b/odbc/src/utils/util.h @@ -10,7 +10,9 @@ namespace NYdb::NOdbc { -std::string GetString(SQLCHAR* str, SQLSMALLINT length); +std::string GetString(SQLCHAR* str, SQLINTEGER length); + +std::string GetString(SQLWCHAR* str, SQLINTEGER length); bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen); diff --git a/odbc/tests/unit/conn_string_ut.cpp b/odbc/tests/unit/conn_string_ut.cpp index 48bb5f7694..4ed8cec370 100644 --- a/odbc/tests/unit/conn_string_ut.cpp +++ b/odbc/tests/unit/conn_string_ut.cpp @@ -20,3 +20,23 @@ TEST(ConnString, TrimsWhitespace) { ASSERT_EQ(params.at("Database"), "/local"); ASSERT_EQ(params.at("Server"), "host"); } + +TEST(OdbcString, ConvertsUtf16ToUtf8) { + SQLWCHAR text[] = {'Y', 'D', 'B', ' ', 0x041f, 0x0440, 0x0438, 0x0432, 0x0435, 0x0442, 0}; + EXPECT_EQ(NYdb::NOdbc::GetString(text, SQL_NTS), "YDB \xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82"); +} + +TEST(OdbcString, ConvertsUtf16SurrogatePairToUtf8) { + SQLWCHAR text[] = {0xd83d, 0xde80, 0}; + EXPECT_EQ(NYdb::NOdbc::GetString(text, SQL_NTS), "\xf0\x9f\x9a\x80"); +} + +TEST(OdbcString, IgnoresAnsiTerminatorIncludedInExplicitLength) { + SQLCHAR text[] = {'/', 'l', 'o', 'c', 'a', 'l', 0}; + EXPECT_EQ(NYdb::NOdbc::GetString(text, 7), "/local"); +} + +TEST(OdbcString, IgnoresUtf16TerminatorIncludedInExplicitLength) { + SQLWCHAR text[] = {'S', 'E', 'L', 'E', 'C', 'T', ' ', '4', '2', 0}; + EXPECT_EQ(NYdb::NOdbc::GetString(text, 10), "SELECT 42"); +} diff --git a/scripts/build_cpack_deb_packages.sh b/scripts/build_cpack_deb_packages.sh index 91f0b8941e..b5663cedb4 100755 --- a/scripts/build_cpack_deb_packages.sh +++ b/scripts/build_cpack_deb_packages.sh @@ -23,6 +23,8 @@ if [ "${YDB_DEB_INSTALL_DEPS:-1}" = "1" ]; then build-essential \ ccache \ cmake \ + dpkg-dev \ + file \ pkg-config \ git \ libidn11-dev \ @@ -46,7 +48,9 @@ if [ "${YDB_DEB_INSTALL_DEPS:-1}" = "1" ]; then python3 \ python3-six \ ragel \ - yasm + yasm \ + odbcinst \ + unixodbc-dev fi touch_existing_sources() { @@ -107,6 +111,7 @@ touch_existing_sources \ include \ library \ plugins \ + odbc \ scripts/build_cpack_deb_packages.sh \ scripts/generate-debian-directory.sh \ src \ @@ -121,8 +126,11 @@ cmake -S . -B build-deb \ -DYDB_SDK_TESTS=OFF \ -DYDB_SDK_ENABLE_OTEL_METRICS=ON \ -DYDB_SDK_ENABLE_OTEL_TRACE=ON \ + -DYDB_SDK_ODBC=ON \ -DBUILD_SHARED_LIBS=OFF \ -DYDB_SDK_USE_SYSTEM_GOOGLEAPIS=ON \ + -DYDB_ODBC_INSTALL_LIBDIR="/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)" \ + -DYDB_ODBC_INSTALL_DATADIR=/usr/share/ydb-odbc \ -DCMAKE_INSTALL_PREFIX=/usr/share/yandex \ -DCMAKE_PREFIX_PATH="/usr/share/yandex" \ "${CMAKE_COMPILER_LAUNCHER_ARGS[@]}" diff --git a/scripts/test_deb_packages.sh b/scripts/test_deb_packages.sh index 194c6090d8..2c1e434194 100755 --- a/scripts/test_deb_packages.sh +++ b/scripts/test_deb_packages.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -euo pipefail if [ "$#" -ne 1 ]; then echo "Usage: $0 " @@ -10,17 +10,47 @@ DEB_DIR=$(realpath "$1") SCRIPT_DIR=$(dirname "$(realpath "$0")") SOURCE_DIR=$(realpath "$SCRIPT_DIR/..") TEST_DIR=$(realpath "$SCRIPT_DIR/../tests/deb_package") +YDB_TEST_IMAGE="${YDB_TEST_IMAGE:-ydbplatform/local-ydb:25.2.1}" +YDB_TEST_CONTAINER="ydb-odbc-package-test-$$" + +cleanup() { + docker rm -f "$YDB_TEST_CONTAINER" >/dev/null 2>&1 || true +} +trap cleanup EXIT echo "Building test Docker image..." docker build -t ydb-cpp-sdk-deb-test "$TEST_DIR" +echo "Starting local YDB ${YDB_TEST_IMAGE}..." +docker run -d --name "$YDB_TEST_CONTAINER" --network host \ + -e GRPC_TLS_PORT=2135 \ + -e GRPC_PORT=2136 \ + -e MON_PORT=8765 \ + -e YDB_DEFAULT_LOG_LEVEL=NOTICE \ + -e YDB_USE_IN_MEMORY_PDISKS=true \ + "$YDB_TEST_IMAGE" >/dev/null + +for _ in $(seq 1 60); do + if docker exec "$YDB_TEST_CONTAINER" /bin/sh -c \ + "/ydb -e grpc://localhost:2136 -d /local scheme ls" >/dev/null 2>&1; then + break + fi + sleep 2 +done +if ! docker exec "$YDB_TEST_CONTAINER" /bin/sh -c \ + "/ydb -e grpc://localhost:2136 -d /local scheme ls" >/dev/null 2>&1; then + docker logs "$YDB_TEST_CONTAINER" || true + echo "Local YDB did not become ready" >&2 + exit 1 +fi + echo "Running test container..." -docker run --rm \ +docker run --rm --network host \ -v "$DEB_DIR:/deb_packages:ro" \ -v "$SOURCE_DIR:/source:ro" \ ydb-cpp-sdk-deb-test \ bash -c ' -set -e +set -euo pipefail apt-get update if ! compgen -G "/deb_packages/yandex-googleapis-api-common-protos*.deb" > /dev/null; then @@ -34,6 +64,116 @@ else dpkg -i /deb_packages/yandex-googleapis-api-common-protos*.deb fi +odbc_packages=(/deb_packages/ydb-odbc_*.deb) +if [ "${#odbc_packages[@]}" -ne 1 ] || [ ! -f "${odbc_packages[0]}" ]; then + echo "Expected exactly one ydb-odbc package, found: ${odbc_packages[*]}" >&2 + exit 1 +fi +odbc_deb="${odbc_packages[0]}" +sdk_version="$(sed -nE '\''s/.*YDB_SDK_VERSION = "([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p'\'' /source/src/version.h)" +package_version="$(dpkg-deb -f "$odbc_deb" Version)" +package_name="$(dpkg-deb -f "$odbc_deb" Package)" +package_arch="$(dpkg-deb -f "$odbc_deb" Architecture)" +package_depends="$(dpkg-deb -f "$odbc_deb" Depends)" +host_arch="$(dpkg --print-architecture)" +multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)" +driver_path="/usr/lib/${multiarch}/libydb-odbc.so" +driver_template="/usr/share/ydb-odbc/odbcinst.ini" + +test "$package_name" = ydb-odbc +test "$package_version" = "$sdk_version" +test "$package_arch" = "$host_arch" +for dependency in odbcinst libodbcinst2 libc6; do + if ! grep -Eq "(^|, )${dependency}([ (]|,|$)" <<<"$package_depends"; then + echo "Missing ydb-odbc dependency ${dependency}: ${package_depends}" >&2 + exit 1 + fi +done + +cat >/tmp/unrelated-odbcinst.ini </etc/odbc.ini </root/.odbc.ini </tmp/odbc-ini.sha256 + +verify_ydb_registration() { + local registration + registration="$(odbcinst -q -d -n YDB)" + grep -Fx "Driver=${driver_path}" <<<"$registration" + grep -Fx "Setup=${driver_path}" <<<"$registration" + grep -Fx "UsageCount=1" <<<"$registration" +} + +run_odbc_consumers() { + local isql_output + isql_output="$(printf "SELECT 42 AS value;\n" | isql -b -v YDBPackageTest)" + echo "$isql_output" + grep -Eq "(^|[^0-9])42([^0-9]|$)" <<<"$isql_output" + /odbc_qt_test/build/ydb_odbc_qt_test \ + "Driver={YDB};Server=localhost:2136;Database=/local" +} + +rm -rf /tmp/ydb-odbc-old +dpkg-deb --raw-extract "$odbc_deb" /tmp/ydb-odbc-old +sed -i "s/^Version: .*/Version: ${sdk_version}~package-test1/" \ + /tmp/ydb-odbc-old/DEBIAN/control +dpkg-deb --build /tmp/ydb-odbc-old /tmp/ydb-odbc-old.deb + +apt-get install -y /tmp/ydb-odbc-old.deb +test -f "$driver_path" +test -f "$driver_template" +grep -Fx "Driver=${driver_path}" "$driver_template" +verify_ydb_registration +run_odbc_consumers + +apt-get install -y "$odbc_deb" +test "$(dpkg-query -W -f='\''${Version}'\'' ydb-odbc)" = "$sdk_version" +verify_ydb_registration +run_odbc_consumers +sha256sum --check /tmp/odbc-ini.sha256 + +apt-get remove -y ydb-odbc +test ! -e "$driver_path" +test ! -e "$driver_template" +if odbcinst -q -d -n YDB >/dev/null 2>&1; then + echo "YDB remained registered after package removal" >&2 + exit 1 +fi +odbcinst -q -d -n UnrelatedPackageTest >/dev/null +sha256sum --check /tmp/odbc-ini.sha256 + +apt-get install -y "$odbc_deb" +verify_ydb_registration +odbcinst -u -d -n YDB +cat >/tmp/replacement-ydb-odbcinst.ini < +#include +#include +#include +#include + +#include + +int main(int argc, char** argv) { + QCoreApplication app(argc, argv); + + if (argc != 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 2; + } + + if (!QSqlDatabase::isDriverAvailable("QODBC")) { + std::cerr << "Qt QODBC plugin is not available" << std::endl; + return 3; + } + + const QString connectionName = QStringLiteral("ydb-odbc-package-test"); + { + QSqlDatabase database = QSqlDatabase::addDatabase("QODBC", connectionName); + database.setDatabaseName(QString::fromLocal8Bit(argv[1])); + + if (!database.open()) { + std::cerr << "QODBC connection failed: " + << database.lastError().text().toStdString() << std::endl; + return 4; + } + + QSqlQuery query(database); + query.setForwardOnly(true); + if (!query.exec(QStringLiteral("SELECT 42 AS value"))) { + std::cerr << "QODBC query failed: " + << query.lastError().text().toStdString() << std::endl; + return 5; + } + if (!query.next()) { + std::cerr << "QODBC returned no row: " + << query.lastError().text().toStdString() << std::endl; + return 6; + } + const QVariant value = query.value(0); + if (value.toInt() != 42) { + std::cerr << "QODBC returned an unexpected SELECT result: type=" + << value.typeName() << ", value=" + << value.toString().toStdString() << std::endl; + return 7; + } + + database.close(); + } + QSqlDatabase::removeDatabase(connectionName); + + return 0; +}