From e0bd9c1f06f5b7123a986ba7de00a4b355ba7942 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Fri, 18 Apr 2025 13:21:32 +0000 Subject: [PATCH 01/55] started work --- CMakeLists.txt | 5 ++ odbc/CMakeLists.txt | 64 ++++++++++++++++ odbc/README.md | 105 ++++++++++++++++++++++++++ odbc/include/client/driver.h | 15 ++++ odbc/include/client/query.h | 17 +++++ odbc/include/ydb_odbc.h | 89 ++++++++++++++++++++++ odbc/odbc.ini | 9 +++ odbc/odbcinst.ini | 7 ++ odbc/src/client/driver.cpp | 25 ++++++ odbc/src/client/query.cpp | 56 ++++++++++++++ odbc/src/connection.c | 132 ++++++++++++++++++++++++++++++++ odbc/src/descriptor.c | 109 +++++++++++++++++++++++++++ odbc/src/driver.c | 142 +++++++++++++++++++++++++++++++++++ odbc/src/statement.c | 98 ++++++++++++++++++++++++ 14 files changed, 873 insertions(+) create mode 100644 odbc/CMakeLists.txt create mode 100644 odbc/README.md create mode 100644 odbc/include/client/driver.h create mode 100644 odbc/include/client/query.h create mode 100644 odbc/include/ydb_odbc.h create mode 100644 odbc/odbc.ini create mode 100644 odbc/odbcinst.ini create mode 100644 odbc/src/client/driver.cpp create mode 100644 odbc/src/client/query.cpp create mode 100644 odbc/src/connection.c create mode 100644 odbc/src/descriptor.c create mode 100644 odbc/src/driver.c create mode 100644 odbc/src/statement.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 72b66aad079..fc21d66c269 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ option(YDB_SDK_EXAMPLES "Build YDB C++ SDK examples" On) option(YDB_SDK_ENABLE_OTEL_METRICS "Build OpenTelemetry metrics plugin" Off) option(YDB_SDK_ENABLE_OTEL_TRACE "Build OpenTelemetry trace plugin" Off) option(YDB_CPP_SDK_SLO_USE_INSTALLED_SDK "Build only SLO workloads against an installed ydb-cpp-sdk package" Off) +option(YDB_SDK_ODBC "Build YDB ODBC driver" On) set(YDB_SDK_GOOGLE_COMMON_PROTOS_TARGET "" CACHE STRING "Name of cmake target preparing google common proto library") option(YDB_SDK_USE_RAPID_JSON "Search for rapid json library in system" ON) @@ -85,6 +86,10 @@ add_subdirectory(plugins) #_ydb_sdk_validate_public_headers() +if (YDB_SDK_ODBC) + add_subdirectory(odbc) +endif() + if (YDB_SDK_EXAMPLES) add_subdirectory(examples) endif() diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt new file mode 100644 index 00000000000..985782900ee --- /dev/null +++ b/odbc/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.14) +project(ydb-odbc VERSION 0.1.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Находим зависимости +find_package(ODBC REQUIRED) + +# Добавляем исходники +add_library(ydb-odbc SHARED + src/driver.c + src/connection.c + src/statement.c + src/descriptor.c + src/client/driver.cpp + src/client/query.cpp +) + +# Добавляем заголовочные файлы +target_include_directories(ydb-odbc + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${ODBC_INCLUDE_DIRS} + /usr/include + /usr/local/include +) + +# Линкуем с YDB SDK и ODBC +target_link_libraries(ydb-odbc + PUBLIC + YDB-CPP-SDK::Query + YDB-CPP-SDK::Table + YDB-CPP-SDK::Driver + ODBC::ODBC +) + +# Устанавливаем драйвер +install(TARGETS ydb-odbc + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +# Устанавливаем заголовочные файлы +install(DIRECTORY include/ + DESTINATION include/ydb-odbc +) + +# Добавляем тесты +# add_subdirectory(tests) + +# Правила установки +include(GNUInstallDirs) + +install(FILES + odbcinst.ini + DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d + RENAME ydb-odbc.ini +) + +install(FILES + odbc.ini + DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} +) diff --git a/odbc/README.md b/odbc/README.md new file mode 100644 index 00000000000..4d502aaad73 --- /dev/null +++ b/odbc/README.md @@ -0,0 +1,105 @@ +# YDB ODBC Driver + +ODBC драйвер для YDB. + +## Требования + +- CMake 3.10 или выше +- Компилятор C/C++ с поддержкой C11 и C++20 +- YDB C++ SDK +- unixODBC (для Linux/macOS) + +## Сборка + +```bash +mkdir build && cd build +cmake .. +make +``` + +## Установка + +```bash +sudo make install +``` + +Это установит: +- Библиотеку драйвера в `/usr/local/lib/` +- Конфигурацию драйвера в `/etc/odbcinst.d/` +- Конфигурацию источников данных в `/etc/odbc.ini` + +## Настройка + +1. Убедитесь, что драйвер зарегистрирован: +```bash +odbcinst -q -d +``` + +2. Проверьте доступные источники данных: +```bash +odbcinst -q -s +``` + +3. Отредактируйте `/etc/odbc.ini` для настройки подключения: +```ini +[YDB] +Driver=YDB +Description=YDB Database Connection +Server=grpc://your-server:2136 +Database=your-database +AuthMode=none # или token для аутентификации по токену +``` + +## Использование + +Пример подключения через isql: +```bash +isql -v YDB +``` + +Пример использования в C: +```c +SQLHENV env; +SQLHDBC dbc; +SQLHSTMT stmt; + +// Инициализация окружения +SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); +SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + +// Подключение +SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); +SQLConnect(dbc, (SQLCHAR*)"YDB", SQL_NTS, + (SQLCHAR*)"", SQL_NTS, + (SQLCHAR*)"", SQL_NTS); + +// Выполнение запроса +SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); +SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM mytable", SQL_NTS); + +// Очистка +SQLFreeHandle(SQL_HANDLE_STMT, stmt); +SQLDisconnect(dbc); +SQLFreeHandle(SQL_HANDLE_DBC, dbc); +SQLFreeHandle(SQL_HANDLE_ENV, env); +``` + +## Поддерживаемые функции + +- SQLAllocHandle +- SQLConnect +- SQLDisconnect +- SQLExecDirect +- SQLFetch +- SQLGetData +- SQLPrepare +- SQLExecute +- SQLCloseCursor +- SQLFreeHandle +- SQLGetInfo +- SQLGetDescField +- SQLSetDescField + +## Лицензия + +Apache License 2.0 \ No newline at end of file diff --git a/odbc/include/client/driver.h b/odbc/include/client/driver.h new file mode 100644 index 00000000000..6a95f9958c8 --- /dev/null +++ b/odbc/include/client/driver.h @@ -0,0 +1,15 @@ +#pragma once + +#include // для size_t + +#ifdef __cplusplus +extern "C" { +#endif + +// Функции для работы с YDB через C++ SDK +void* YDB_CreateDriver(const char* endpoint, const char* user, const char* password); +void YDB_DestroyDriver(void* driver); + +#ifdef __cplusplus +} +#endif diff --git a/odbc/include/client/query.h b/odbc/include/client/query.h new file mode 100644 index 00000000000..ac6802dc6c8 --- /dev/null +++ b/odbc/include/client/query.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void* YDB_CreateQueryClient(void* driver); +void YDB_DestroyQueryClient(void* query_client); + +int YDB_ExecuteQuery(void* query_client, const char* query, void** result); +void YDB_FreeExecuteQueryResult(void* result); + +#ifdef __cplusplus +} +#endif diff --git a/odbc/include/ydb_odbc.h b/odbc/include/ydb_odbc.h new file mode 100644 index 00000000000..2415f80f55e --- /dev/null +++ b/odbc/include/ydb_odbc.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Структура для хранения информации о драйвере +typedef struct { + char name[256]; + char version[64]; + char description[1024]; +} YDB_DRIVER_INFO; + +// Структура для хранения состояния соединения +typedef struct { + void* ydb_driver; + void* query_client; + int connected; +} YDB_CONNECTION; + +// Структура для хранения состояния оператора +typedef struct { + YDB_CONNECTION* connection; + void* query_client; + void* result; + size_t current_row; +} YDB_STATEMENT; + +// Структура для хранения дескриптора +typedef struct { + void** descriptors; + size_t descriptors_size; +} YDB_DESCRIPTOR; + +// Функции драйвера +SQLRETURN YDB_SQLGetInfo(SQLSMALLINT InfoType, SQLPOINTER InfoValue, + SQLSMALLINT BufferLength, SQLSMALLINT* StringLength); + +SQLRETURN YDB_SQLConnect(SQLHDBC ConnectionHandle, SQLCHAR* ServerName, + SQLSMALLINT NameLength1, SQLCHAR* UserName, + SQLSMALLINT NameLength2, SQLCHAR* Authentication, + SQLSMALLINT NameLength3); + +SQLRETURN YDB_SQLDriverConnect(SQLHDBC ConnectionHandle, SQLHWND WindowHandle, + SQLCHAR* InConnectionString, SQLSMALLINT StringLength1, + SQLCHAR* OutConnectionString, SQLSMALLINT BufferLength, + SQLSMALLINT* StringLength2, SQLUSMALLINT DriverCompletion); + +// Функции соединения +SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle); + +SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, + SQLPOINTER InfoValue, SQLSMALLINT BufferLength, + SQLSMALLINT* StringLength); + +SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, + SQLHANDLE* OutputHandle); + +SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle); + +// Функции оператора +SQLRETURN YDB_SQLExecDirect(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength); + +SQLRETURN YDB_SQLPrepare(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength); + +SQLRETURN YDB_SQLExecute(SQLHSTMT StatementHandle); + +SQLRETURN YDB_SQLFetch(SQLHSTMT StatementHandle); + +SQLRETURN YDB_SQLCloseCursor(SQLHSTMT StatementHandle); + +// Функции дескриптора +SQLRETURN YDB_SQLGetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength, SQLINTEGER* StringLength); + +SQLRETURN YDB_SQLSetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength); + +#ifdef __cplusplus +} +#endif diff --git a/odbc/odbc.ini b/odbc/odbc.ini new file mode 100644 index 00000000000..6335b3ee389 --- /dev/null +++ b/odbc/odbc.ini @@ -0,0 +1,9 @@ +[ODBC Data Sources] +YDB=YDB ODBC Driver + +[YDB] +Driver=YDB +Description=YDB Database Connection +Server=grpc://localhost:2136 +Database=local +AuthMode=none \ No newline at end of file diff --git a/odbc/odbcinst.ini b/odbc/odbcinst.ini new file mode 100644 index 00000000000..fade7b6fb92 --- /dev/null +++ b/odbc/odbcinst.ini @@ -0,0 +1,7 @@ +[YDB] +Description=YDB ODBC Driver +Driver=/usr/local/lib/libydb-odbc.so +Setup=/usr/local/lib/libydb-odbc.so +Threading=2 +FileUsage=1 +UsageCount=1 \ No newline at end of file diff --git a/odbc/src/client/driver.cpp b/odbc/src/client/driver.cpp new file mode 100644 index 00000000000..faa87471e43 --- /dev/null +++ b/odbc/src/client/driver.cpp @@ -0,0 +1,25 @@ +#include "client/driver.h" + +#include + +extern "C" { + +void* YDB_CreateDriver(const char* endpoint, const char* user, const char* password) { + try { + auto config = NYdb::TDriverConfig().SetEndpoint(std::string(endpoint, strlen(endpoint))); + + auto* driver = new NYdb::TDriver(config); + return static_cast(driver); + } catch (...) { + return nullptr; + } +} + +void YDB_DestroyDriver(void* driver) { + if (driver) { + auto* ydb_driver = static_cast(driver); + delete ydb_driver; + } +} + +} diff --git a/odbc/src/client/query.cpp b/odbc/src/client/query.cpp new file mode 100644 index 00000000000..5da339d19bd --- /dev/null +++ b/odbc/src/client/query.cpp @@ -0,0 +1,56 @@ +#include "client/query.h" + +#include + +#include + +extern "C" { + +void* YDB_CreateQueryClient(void* driver) { + if (!driver) return nullptr; + + try { + auto* ydb_driver = static_cast(driver); + auto* query_client = new NYdb::NQuery::TQueryClient(*ydb_driver); + return static_cast(query_client); + } catch (...) { + return nullptr; + } +} + +void YDB_DestroyQueryClient(void* query_client) { + if (query_client) { + auto* client = static_cast(query_client); + delete client; + } +} + +int YDB_ExecuteQuery(void* query_client, const char* query, void** result) { + if (!query_client || !query || !result) { + return 0; + } + + try { + auto* client = static_cast(query_client); + auto executeResult = client->ExecuteQuery(std::string(query, strlen(query)), NYdb::NQuery::TTxControl::NoTx()).GetValueSync(); + + if (!executeResult.IsSuccess()) { + return 0; + } + + *result = reinterpret_cast(new NYdb::NQuery::TExecuteQueryResult(executeResult)); + + return 1; + } catch (...) { + return 0; + } +} + +void YDB_FreeExecuteQueryResult(void* result) { + if (result) { + auto* executeResult = reinterpret_cast(result); + delete executeResult; + } +} + +} // extern "C" diff --git a/odbc/src/connection.c b/odbc/src/connection.c new file mode 100644 index 00000000000..d95dc8ade72 --- /dev/null +++ b/odbc/src/connection.c @@ -0,0 +1,132 @@ +#include "ydb_odbc.h" +#include "client/driver.h" +#include "client/query.h" + +#include +#include + +SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle) { + YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; + if (!conn) { + return SQL_ERROR; + } + + if (conn->connected) { + if (conn->query_client) { + YDB_DestroyQueryClient(conn->query_client); + conn->query_client = NULL; + } + + if (conn->ydb_driver) { + YDB_DestroyDriver(conn->ydb_driver); + conn->ydb_driver = NULL; + } + + conn->connected = 0; + } + + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, + SQLPOINTER InfoValue, SQLSMALLINT BufferLength, + SQLSMALLINT* StringLength) { + YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; + if (!conn || !conn->connected) { + return SQL_ERROR; + } + + switch (InfoType) { + case SQL_DATABASE_NAME: + if (InfoValue && BufferLength > 0) { + const char* dbName = "YDB"; + strncpy((char*)InfoValue, dbName, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(dbName); + } + return SQL_SUCCESS; + } + break; + + case SQL_SERVER_NAME: + if (InfoValue && BufferLength > 0) { + const char* serverName = "Yandex Database"; + strncpy((char*)InfoValue, serverName, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(serverName); + } + return SQL_SUCCESS; + } + break; + } + + return SQL_ERROR; +} + +SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, + SQLHANDLE* OutputHandle) { + if (!OutputHandle) { + return SQL_ERROR; + } + + switch (HandleType) { + case SQL_HANDLE_DBC: + *OutputHandle = calloc(1, sizeof(YDB_CONNECTION)); + return SQL_SUCCESS; + + case SQL_HANDLE_STMT: + *OutputHandle = calloc(1, sizeof(YDB_STATEMENT)); + return SQL_SUCCESS; + + case SQL_HANDLE_DESC: + *OutputHandle = calloc(1, sizeof(YDB_DESCRIPTOR)); + return SQL_SUCCESS; + + default: + return SQL_ERROR; + } +} + +SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) { + if (!Handle) { + return SQL_ERROR; + } + + switch (HandleType) { + case SQL_HANDLE_DBC: + { + YDB_CONNECTION* conn = (YDB_CONNECTION*)Handle; + if (conn->connected) { + YDB_SQLDisconnect((SQLHDBC)conn); + } + free(conn); + } + return SQL_SUCCESS; + + case SQL_HANDLE_STMT: + { + YDB_STATEMENT* stmt = (YDB_STATEMENT*)Handle; + if (stmt->result) { + YDB_FreeExecuteQueryResult(stmt->result); + } + if (stmt->query_client) { + YDB_DestroyQueryClient(stmt->query_client); + } + free(stmt); + } + return SQL_SUCCESS; + + case SQL_HANDLE_DESC: + { + YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)Handle; + if (desc->descriptors) { + free(desc->descriptors); + } + free(desc); + } + return SQL_SUCCESS; + + default: + return SQL_ERROR; + } +} \ No newline at end of file diff --git a/odbc/src/descriptor.c b/odbc/src/descriptor.c new file mode 100644 index 00000000000..15380bf40aa --- /dev/null +++ b/odbc/src/descriptor.c @@ -0,0 +1,109 @@ +#include "ydb_odbc.h" +#include +#include + +// Структура для хранения поля дескриптора +typedef struct { + SQLSMALLINT field_identifier; + char value[1024]; +} YDB_DESCRIPTOR_FIELD; + +// Структура для хранения записи дескриптора +typedef struct { + YDB_DESCRIPTOR_FIELD* fields; + size_t fields_count; +} YDB_DESCRIPTOR_RECORD; + +SQLRETURN YDB_SQLGetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength, SQLINTEGER* StringLength) { + YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)DescriptorHandle; + if (!desc || !desc->descriptors || RecNumber < 1 || RecNumber > desc->descriptors_size) { + return SQL_ERROR; + } + + YDB_DESCRIPTOR_RECORD* record = (YDB_DESCRIPTOR_RECORD*)desc->descriptors[RecNumber - 1]; + if (!record) { + return SQL_ERROR; + } + + for (size_t i = 0; i < record->fields_count; i++) { + if (record->fields[i].field_identifier == FieldIdentifier) { + if (Value && BufferLength > 0) { + strncpy((char*)Value, record->fields[i].value, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(record->fields[i].value); + } + return SQL_SUCCESS; + } + break; + } + } + + return SQL_ERROR; +} + +SQLRETURN YDB_SQLSetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, + SQLSMALLINT FieldIdentifier, SQLPOINTER Value, + SQLINTEGER BufferLength) { + YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)DescriptorHandle; + if (!desc || RecNumber < 1) { + return SQL_ERROR; + } + + // Увеличиваем размер массива дескрипторов, если нужно + if (RecNumber > desc->descriptors_size) { + void** new_descriptors = realloc(desc->descriptors, RecNumber * sizeof(void*)); + if (!new_descriptors) { + return SQL_ERROR; + } + + // Инициализируем новые записи + for (size_t i = desc->descriptors_size; i < RecNumber; i++) { + YDB_DESCRIPTOR_RECORD* record = calloc(1, sizeof(YDB_DESCRIPTOR_RECORD)); + if (!record) { + // Освобождаем память в случае ошибки + for (size_t j = desc->descriptors_size; j < i; j++) { + free(new_descriptors[j]); + } + free(new_descriptors); + return SQL_ERROR; + } + new_descriptors[i] = record; + } + + desc->descriptors = new_descriptors; + desc->descriptors_size = RecNumber; + } + + YDB_DESCRIPTOR_RECORD* record = (YDB_DESCRIPTOR_RECORD*)desc->descriptors[RecNumber - 1]; + if (!record) { + record = calloc(1, sizeof(YDB_DESCRIPTOR_RECORD)); + if (!record) { + return SQL_ERROR; + } + desc->descriptors[RecNumber - 1] = record; + } + + // Проверяем, существует ли уже поле с таким идентификатором + for (size_t i = 0; i < record->fields_count; i++) { + if (record->fields[i].field_identifier == FieldIdentifier) { + // Обновляем значение + strncpy(record->fields[i].value, (char*)Value, sizeof(record->fields[i].value) - 1); + return SQL_SUCCESS; + } + } + + // Добавляем новое поле + YDB_DESCRIPTOR_FIELD* new_fields = realloc(record->fields, (record->fields_count + 1) * sizeof(YDB_DESCRIPTOR_FIELD)); + if (!new_fields) { + return SQL_ERROR; + } + + record->fields = new_fields; + record->fields[record->fields_count].field_identifier = FieldIdentifier; + strncpy(record->fields[record->fields_count].value, (char*)Value, sizeof(record->fields[record->fields_count].value) - 1); + record->fields_count++; + + return SQL_SUCCESS; +} \ No newline at end of file diff --git a/odbc/src/driver.c b/odbc/src/driver.c new file mode 100644 index 00000000000..5fc19f7294b --- /dev/null +++ b/odbc/src/driver.c @@ -0,0 +1,142 @@ +#include "ydb_odbc.h" +#include +#include + +// Глобальные переменные для хранения состояния +static YDB_DRIVER_INFO driver_info = { + .name = "YDB ODBC Driver", + .version = "1.0.0", + .description = "ODBC driver for Yandex Database" +}; + +SQLRETURN YDB_SQLGetInfo(SQLSMALLINT InfoType, SQLPOINTER InfoValue, + SQLSMALLINT BufferLength, SQLSMALLINT* StringLength) { + switch (InfoType) { + case SQL_DRIVER_NAME: + if (InfoValue && BufferLength > 0) { + strncpy(InfoValue, driver_info.name, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(driver_info.name); + } + return SQL_SUCCESS; + } + break; + + case SQL_DRIVER_VER: + if (InfoValue && BufferLength > 0) { + strncpy(InfoValue, driver_info.version, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(driver_info.version); + } + return SQL_SUCCESS; + } + break; + } + + return SQL_ERROR; +} + +SQLRETURN YDB_SQLConnect(SQLHDBC ConnectionHandle, SQLCHAR* ServerName, + SQLSMALLINT NameLength1, SQLCHAR* UserName, + SQLSMALLINT NameLength2, SQLCHAR* Authentication, + SQLSMALLINT NameLength3) { + YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; + if (!conn) { + return SQL_ERROR; + } + + // TODO: Реализовать подключение к YDB через C++ SDK + // Здесь нужно будет использовать C++ код через extern "C" функции + + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLDriverConnect(SQLHDBC ConnectionHandle, SQLHWND WindowHandle, + SQLCHAR* InConnectionString, SQLSMALLINT StringLength1, + SQLCHAR* OutConnectionString, SQLSMALLINT BufferLength, + SQLSMALLINT* StringLength2, SQLUSMALLINT DriverCompletion) { + // TODO: Реализовать парсинг строки подключения + return SQL_ERROR; +} + +SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle) { + YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; + if (!conn) { + return SQL_ERROR; + } + + // TODO: Реализовать отключение от YDB + + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, + SQLPOINTER InfoValue, SQLSMALLINT BufferLength, + SQLSMALLINT* StringLength) { + YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; + if (!conn || !conn->connected) { + return SQL_ERROR; + } + + switch (InfoType) { + case SQL_DATABASE_NAME: + if (InfoValue && BufferLength > 0) { + const char* dbName = "YDB"; + strncpy(InfoValue, dbName, BufferLength - 1); + if (StringLength) { + *StringLength = strlen(dbName); + } + return SQL_SUCCESS; + } + break; + } + + return SQL_ERROR; +} + +SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, + SQLHANDLE* OutputHandle) { + if (!OutputHandle) { + return SQL_ERROR; + } + + switch (HandleType) { + case SQL_HANDLE_DBC: + *OutputHandle = calloc(1, sizeof(YDB_CONNECTION)); + return SQL_SUCCESS; + + case SQL_HANDLE_STMT: + *OutputHandle = calloc(1, sizeof(YDB_STATEMENT)); + return SQL_SUCCESS; + + case SQL_HANDLE_DESC: + *OutputHandle = calloc(1, sizeof(YDB_DESCRIPTOR)); + return SQL_SUCCESS; + + default: + return SQL_ERROR; + } +} + +SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) { + if (!Handle) { + return SQL_ERROR; + } + + switch (HandleType) { + case SQL_HANDLE_DBC: + free(Handle); + return SQL_SUCCESS; + + case SQL_HANDLE_STMT: + free(Handle); + return SQL_SUCCESS; + + case SQL_HANDLE_DESC: + free(Handle); + return SQL_SUCCESS; + + default: + return SQL_ERROR; + } +} \ No newline at end of file diff --git a/odbc/src/statement.c b/odbc/src/statement.c new file mode 100644 index 00000000000..b9a7a5ffcd7 --- /dev/null +++ b/odbc/src/statement.c @@ -0,0 +1,98 @@ +#include "ydb_odbc.h" +#include "client/query.h" +#include +#include + +SQLRETURN YDB_SQLExecDirect(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength) { + YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; + if (!stmt || !stmt->connection || !stmt->connection->connected || !StatementText) { + return SQL_ERROR; + } + + // Если текст запроса не указан, используем длину строки + if (TextLength == SQL_NTS) { + TextLength = strlen((char*)StatementText); + } + + // Создаем клиент запросов, если его еще нет + if (!stmt->query_client) { + stmt->query_client = YDB_CreateQueryClient(stmt->connection->ydb_driver); + if (!stmt->query_client) { + return SQL_ERROR; + } + } + + // Освобождаем предыдущий результат, если он есть + if (stmt->result) { + YDB_FreeExecuteQueryResult(stmt->result); + stmt->result = NULL; + } + + // Выполняем запрос + if (!YDB_ExecuteQuery(stmt->query_client, (char*)StatementText, &stmt->result)) { + return SQL_ERROR; + } + + stmt->current_row = 0; + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLPrepare(SQLHSTMT StatementHandle, SQLCHAR* StatementText, + SQLINTEGER TextLength) { + // YDB не требует предварительной подготовки запросов + // Просто сохраняем текст запроса для последующего выполнения + YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; + if (!stmt || !StatementText) { + return SQL_ERROR; + } + + // Если текст запроса не указан, используем длину строки + if (TextLength == SQL_NTS) { + TextLength = strlen((char*)StatementText); + } + + // TODO: Сохранить текст запроса для последующего выполнения + + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLExecute(SQLHSTMT StatementHandle) { + // В YDB все запросы выполняются сразу + // Эта функция просто вызывает SQLExecDirect с сохраненным текстом запроса + YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; + if (!stmt) { + return SQL_ERROR; + } + + // TODO: Выполнить сохраненный запрос + + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLFetch(SQLHSTMT StatementHandle) { + YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; + if (!stmt || !stmt->result) { + return SQL_ERROR; + } + + // TODO: Преобразовать данные текущей строки в формат ODBC + + stmt->current_row++; + return SQL_SUCCESS; +} + +SQLRETURN YDB_SQLCloseCursor(SQLHSTMT StatementHandle) { + YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; + if (!stmt) { + return SQL_ERROR; + } + + if (stmt->result) { + YDB_FreeExecuteQueryResult(stmt->result); + stmt->result = NULL; + } + + stmt->current_row = 0; + return SQL_SUCCESS; +} \ No newline at end of file From 2613dac14a620073b4da1964176e0bba46f80680 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Sat, 10 May 2025 18:12:57 +0000 Subject: [PATCH 02/55] C API --- CMakeLists.txt | 5 +- c_api/CMakeLists.txt | 18 ++ c_api/README.md | 51 ++++ c_api/include/ydb-cpp-sdk/c_api/driver.h | 47 +++ c_api/include/ydb-cpp-sdk/c_api/query.h | 39 +++ c_api/include/ydb-cpp-sdk/c_api/result.h | 32 ++ c_api/include/ydb-cpp-sdk/c_api/value.h | 75 +++++ c_api/src/driver.cpp | 172 +++++++++++ c_api/src/impl/driver_impl.h | 22 ++ c_api/src/impl/result_impl.h | 14 + c_api/src/impl/value_impl.h | 15 + c_api/src/query.cpp | 140 +++++++++ c_api/src/result.cpp | 95 ++++++ c_api/src/value.cpp | 276 ++++++++++++++++++ cmake/common.cmake | 8 +- cmake/ydb-cpp-sdk-config.cmake.in | 2 +- examples/CMakeLists.txt | 1 + examples/basic_example/CMakeLists.txt | 6 +- examples/bulk_upsert_simple/CMakeLists.txt | 2 +- examples/c_api/CMakeLists.txt | 5 + examples/c_api/main.c | 37 +++ examples/pagination/CMakeLists.txt | 2 +- examples/secondary_index/CMakeLists.txt | 2 +- .../secondary_index_builtin/CMakeLists.txt | 2 +- .../topic_reader/eventloop/CMakeLists.txt | 2 +- examples/topic_reader/simple/CMakeLists.txt | 2 +- .../topic_reader/transaction/CMakeLists.txt | 2 +- examples/ttl/CMakeLists.txt | 2 +- examples/vector_index/CMakeLists.txt | 2 +- odbc/CMakeLists.txt | 6 +- odbc/include/client/driver.h | 15 - odbc/include/client/query.h | 17 -- odbc/src/client/driver.cpp | 25 -- odbc/src/client/query.cpp | 56 ---- .../integration/basic_example/CMakeLists.txt | 6 +- tests/integration/bulk_upsert/CMakeLists.txt | 2 +- .../integration/server_restart/CMakeLists.txt | 2 +- tests/unit/client/CMakeLists.txt | 4 +- 38 files changed, 1070 insertions(+), 141 deletions(-) create mode 100644 c_api/CMakeLists.txt create mode 100644 c_api/README.md create mode 100644 c_api/include/ydb-cpp-sdk/c_api/driver.h create mode 100644 c_api/include/ydb-cpp-sdk/c_api/query.h create mode 100644 c_api/include/ydb-cpp-sdk/c_api/result.h create mode 100644 c_api/include/ydb-cpp-sdk/c_api/value.h create mode 100644 c_api/src/driver.cpp create mode 100644 c_api/src/impl/driver_impl.h create mode 100644 c_api/src/impl/result_impl.h create mode 100644 c_api/src/impl/value_impl.h create mode 100644 c_api/src/query.cpp create mode 100644 c_api/src/result.cpp create mode 100644 c_api/src/value.cpp create mode 100644 examples/c_api/CMakeLists.txt create mode 100644 examples/c_api/main.c delete mode 100644 odbc/include/client/driver.h delete mode 100644 odbc/include/client/query.h delete mode 100644 odbc/src/client/driver.cpp delete mode 100644 odbc/src/client/query.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fc21d66c269..9cdfaccea56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ string(REGEX MATCH "YDB_SDK_VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ ${YDB_SD set(YDB_SDK_VERSION ${CMAKE_MATCH_1}) message(STATUS "YDB C++ SDK version: ${YDB_SDK_VERSION}") -project(YDB-CPP-SDK VERSION ${YDB_SDK_VERSION} LANGUAGES C CXX ASM) +project(ydb-cpp-sdk VERSION ${YDB_SDK_VERSION} LANGUAGES C CXX ASM) option(YDB_SDK_INSTALL "Install YDB C++ SDK" Off) option(YDB_SDK_TESTS "Build YDB C++ SDK tests" Off) @@ -83,6 +83,7 @@ add_subdirectory(include/ydb-cpp-sdk/client) add_subdirectory(src) add_subdirectory(util) add_subdirectory(plugins) +add_subdirectory(c_api) #_ydb_sdk_validate_public_headers() @@ -184,7 +185,7 @@ if (YDB_SDK_INSTALL) install(EXPORT ydb-cpp-sdk-core-targets FILE ydb-cpp-sdk-core-targets.cmake CONFIGURATIONS RELEASE - NAMESPACE YDB-CPP-SDK:: + NAMESPACE ydb-cpp-sdk:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ydb-cpp-sdk/release COMPONENT libydb-cpp ) diff --git a/c_api/CMakeLists.txt b/c_api/CMakeLists.txt new file mode 100644 index 00000000000..6f7a7295390 --- /dev/null +++ b/c_api/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(ydb-c-api STATIC + src/driver.cpp + src/query.cpp + src/result.cpp + src/value.cpp +) + +target_include_directories(ydb-c-api PUBLIC include) + +target_link_libraries(ydb-c-api + PRIVATE + yutil + ydb-cpp-sdk::Query + ydb-cpp-sdk::Table + ydb-cpp-sdk::Driver +) + +add_library(ydb-cpp-sdk::c-api ALIAS ydb-c-api) diff --git a/c_api/README.md b/c_api/README.md new file mode 100644 index 00000000000..b7f929c5538 --- /dev/null +++ b/c_api/README.md @@ -0,0 +1,51 @@ +Синхронный API Асинхронный API + +libpq: +```c +PGconn *conn = PQconnectdb("..."); +// Блокирует выполнение до завершения +``` + +libpq: +```c +PGconn *conn = PQconnectStart("..."); +do { + pollstatus = PQconnectPoll(conn); + // Ожидание событий +} while (pollstatus != PGRES_POLLING_OK); +``` + +MySQL: +
MYSQL *conn = mysql_init(NULL); +
mysql_real_connect(conn, ...); + +MySQL: +
status = mysql_real_connect_nonblocking(mysql, ...); +
while (status == NET_ASYNC_NOT_READY) { +
// Обработка других задач
+ status = mysql_real_connect_nonblocking(...);
+} + +Выполнение запросов + +Синхронный API Асинхронный API + +libpq:
PGresult *res = PQexec(conn, "SELECT ...");
Ожидает завершения выполнения + +libpq:
PQsendQuery(conn, "SELECT ...");
// Можно выполнять другую работу
while ((res = PQgetResult(conn)) != NULL) {
// Обработка результатов
} + +MySQL:
mysql_query(conn, "SELECT ...");
result = mysql_store_result(conn); + +MySQL:
status = mysql_real_query_nonblocking(mysql, "...");
// Проверка status и ожидание
status = mysql_store_result_nonblocking(mysql, &result); + +Обработка ошибок + +Синхронный API Асинхронный API + +libpq:
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "%s", PQerrorMessage(conn));
} + +libpq:
Такая же проверка, но в каждом шаге асинхронного процесса:
if (pollstatus == PGRES_POLLING_FAILED) {
fprintf(stderr, "%s", PQerrorMessage(conn));
} + +MySQL:
if (mysql_query(conn, query)) {
fprintf(stderr, "%s", mysql_error(conn));
} + +MySQL:
if (status == NET_ASYNC_ERROR) {
fprintf(stderr, "%s", mysql_error(mysql));
} \ No newline at end of file diff --git a/c_api/include/ydb-cpp-sdk/c_api/driver.h b/c_api/include/ydb-cpp-sdk/c_api/driver.h new file mode 100644 index 00000000000..557994675f0 --- /dev/null +++ b/c_api/include/ydb-cpp-sdk/c_api/driver.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct TYdbDriverConfigImpl TYdbDriverConfig; +typedef struct TYdbDriverImpl TYdbDriver; + +typedef enum { + YDB_DRIVER_CONFIG_OK, + YDB_DRIVER_CONFIG_INVALID, +} EYdbDriverConfigStatus; + +typedef enum { + YDB_DRIVER_OK, + YDB_DRIVER_ERROR, +} EYdbDriverStatus; + +// Создание и уничтожение конфигурации +TYdbDriverConfig* YdbCreateDriverConfig(const char* connectionString); +void YdbDestroyDriverConfig(TYdbDriverConfig* config); + +// Установка параметров конфигурации +TYdbDriverConfig* YdbSetEndpoint(TYdbDriverConfig* config, const char* endpoint); +TYdbDriverConfig* YdbSetDatabase(TYdbDriverConfig* config, const char* database); +TYdbDriverConfig* YdbSetAuthToken(TYdbDriverConfig* config, const char* token); +TYdbDriverConfig* YdbSetSecureConnection(TYdbDriverConfig* config, const char* cert); + +// Получение результата конфигурации +EYdbDriverConfigStatus YdbGetDriverConfigStatus(TYdbDriverConfig* config); +const char* YdbGetDriverConfigErrorMessage(TYdbDriverConfig* config); + +// Создание и уничтожение драйвера +TYdbDriver* YdbCreateDriver(const char* connectionString); +TYdbDriver* YdbCreateDriverFromConfig(TYdbDriverConfig* config); +void YdbDestroyDriver(TYdbDriver* driver); + +// Получение результата драйвера +EYdbDriverStatus YdbGetDriverStatus(TYdbDriver* driver); +const char* YdbGetDriverErrorMessage(TYdbDriver* driver); + +#ifdef __cplusplus +} +#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/query.h b/c_api/include/ydb-cpp-sdk/c_api/query.h new file mode 100644 index 00000000000..e3bd3918ac6 --- /dev/null +++ b/c_api/include/ydb-cpp-sdk/c_api/query.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "driver.h" +#include "result.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct TYdbQueryClientImpl TYdbQueryClient; +typedef struct TYdbQueryResultImpl TYdbQueryResult; + +typedef enum { + YDB_QUERY_CLIENT_OK, + YDB_QUERY_CLIENT_ERROR, +} EYdbQueryClientError; + +typedef enum { + YDB_QUERY_RESULT_OK, + YDB_QUERY_RESULT_ERROR, +} EYdbQueryResultError; + +// Создание и уничтожение клиента запросов +TYdbQueryClient* YdbCreateQueryClient(TYdbDriver* driver); +void YdbDestroyQueryClient(TYdbQueryClient* queryClient); + +// Выполнение запроса +TYdbQueryResult* YdbExecuteQuery(TYdbQueryClient* queryClient, const char* query); +void YdbDestroyQueryResult(TYdbQueryResult* result); + +// Получение результата запроса +int YdbGetQueryResultSetsCount(TYdbQueryResult* result); +TYdbResultSet* YdbGetQueryResultSet(TYdbQueryResult* result, size_t index); + +#ifdef __cplusplus +} +#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/result.h b/c_api/include/ydb-cpp-sdk/c_api/result.h new file mode 100644 index 00000000000..fc762ccf864 --- /dev/null +++ b/c_api/include/ydb-cpp-sdk/c_api/result.h @@ -0,0 +1,32 @@ +#pragma once + +#include "value.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct TYdbResultSetImpl TYdbResultSet; + +typedef enum { + YDB_RESULT_SET_OK, + YDB_RESULT_SET_ERROR, +} EYdbResultSetStatus; + +int YdbGetColumnsCount(TYdbResultSet* resultSet); +int YdbGetRowsCount(TYdbResultSet* resultSet); +int YdbIsTruncated(TYdbResultSet* resultSet); + +const char* YdbGetColumnName(TYdbResultSet* resultSet, size_t index); +int YdbGetColumnIndex(TYdbResultSet* resultSet, const char* name); + +TYdbValue* YdbGetValue(TYdbResultSet* resultSet, size_t rowIndex, const char* name); +TYdbValue* YdbGetValueByIndex(TYdbResultSet* resultSet, size_t rowIndex, size_t columnIndex); + +void YdbDestroyResultSet(TYdbResultSet* resultSet); + +#ifdef __cplusplus +} +#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/value.h b/c_api/include/ydb-cpp-sdk/c_api/value.h new file mode 100644 index 00000000000..5f0ad2caa11 --- /dev/null +++ b/c_api/include/ydb-cpp-sdk/c_api/value.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct TYdbValueImpl TYdbValue; +typedef struct TYdbParamsImpl TYdbParams; + +typedef enum { + YDB_VALUE_OK, + YDB_VALUE_ERROR, +} EYdbValueStatus; + +typedef enum { + YDB_TYPE_KIND_UNDEFINED, + YDB_TYPE_KIND_PRIMITIVE, + YDB_TYPE_KIND_OPTIONAL, + YDB_TYPE_KIND_LIST, + YDB_TYPE_KIND_TUPLE, + YDB_TYPE_KIND_STRUCT, + YDB_TYPE_KIND_DICT, + YDB_TYPE_KIND_VARIANT, +} EYdbTypeKind; + +typedef enum { + YDB_PRIMITIVE_TYPE_UNDEFINED, + YDB_PRIMITIVE_TYPE_BOOL, + YDB_PRIMITIVE_TYPE_INT8, + YDB_PRIMITIVE_TYPE_UINT8, + YDB_PRIMITIVE_TYPE_INT16, + YDB_PRIMITIVE_TYPE_UINT16, + YDB_PRIMITIVE_TYPE_INT32, + YDB_PRIMITIVE_TYPE_UINT32, + YDB_PRIMITIVE_TYPE_INT64, + YDB_PRIMITIVE_TYPE_UINT64, + YDB_PRIMITIVE_TYPE_FLOAT, + YDB_PRIMITIVE_TYPE_DOUBLE, + YDB_PRIMITIVE_TYPE_STRING, + YDB_PRIMITIVE_TYPE_UTF8, + YDB_PRIMITIVE_TYPE_YSON, + YDB_PRIMITIVE_TYPE_JSON, + YDB_PRIMITIVE_TYPE_JSON_DOCUMENT, + YDB_PRIMITIVE_TYPE_DYNUMBER, +} EYdbPrimitiveType; + +EYdbTypeKind YdbGetTypeKind(TYdbValue* value); +EYdbPrimitiveType YdbGetPrimitiveType(TYdbValue* value); + +EYdbValueStatus YdbGetBool(TYdbValue* value, bool* result); +EYdbValueStatus YdbGetInt8(TYdbValue* value, int8_t* result); +EYdbValueStatus YdbGetUint8(TYdbValue* value, uint8_t* result); +EYdbValueStatus YdbGetInt16(TYdbValue* value, int16_t* result); +EYdbValueStatus YdbGetUint16(TYdbValue* value, uint16_t* result); +EYdbValueStatus YdbGetInt32(TYdbValue* value, int32_t* result); +EYdbValueStatus YdbGetUint32(TYdbValue* value, uint32_t* result); +EYdbValueStatus YdbGetInt64(TYdbValue* value, int64_t* result); +EYdbValueStatus YdbGetUint64(TYdbValue* value, uint64_t* result); +EYdbValueStatus YdbGetFloat(TYdbValue* value, float* result); +EYdbValueStatus YdbGetDouble(TYdbValue* value, double* result); +EYdbValueStatus YdbGetString(TYdbValue* value, char** result); +EYdbValueStatus YdbGetUtf8(TYdbValue* value, char** result); +EYdbValueStatus YdbGetYson(TYdbValue* value, char** result); +EYdbValueStatus YdbGetJson(TYdbValue* value, char** result); +EYdbValueStatus YdbGetJsonDocument(TYdbValue* value, char** result); +EYdbValueStatus YdbGetDyNumber(TYdbValue* value, char** result); + +void YdbDestroyValue(TYdbValue* value); + +#ifdef __cplusplus +} +#endif diff --git a/c_api/src/driver.cpp b/c_api/src/driver.cpp new file mode 100644 index 00000000000..27e1093d68e --- /dev/null +++ b/c_api/src/driver.cpp @@ -0,0 +1,172 @@ +#include + +#include "impl/driver_impl.h" // NOLINT + +#include + +extern "C" { + +// Создание и уничтожение конфигурации +TYdbDriverConfig* YdbCreateDriverConfig(const char* connectionString) { + try { + if (!connectionString) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config pointer"}; + } + + try { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_OK, "", NYdb::TDriverConfig(connectionString)}; + } catch (const std::exception& e) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; + } + } catch (const std::exception& e) { + return nullptr; + } +} + +void YdbDestroyDriverConfig(TYdbDriverConfig* config) { + if (config) { + delete config; + } +} + +// Установка параметров конфигурации +TYdbDriverConfig* YdbSetEndpoint(TYdbDriverConfig* config, const char* endpoint) { + try { + if (!config) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; + } + if (!endpoint) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid endpoint"}; + } + + try { + config->config.SetEndpoint(std::string(endpoint)); + return config; + } catch (const std::exception& e) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +TYdbDriverConfig* YdbSetDatabase(TYdbDriverConfig* config, const char* database) { + try { + if (!config) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; + } + if (!database) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid database"}; + } + + try { + config->config.SetDatabase(std::string(database)); + return config; + } catch (const std::exception& e) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +TYdbDriverConfig* YdbSetAuthToken(TYdbDriverConfig* config, const char* token) { + try { + if (!config) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; + } + if (!token) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid token"}; + } + + try { + config->config.SetAuthToken(std::string(token)); + return config; + } catch (const std::exception& e) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +TYdbDriverConfig* YdbSetSecureConnection(TYdbDriverConfig* config, const char* cert) { + try { + if (!config) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; + } + if (!cert) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid certificate"}; + } + + try { + config->config.UseSecureConnection(std::string(cert)); + return config; + } catch (const std::exception& e) { + return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +EYdbDriverConfigStatus YdbGetDriverConfigStatus(TYdbDriverConfig* config) { + if (!config) { + return YDB_DRIVER_CONFIG_INVALID; + } + + return config->errorCode; +} + +const char* YdbGetDriverConfigErrorMessage(TYdbDriverConfig* config) { + if (!config) { + return "Invalid config"; + } + + return config->errorMessage.c_str(); +} + +// Создание и уничтожение драйвера +TYdbDriver* YdbCreateDriverFromConfig(TYdbDriverConfig* config) { + try { + if (!config) { + return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid config"}; + } + + if (config->errorCode != 0) { + return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid config: " + config->errorMessage}; + } + + try { + return new TYdbDriver{YDB_DRIVER_OK, "", NYdb::TDriver(config->config)}; + } catch (const std::exception& e) { + return new TYdbDriver{YDB_DRIVER_ERROR, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +TYdbDriver* YdbCreateDriver(const char* connectionString) { + try { + if (!connectionString) { + return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid connection string"}; + } + + try { + return new TYdbDriver{YDB_DRIVER_OK, "", NYdb::TDriver(std::string(connectionString))}; + } catch (const std::exception& e) { + return new TYdbDriver{YDB_DRIVER_ERROR, e.what()}; + } + } catch (...) { + return nullptr; + } +} + +void YdbDestroyDriver(TYdbDriver* driver) { + if (driver) { + delete driver; + } +} + +} diff --git a/c_api/src/impl/driver_impl.h b/c_api/src/impl/driver_impl.h new file mode 100644 index 00000000000..87644f15f26 --- /dev/null +++ b/c_api/src/impl/driver_impl.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +#include +#include + +struct TYdbDriverConfigImpl { + EYdbDriverConfigStatus errorCode; + std::string errorMessage; + + NYdb::TDriverConfig config; +}; + +struct TYdbDriverImpl { + EYdbDriverStatus errorCode; + std::string errorMessage; + + std::optional driver; +}; diff --git a/c_api/src/impl/result_impl.h b/c_api/src/impl/result_impl.h new file mode 100644 index 00000000000..b662ea4f452 --- /dev/null +++ b/c_api/src/impl/result_impl.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +#include + +struct TYdbResultSetImpl { + EYdbResultSetStatus errorCode; + std::string errorMessage; + + std::optional result; +}; diff --git a/c_api/src/impl/value_impl.h b/c_api/src/impl/value_impl.h new file mode 100644 index 00000000000..d7a7a4eba9e --- /dev/null +++ b/c_api/src/impl/value_impl.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +#include +#include + +struct TYdbValueImpl { + EYdbValueStatus errorCode; + std::string errorMessage; + + std::optional value; +}; diff --git a/c_api/src/query.cpp b/c_api/src/query.cpp new file mode 100644 index 00000000000..0f11c52c17c --- /dev/null +++ b/c_api/src/query.cpp @@ -0,0 +1,140 @@ +#include + +#include + +#include "impl/driver_impl.h" // NOLINT +#include "impl/result_impl.h" // NOLINT + +#include + +struct TYdbQueryClientImpl { + EYdbQueryClientError errorCode; + std::string errorMessage; + + std::optional client; +}; + +struct TYdbQueryResultImpl { + EYdbQueryResultError errorCode; + std::string errorMessage; + + std::optional result; +}; + +extern "C" { + +TYdbQueryClient* YdbCreateQueryClient(TYdbDriver* driver) { + try { + if (!driver || !driver->driver.has_value()) { + return new TYdbQueryClient{ + YDB_QUERY_CLIENT_ERROR, + "Invalid driver" + }; + } + + try { + return new TYdbQueryClient{ + YDB_QUERY_CLIENT_OK, + "", + NYdb::NQuery::TQueryClient(*driver->driver) + }; + } catch (const std::exception& e) { + return new TYdbQueryClient{ + YDB_QUERY_CLIENT_ERROR, + e.what() + }; + } + } catch (...) { + return nullptr; + } +} + +void YdbDestroyQueryClient(TYdbQueryClient* queryClient) { + if (queryClient) { + delete queryClient; + } +} + +TYdbQueryResult* YdbExecuteQuery(TYdbQueryClient* queryClient, const char* query) { + try { + if (!queryClient || !queryClient->client.has_value()) { + return new TYdbQueryResult{ + YDB_QUERY_RESULT_ERROR, + "Invalid query client" + }; + } + + if (!query) { + return new TYdbQueryResult{ + YDB_QUERY_RESULT_ERROR, + "Invalid query" + }; + } + + try { + auto client = *queryClient->client; + auto executeResult = client.ExecuteQuery( + std::string(query), + NYdb::NQuery::TTxControl::NoTx() + ).GetValueSync(); + + if (!executeResult.IsSuccess()) { + return new TYdbQueryResult{ + YDB_QUERY_RESULT_ERROR, + "Query execution failed: " + executeResult.GetIssues().ToString() + }; + } + + return new TYdbQueryResult{ + YDB_QUERY_RESULT_OK, + "", + executeResult + }; + } catch (const std::exception& e) { + return new TYdbQueryResult{ + YDB_QUERY_RESULT_ERROR, + e.what() + }; + } + } catch (...) { + return nullptr; + } +} + +void YdbDestroyQueryResult(TYdbQueryResult* result) { + if (result) { + delete result; + } +} + +TYdbResultSet* YdbGetQueryResultSet(TYdbQueryResult* result, size_t index) { + try { + if (!result || !result->result.has_value()) { + return nullptr; + } + + try { + return new TYdbResultSet{ + YDB_RESULT_SET_OK, + "", + result->result->GetResultSet(index) + }; + } catch (const std::exception& e) { + return new TYdbResultSet{ + YDB_RESULT_SET_ERROR, + e.what() + }; + } + } catch (...) { + return nullptr; + } +} + +int YdbGetQueryResultSetsCount(TYdbQueryResult* result) { + if (!result || !result->result.has_value()) { + return -1; + } + return result->result->GetResultSets().size(); +} + +} diff --git a/c_api/src/result.cpp b/c_api/src/result.cpp new file mode 100644 index 00000000000..c6c52bc7373 --- /dev/null +++ b/c_api/src/result.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include + +#include "impl/result_impl.h" // NOLINT +#include "impl/value_impl.h" // NOLINT + +extern "C" { + +int YdbGetColumnsCount(TYdbResultSet* resultSet) { + if (!resultSet || !resultSet->result.has_value()) { + return -1; + } + + return resultSet->result->ColumnsCount(); +} + +int YdbGetRowsCount(TYdbResultSet* resultSet) { + if (!resultSet || !resultSet->result.has_value()) { + return -1; + } + + return resultSet->result->RowsCount(); +} + +int YdbIsTruncated(TYdbResultSet* resultSet) { + if (!resultSet || !resultSet->result.has_value()) { + return -1; + } + + return resultSet->result->Truncated(); +} + +const char* YdbGetColumnName(TYdbResultSet* resultSet, size_t index) { + if (!resultSet || !resultSet->result.has_value()) { + return nullptr; + } + + return resultSet->result->GetColumnsMeta()[index].Name.c_str(); +} + +int YdbGetColumnIndex(TYdbResultSet* resultSet, const char* name) { + try { + if (!resultSet || !resultSet->result.has_value()) { + return -1; + } + + NYdb::TResultSetParser parser(*resultSet->result); + return parser.ColumnIndex(name); + } catch (...) { + return -1; + } +} + +TYdbValue* YdbGetValue(TYdbResultSet* resultSet, size_t rowIndex, const char* name) { + try { + if (!resultSet || !resultSet->result.has_value()) { + return new TYdbValue{YDB_VALUE_ERROR, "Invalid result set"}; + } + + NYdb::TResultSetParser parser(*resultSet->result); + int columnIndex = parser.ColumnIndex(name); + if (columnIndex == -1) { + return new TYdbValue{YDB_VALUE_ERROR, "Invalid column name"}; + } + + return YdbGetValueByIndex(resultSet, rowIndex, columnIndex); + } catch (...) { + return nullptr; + } +} + +TYdbValue* YdbGetValueByIndex(TYdbResultSet* resultSet, size_t rowIndex, size_t columnIndex) { + try { + if (!resultSet || !resultSet->result.has_value()) { + return nullptr; + } + + auto proto = NYdb::TProtoAccessor::GetProto(*resultSet->result); + + auto type = resultSet->result->GetColumnsMeta()[columnIndex].Type; + auto value = proto.rows(rowIndex).items(columnIndex); + + return new TYdbValue{YDB_VALUE_OK, "", NYdb::TValue{type, value}}; + } catch (...) { + return nullptr; + } +} + +void YdbDestroyResultSet(TYdbResultSet* resultSet) { + delete resultSet; +} + +} diff --git a/c_api/src/value.cpp b/c_api/src/value.cpp new file mode 100644 index 00000000000..2aa25ce4183 --- /dev/null +++ b/c_api/src/value.cpp @@ -0,0 +1,276 @@ +#include + +#include + +#include "impl/value_impl.h" // NOLINT + +extern "C" { + +EYdbTypeKind YdbGetTypeKind(TYdbValue* value) { + if (!value || !value->value.has_value()) { + return YDB_TYPE_KIND_UNDEFINED; + } + + NYdb::TValueParser valueParser(*value->value); + + switch (valueParser.GetKind()) { + case NYdb::TTypeParser::ETypeKind::Primitive: + return YDB_TYPE_KIND_PRIMITIVE; + case NYdb::TTypeParser::ETypeKind::Optional: + return YDB_TYPE_KIND_OPTIONAL; + case NYdb::TTypeParser::ETypeKind::List: + return YDB_TYPE_KIND_LIST; + case NYdb::TTypeParser::ETypeKind::Struct: + return YDB_TYPE_KIND_STRUCT; + case NYdb::TTypeParser::ETypeKind::Tuple: + return YDB_TYPE_KIND_TUPLE; + case NYdb::TTypeParser::ETypeKind::Dict: + return YDB_TYPE_KIND_DICT; + case NYdb::TTypeParser::ETypeKind::Variant: + return YDB_TYPE_KIND_VARIANT; + default: + return YDB_TYPE_KIND_UNDEFINED; + } +} + +EYdbPrimitiveType YdbGetPrimitiveType(TYdbValue* value) { + if (!value || !value->value.has_value()) { + return YDB_PRIMITIVE_TYPE_UNDEFINED; + } + + try { + NYdb::TValueParser valueParser(*value->value); + + switch (valueParser.GetPrimitiveType()) { + case NYdb::EPrimitiveType::Int8: + return YDB_PRIMITIVE_TYPE_INT8; + case NYdb::EPrimitiveType::Uint8: + return YDB_PRIMITIVE_TYPE_UINT8; + case NYdb::EPrimitiveType::Int16: + return YDB_PRIMITIVE_TYPE_INT16; + case NYdb::EPrimitiveType::Uint16: + return YDB_PRIMITIVE_TYPE_UINT16; + case NYdb::EPrimitiveType::Int32: + return YDB_PRIMITIVE_TYPE_INT32; + case NYdb::EPrimitiveType::Uint32: + return YDB_PRIMITIVE_TYPE_UINT32; + case NYdb::EPrimitiveType::Int64: + return YDB_PRIMITIVE_TYPE_INT64; + case NYdb::EPrimitiveType::Uint64: + return YDB_PRIMITIVE_TYPE_UINT64; + case NYdb::EPrimitiveType::Float: + return YDB_PRIMITIVE_TYPE_FLOAT; + case NYdb::EPrimitiveType::Double: + return YDB_PRIMITIVE_TYPE_DOUBLE; + case NYdb::EPrimitiveType::String: + return YDB_PRIMITIVE_TYPE_STRING; + case NYdb::EPrimitiveType::Utf8: + return YDB_PRIMITIVE_TYPE_UTF8; + case NYdb::EPrimitiveType::Yson: + return YDB_PRIMITIVE_TYPE_YSON; + case NYdb::EPrimitiveType::Json: + return YDB_PRIMITIVE_TYPE_JSON; + case NYdb::EPrimitiveType::JsonDocument: + return YDB_PRIMITIVE_TYPE_JSON_DOCUMENT; + case NYdb::EPrimitiveType::DyNumber: + return YDB_PRIMITIVE_TYPE_DYNUMBER; + default: + return YDB_PRIMITIVE_TYPE_UNDEFINED; + } + } catch (...) { + return YDB_PRIMITIVE_TYPE_UNDEFINED; + } +} + +EYdbValueStatus YdbGetInt8(TYdbValue* value, int8_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetInt8(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetUint8(TYdbValue* value, uint8_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetUint8(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetInt16(TYdbValue* value, int16_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetInt16(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetUint16(TYdbValue* value, uint16_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetUint16(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetInt32(TYdbValue* value, int32_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetInt32(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetUint32(TYdbValue* value, uint32_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetUint32(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetInt64(TYdbValue* value, int64_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetInt64(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetUint64(TYdbValue* value, uint64_t* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetUint64(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetFloat(TYdbValue* value, float* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetFloat(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetDouble(TYdbValue* value, double* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetDouble(); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetString(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetString().c_str(), valueParser.GetString().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetUtf8(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetUtf8().c_str(), valueParser.GetUtf8().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetYson(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetYson().c_str(), valueParser.GetYson().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetJson(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetJson().c_str(), valueParser.GetJson().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetJsonDocument(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetJsonDocument().c_str(), valueParser.GetJsonDocument().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetDyNumber(TYdbValue* value, char** result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = strndup(valueParser.GetDyNumber().c_str(), valueParser.GetDyNumber().size()); + return YDB_VALUE_OK; +} + +EYdbValueStatus YdbGetBool(TYdbValue* value, bool* result) { + if (!value || !value->value.has_value()) { + return YDB_VALUE_ERROR; + } + + NYdb::TValueParser valueParser(*value->value); + + *result = valueParser.GetBool(); + return YDB_VALUE_OK; +} + +void YdbDestroyValue(TYdbValue* value) { + delete value; +} + +} diff --git a/cmake/common.cmake b/cmake/common.cmake index b40eee34324..e23aeda5e5e 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -168,7 +168,7 @@ function(resources Tgt Output) endfunction() function(_ydb_sdk_make_client_component CmpName Tgt) - add_library(YDB-CPP-SDK::${CmpName} ALIAS ${Tgt}) + add_library(ydb-cpp-sdk::${CmpName} ALIAS ${Tgt}) if (CmpName STREQUAL "Iam" OR CmpName STREQUAL "IamPrivate") set(PKG_COMP_NAME "libydb-cpp-iam") @@ -196,7 +196,7 @@ endfunction() function(_ydb_sdk_add_library Tgt) cmake_parse_arguments(ARG - "INTERFACE" "" "" + "INTERFACE;OBJECT" "" "" ${ARGN} ) @@ -206,7 +206,9 @@ function(_ydb_sdk_add_library Tgt) set(libraryMode "INTERFACE") set(includeMode "INTERFACE") endif() - + if (ARG_OBJECT) + set(libraryMode "OBJECT") + endif() add_library(${Tgt} ${libraryMode}) target_include_directories(${Tgt} ${includeMode} $ diff --git a/cmake/ydb-cpp-sdk-config.cmake.in b/cmake/ydb-cpp-sdk-config.cmake.in index c6817d87998..9b303352b18 100644 --- a/cmake/ydb-cpp-sdk-config.cmake.in +++ b/cmake/ydb-cpp-sdk-config.cmake.in @@ -103,7 +103,7 @@ function(_find_ydb_sdk_component CompName) message(FATAL_ERROR "${CompName} is not available component") endif() list(GET YDB-CPP-SDK_COMPONENT_TARGETS ${CompId} Tgt) - add_library(YDB-CPP-SDK::${CompName} ALIAS YDB-CPP-SDK::${Tgt}) + add_library(ydb-cpp-sdk::${CompName} ALIAS ydb-cpp-sdk::${Tgt}) set(${_ydb_sdk_package_name}_${CompName}_FOUND TRUE PARENT_SCOPE) endfunction() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f7f2028139f..0c712974cf3 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(basic_example) add_subdirectory(bulk_upsert_simple) +add_subdirectory(c_api) add_subdirectory(pagination) add_subdirectory(secondary_index) add_subdirectory(secondary_index_builtin) diff --git a/examples/basic_example/CMakeLists.txt b/examples/basic_example/CMakeLists.txt index 75d2d1b2538..8e513439417 100644 --- a/examples/basic_example/CMakeLists.txt +++ b/examples/basic_example/CMakeLists.txt @@ -3,9 +3,9 @@ add_executable(basic_example) target_link_libraries(basic_example PUBLIC yutil getopt - YDB-CPP-SDK::Query - YDB-CPP-SDK::Params - YDB-CPP-SDK::Driver + ydb-cpp-sdk::Query + ydb-cpp-sdk::Params + ydb-cpp-sdk::Driver ) target_sources(basic_example PRIVATE diff --git a/examples/bulk_upsert_simple/CMakeLists.txt b/examples/bulk_upsert_simple/CMakeLists.txt index 4f7c3eca7f9..34b8ed62c3c 100644 --- a/examples/bulk_upsert_simple/CMakeLists.txt +++ b/examples/bulk_upsert_simple/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(bulk_upsert_simple) target_link_libraries(bulk_upsert_simple PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(bulk_upsert_simple PRIVATE diff --git a/examples/c_api/CMakeLists.txt b/examples/c_api/CMakeLists.txt new file mode 100644 index 00000000000..5060c4c25d3 --- /dev/null +++ b/examples/c_api/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(c_api_example + main.c +) + +target_link_libraries(c_api_example ydb-cpp-sdk::c-api) diff --git a/examples/c_api/main.c b/examples/c_api/main.c new file mode 100644 index 00000000000..ca1e2018d0f --- /dev/null +++ b/examples/c_api/main.c @@ -0,0 +1,37 @@ +#include + +#include + +#include + +int main() { + TYdbDriver* driver = YdbCreateDriver("grpc://localhost:2136/?database=/local"); + + TYdbQueryClient* query = YdbCreateQueryClient(driver); + + TYdbQueryResult* result = YdbExecuteQuery(query, "SELECT 1"); + + int resultSetsCount = YdbGetQueryResultSetsCount(result); + for (int i = 0; i < resultSetsCount; i++) { + TYdbResultSet* resultSet = YdbGetQueryResultSet(result, i); + int rowsCount = YdbGetRowsCount(resultSet); + for (int j = 0; j < rowsCount; j++) { + TYdbValue* value = YdbGetValueByIndex(resultSet, j, 0); + + EYdbPrimitiveType primitiveType = YdbGetPrimitiveType(value); + if (primitiveType == YDB_PRIMITIVE_TYPE_INT32) { + int32_t int32Value; + YdbGetInt32(value, &int32Value); + printf("%" PRId32 "\n", int32Value); + } else { + printf("Unknown primitive type\n"); + } + YdbDestroyValue(value); + } + } + + YdbDestroyQueryResult(result); + YdbDestroyQueryClient(query); + YdbDestroyDriver(driver); + return 0; +} diff --git a/examples/pagination/CMakeLists.txt b/examples/pagination/CMakeLists.txt index 0936f385585..2b29726f007 100644 --- a/examples/pagination/CMakeLists.txt +++ b/examples/pagination/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(pagination) target_link_libraries(pagination PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(pagination PRIVATE diff --git a/examples/secondary_index/CMakeLists.txt b/examples/secondary_index/CMakeLists.txt index 6030de5f7f0..47364c55979 100644 --- a/examples/secondary_index/CMakeLists.txt +++ b/examples/secondary_index/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(secondary_index) target_link_libraries(secondary_index PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(secondary_index PRIVATE diff --git a/examples/secondary_index_builtin/CMakeLists.txt b/examples/secondary_index_builtin/CMakeLists.txt index b46cc79159c..e03e675827a 100644 --- a/examples/secondary_index_builtin/CMakeLists.txt +++ b/examples/secondary_index_builtin/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(secondary_index_builtin) target_link_libraries(secondary_index_builtin PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(secondary_index_builtin PRIVATE diff --git a/examples/topic_reader/eventloop/CMakeLists.txt b/examples/topic_reader/eventloop/CMakeLists.txt index 2cdc984955f..114a0ae35ba 100644 --- a/examples/topic_reader/eventloop/CMakeLists.txt +++ b/examples/topic_reader/eventloop/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(persqueue_reader_eventloop) target_link_libraries(persqueue_reader_eventloop PUBLIC yutil - YDB-CPP-SDK::Topic + ydb-cpp-sdk::Topic getopt ) diff --git a/examples/topic_reader/simple/CMakeLists.txt b/examples/topic_reader/simple/CMakeLists.txt index 68846ab215c..2b7da165f05 100644 --- a/examples/topic_reader/simple/CMakeLists.txt +++ b/examples/topic_reader/simple/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(simple_persqueue_reader) target_link_libraries(simple_persqueue_reader PUBLIC yutil - YDB-CPP-SDK::Topic + ydb-cpp-sdk::Topic getopt ) diff --git a/examples/topic_reader/transaction/CMakeLists.txt b/examples/topic_reader/transaction/CMakeLists.txt index 64d30b4d8c6..77fd8ab446e 100644 --- a/examples/topic_reader/transaction/CMakeLists.txt +++ b/examples/topic_reader/transaction/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(read_from_topic_in_transaction) target_link_libraries(read_from_topic_in_transaction PUBLIC yutil - YDB-CPP-SDK::Topic + ydb-cpp-sdk::Topic getopt ) diff --git a/examples/ttl/CMakeLists.txt b/examples/ttl/CMakeLists.txt index 48a004b4cc6..9a0655e7d1b 100644 --- a/examples/ttl/CMakeLists.txt +++ b/examples/ttl/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(ttl) target_link_libraries(ttl PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(ttl PRIVATE diff --git a/examples/vector_index/CMakeLists.txt b/examples/vector_index/CMakeLists.txt index 19249951e37..792d11cb3b1 100644 --- a/examples/vector_index/CMakeLists.txt +++ b/examples/vector_index/CMakeLists.txt @@ -4,7 +4,7 @@ target_link_libraries(vector_index PUBLIC yutil getopt - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table ) target_sources(vector_index PRIVATE diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 985782900ee..b2a5b03b083 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -30,9 +30,9 @@ target_include_directories(ydb-odbc # Линкуем с YDB SDK и ODBC target_link_libraries(ydb-odbc PUBLIC - YDB-CPP-SDK::Query - YDB-CPP-SDK::Table - YDB-CPP-SDK::Driver + ydb-cpp-sdk::Query + ydb-cpp-sdk::Table + ydb-cpp-sdk::Driver ODBC::ODBC ) diff --git a/odbc/include/client/driver.h b/odbc/include/client/driver.h deleted file mode 100644 index 6a95f9958c8..00000000000 --- a/odbc/include/client/driver.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include // для size_t - -#ifdef __cplusplus -extern "C" { -#endif - -// Функции для работы с YDB через C++ SDK -void* YDB_CreateDriver(const char* endpoint, const char* user, const char* password); -void YDB_DestroyDriver(void* driver); - -#ifdef __cplusplus -} -#endif diff --git a/odbc/include/client/query.h b/odbc/include/client/query.h deleted file mode 100644 index ac6802dc6c8..00000000000 --- a/odbc/include/client/query.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -void* YDB_CreateQueryClient(void* driver); -void YDB_DestroyQueryClient(void* query_client); - -int YDB_ExecuteQuery(void* query_client, const char* query, void** result); -void YDB_FreeExecuteQueryResult(void* result); - -#ifdef __cplusplus -} -#endif diff --git a/odbc/src/client/driver.cpp b/odbc/src/client/driver.cpp deleted file mode 100644 index faa87471e43..00000000000 --- a/odbc/src/client/driver.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "client/driver.h" - -#include - -extern "C" { - -void* YDB_CreateDriver(const char* endpoint, const char* user, const char* password) { - try { - auto config = NYdb::TDriverConfig().SetEndpoint(std::string(endpoint, strlen(endpoint))); - - auto* driver = new NYdb::TDriver(config); - return static_cast(driver); - } catch (...) { - return nullptr; - } -} - -void YDB_DestroyDriver(void* driver) { - if (driver) { - auto* ydb_driver = static_cast(driver); - delete ydb_driver; - } -} - -} diff --git a/odbc/src/client/query.cpp b/odbc/src/client/query.cpp deleted file mode 100644 index 5da339d19bd..00000000000 --- a/odbc/src/client/query.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "client/query.h" - -#include - -#include - -extern "C" { - -void* YDB_CreateQueryClient(void* driver) { - if (!driver) return nullptr; - - try { - auto* ydb_driver = static_cast(driver); - auto* query_client = new NYdb::NQuery::TQueryClient(*ydb_driver); - return static_cast(query_client); - } catch (...) { - return nullptr; - } -} - -void YDB_DestroyQueryClient(void* query_client) { - if (query_client) { - auto* client = static_cast(query_client); - delete client; - } -} - -int YDB_ExecuteQuery(void* query_client, const char* query, void** result) { - if (!query_client || !query || !result) { - return 0; - } - - try { - auto* client = static_cast(query_client); - auto executeResult = client->ExecuteQuery(std::string(query, strlen(query)), NYdb::NQuery::TTxControl::NoTx()).GetValueSync(); - - if (!executeResult.IsSuccess()) { - return 0; - } - - *result = reinterpret_cast(new NYdb::NQuery::TExecuteQueryResult(executeResult)); - - return 1; - } catch (...) { - return 0; - } -} - -void YDB_FreeExecuteQueryResult(void* result) { - if (result) { - auto* executeResult = reinterpret_cast(result); - delete executeResult; - } -} - -} // extern "C" diff --git a/tests/integration/basic_example/CMakeLists.txt b/tests/integration/basic_example/CMakeLists.txt index 55bdd05341b..9eec918ec0e 100644 --- a/tests/integration/basic_example/CMakeLists.txt +++ b/tests/integration/basic_example/CMakeLists.txt @@ -6,9 +6,9 @@ add_ydb_test(NAME basic_example_it GTEST LINK_LIBRARIES yutil api-protos - YDB-CPP-SDK::Driver - YDB-CPP-SDK::Proto - YDB-CPP-SDK::Table + ydb-cpp-sdk::Driver + ydb-cpp-sdk::Proto + ydb-cpp-sdk::Table LABELS integration ) diff --git a/tests/integration/bulk_upsert/CMakeLists.txt b/tests/integration/bulk_upsert/CMakeLists.txt index 46848877c69..535d21f2d61 100644 --- a/tests/integration/bulk_upsert/CMakeLists.txt +++ b/tests/integration/bulk_upsert/CMakeLists.txt @@ -5,7 +5,7 @@ add_ydb_test(NAME bulk_upsert_it GTEST bulk_upsert.h LINK_LIBRARIES yutil - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table LABELS integration ) diff --git a/tests/integration/server_restart/CMakeLists.txt b/tests/integration/server_restart/CMakeLists.txt index 2d485de4e4a..66d1c00d641 100644 --- a/tests/integration/server_restart/CMakeLists.txt +++ b/tests/integration/server_restart/CMakeLists.txt @@ -4,7 +4,7 @@ add_ydb_test(NAME server_restart_it GTEST LINK_LIBRARIES yutil api-grpc - YDB-CPP-SDK::Query + ydb-cpp-sdk::Query gRPC::grpc++ LABELS integration diff --git a/tests/unit/client/CMakeLists.txt b/tests/unit/client/CMakeLists.txt index 95d7afa2284..93c5610ba2c 100644 --- a/tests/unit/client/CMakeLists.txt +++ b/tests/unit/client/CMakeLists.txt @@ -35,8 +35,8 @@ add_ydb_test(NAME client-driver_ut driver/driver_ut.cpp LINK_LIBRARIES yutil - YDB-CPP-SDK::Driver - YDB-CPP-SDK::Table + ydb-cpp-sdk::Driver + ydb-cpp-sdk::Table LABELS unit ) From 74eda9ddf0bc0709931c531501c755cc73a8ec0e Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Fri, 16 May 2025 14:20:46 +0000 Subject: [PATCH 03/55] ODBC MVP --- c_api/CMakeLists.txt | 4 +- cmake/common.cmake | 8 +- cmake/external_libs.cmake | 4 + odbc/CMakeLists.txt | 32 +-- odbc/examples/CMakeLists.txt | 1 + odbc/examples/basic/CMakeLists.txt | 14 ++ odbc/examples/basic/main.cpp | 141 ++++++++++++ odbc/include/ydb_odbc.h | 89 -------- odbc/src/connection.c | 132 ----------- odbc/src/connection.cpp | 119 ++++++++++ odbc/src/connection.h | 53 +++++ odbc/src/descriptor.c | 109 --------- odbc/src/driver.c | 142 ------------ odbc/src/environment.cpp | 38 ++++ odbc/src/environment.h | 40 ++++ odbc/src/odbc_driver.cpp | 253 +++++++++++++++++++++ odbc/src/statement.c | 98 --------- odbc/src/statement.cpp | 342 +++++++++++++++++++++++++++++ odbc/src/statement.h | 72 ++++++ 19 files changed, 1098 insertions(+), 593 deletions(-) create mode 100644 odbc/examples/CMakeLists.txt create mode 100644 odbc/examples/basic/CMakeLists.txt create mode 100644 odbc/examples/basic/main.cpp delete mode 100644 odbc/include/ydb_odbc.h delete mode 100644 odbc/src/connection.c create mode 100644 odbc/src/connection.cpp create mode 100644 odbc/src/connection.h delete mode 100644 odbc/src/descriptor.c delete mode 100644 odbc/src/driver.c create mode 100644 odbc/src/environment.cpp create mode 100644 odbc/src/environment.h create mode 100644 odbc/src/odbc_driver.cpp delete mode 100644 odbc/src/statement.c create mode 100644 odbc/src/statement.cpp create mode 100644 odbc/src/statement.h diff --git a/c_api/CMakeLists.txt b/c_api/CMakeLists.txt index 6f7a7295390..b2c8fec22a4 100644 --- a/c_api/CMakeLists.txt +++ b/c_api/CMakeLists.txt @@ -1,4 +1,6 @@ -add_library(ydb-c-api STATIC +_ydb_sdk_add_library(ydb-c-api SHARED) + +target_sources(ydb-c-api PRIVATE src/driver.cpp src/query.cpp src/result.cpp diff --git a/cmake/common.cmake b/cmake/common.cmake index e23aeda5e5e..54d21a418a2 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -110,7 +110,7 @@ function(generate_enum_serilization Tgt Input) endfunction() function(add_global_library_for TgtName MainName) - add_library(${TgtName} STATIC ${ARGN}) + _ydb_sdk_add_library(${TgtName} STATIC ${ARGN}) if(APPLE) target_link_options(${MainName} INTERFACE "SHELL:-Wl,-force_load,$${TgtName}>") else() @@ -196,7 +196,7 @@ endfunction() function(_ydb_sdk_add_library Tgt) cmake_parse_arguments(ARG - "INTERFACE;OBJECT" "" "" + "INTERFACE;OBJECT;SHARED" "" "" ${ARGN} ) @@ -209,6 +209,9 @@ function(_ydb_sdk_add_library Tgt) if (ARG_OBJECT) set(libraryMode "OBJECT") endif() + if (ARG_SHARED) + set(libraryMode "SHARED") + endif() add_library(${Tgt} ${libraryMode}) target_include_directories(${Tgt} ${includeMode} $ @@ -219,6 +222,7 @@ function(_ydb_sdk_add_library Tgt) YDB_SDK_OSS ) _ydb_sdk_apply_coverage(${Tgt}) + set_property(TARGET ${Tgt} PROPERTY POSITION_INDEPENDENT_CODE ON) endfunction() diff --git a/cmake/external_libs.cmake b/cmake/external_libs.cmake index 469df6000ca..e88fe817577 100644 --- a/cmake/external_libs.cmake +++ b/cmake/external_libs.cmake @@ -197,6 +197,10 @@ if (YDB_SDK_ENABLE_OTEL_METRICS OR YDB_SDK_ENABLE_OTEL_TRACE) set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "${_ydb_sdk_saved_install_component}") endif() +if (YDB_SDK_ODBC) + find_package(ODBC REQUIRED) +endif() + # RapidJSON if (YDB_SDK_USE_RAPID_JSON) find_package(RapidJSON REQUIRED) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index b2a5b03b083..46747768327 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,41 +1,31 @@ -cmake_minimum_required(VERSION 3.14) -project(ydb-odbc VERSION 0.1.0 LANGUAGES C CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# Находим зависимости -find_package(ODBC REQUIRED) - # Добавляем исходники add_library(ydb-odbc SHARED - src/driver.c - src/connection.c - src/statement.c - src/descriptor.c - src/client/driver.cpp - src/client/query.cpp + src/odbc_driver.cpp + src/connection.cpp + src/statement.cpp + src/environment.cpp ) # Добавляем заголовочные файлы target_include_directories(ydb-odbc - PUBLIC + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${ODBC_INCLUDE_DIRS} - /usr/include - /usr/local/include ) # Линкуем с YDB SDK и ODBC target_link_libraries(ydb-odbc - PUBLIC + PRIVATE ydb-cpp-sdk::Query ydb-cpp-sdk::Table ydb-cpp-sdk::Driver ODBC::ODBC ) +set_target_properties(ydb-odbc PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + # Устанавливаем драйвер install(TARGETS ydb-odbc LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -46,6 +36,8 @@ install(DIRECTORY include/ DESTINATION include/ydb-odbc ) +add_subdirectory(examples) + # Добавляем тесты # add_subdirectory(tests) diff --git a/odbc/examples/CMakeLists.txt b/odbc/examples/CMakeLists.txt new file mode 100644 index 00000000000..6f4cd2f5a31 --- /dev/null +++ b/odbc/examples/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(basic) diff --git a/odbc/examples/basic/CMakeLists.txt b/odbc/examples/basic/CMakeLists.txt new file mode 100644 index 00000000000..a34cbd9301b --- /dev/null +++ b/odbc/examples/basic/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(odbc_basic + main.cpp +) + +target_link_libraries(odbc_basic + PRIVATE + ODBC::ODBC +) +target_compile_definitions(odbc_basic + PRIVATE + ODBC_DRIVER_PATH="$" +) + +add_dependencies(odbc_basic ydb-odbc) diff --git a/odbc/examples/basic/main.cpp b/odbc/examples/basic/main.cpp new file mode 100644 index 00000000000..9b8123ef7c1 --- /dev/null +++ b/odbc/examples/basic/main.cpp @@ -0,0 +1,141 @@ +#include +#include + +#include +#include +#include +#include + +void PrintOdbcError(SQLSMALLINT handleType, SQLHANDLE handle) { + SQLCHAR sqlState[6] = {0}; + SQLINTEGER nativeError = 0; + SQLCHAR message[256] = {0}; + SQLSMALLINT textLength = 0; + SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError, message, sizeof(message), &textLength); + std::cerr << "ODBC error: [" << sqlState << "] " << message << std::endl; +} + +int main() { + SQLHENV henv = nullptr; + SQLHDBC hdbc = nullptr; + SQLHSTMT hstmt = nullptr; + SQLRETURN ret; + + std::cout << "1. Allocating environment handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating environment handle" << std::endl; + return 1; + } + SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + std::cout << "2. Allocating connection handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating connection handle" << std::endl; + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "3. Building connection string" << std::endl; + std::string connStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; + SQLCHAR outConnStr[1024] = {0}; + SQLSMALLINT outConnStrLen = 0; + + std::cout << "4. Connecting with SQLDriverConnect" << std::endl; + ret = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)connStr.c_str(), SQL_NTS, + outConnStr, sizeof(outConnStr), &outConnStrLen, SQL_DRIVER_COMPLETE); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error connecting with SQLDriverConnect" << std::endl; + PrintOdbcError(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "5. Allocating statement handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating statement handle" << std::endl; + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "6. Executing query" << std::endl; + SQLCHAR query[] = R"( + DECLARE $p1 AS Int64; + SELECT $p1 + 1, 'test1' as String; + SELECT $p1 + 2, 'test2' as String; + SELECT $p1 + 3, 'test3' as String; + SELECT $p1 + 4, 'test4' as String; + SELECT $p1 + 5, 'test5' as String; + SELECT $p1 + 6, 'test6' as String; + SELECT $p1 + 7, 'test7' as String; + SELECT $p1 + 8, 'test8' as String; + SELECT $p1 + 9, 'test9' as String; + )"; + + int64_t paramValue = 42; + SQLLEN paramInd = 0; + ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, ¶mValue, 0, ¶mInd); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error binding parameter" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + ret = SQLExecDirect(hstmt, query, SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error executing query" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "7. Fetching result" << std::endl; + + SQLLEN ind = 0; + int value1 = 0; + if (SQLBindCol(hstmt, 1, SQL_C_SLONG, &value1, 0, &ind) != SQL_SUCCESS) { + std::cerr << "Error binding column 1" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + SQLCHAR value2[1024] = {0}; + if (SQLBindCol(hstmt, 2, SQL_C_CHAR, &value2, 1024, &ind) != SQL_SUCCESS) { + std::cerr << "Error binding column 2" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + while ((ret = SQLFetch(hstmt)) == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) { + if (ret != SQL_SUCCESS) { + std::cerr << "Error fetching result" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + std::cout << "Result column 1: " << value1 << std::endl; + std::cout << "Result column 2: " << value2 << std::endl; + + std::cout << "--------------------------------" << std::endl; + } + + std::cout << "8. Cleaning up" << std::endl; + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + + return 0; +} diff --git a/odbc/include/ydb_odbc.h b/odbc/include/ydb_odbc.h deleted file mode 100644 index 2415f80f55e..00000000000 --- a/odbc/include/ydb_odbc.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -// Структура для хранения информации о драйвере -typedef struct { - char name[256]; - char version[64]; - char description[1024]; -} YDB_DRIVER_INFO; - -// Структура для хранения состояния соединения -typedef struct { - void* ydb_driver; - void* query_client; - int connected; -} YDB_CONNECTION; - -// Структура для хранения состояния оператора -typedef struct { - YDB_CONNECTION* connection; - void* query_client; - void* result; - size_t current_row; -} YDB_STATEMENT; - -// Структура для хранения дескриптора -typedef struct { - void** descriptors; - size_t descriptors_size; -} YDB_DESCRIPTOR; - -// Функции драйвера -SQLRETURN YDB_SQLGetInfo(SQLSMALLINT InfoType, SQLPOINTER InfoValue, - SQLSMALLINT BufferLength, SQLSMALLINT* StringLength); - -SQLRETURN YDB_SQLConnect(SQLHDBC ConnectionHandle, SQLCHAR* ServerName, - SQLSMALLINT NameLength1, SQLCHAR* UserName, - SQLSMALLINT NameLength2, SQLCHAR* Authentication, - SQLSMALLINT NameLength3); - -SQLRETURN YDB_SQLDriverConnect(SQLHDBC ConnectionHandle, SQLHWND WindowHandle, - SQLCHAR* InConnectionString, SQLSMALLINT StringLength1, - SQLCHAR* OutConnectionString, SQLSMALLINT BufferLength, - SQLSMALLINT* StringLength2, SQLUSMALLINT DriverCompletion); - -// Функции соединения -SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle); - -SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, - SQLPOINTER InfoValue, SQLSMALLINT BufferLength, - SQLSMALLINT* StringLength); - -SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, - SQLHANDLE* OutputHandle); - -SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle); - -// Функции оператора -SQLRETURN YDB_SQLExecDirect(SQLHSTMT StatementHandle, SQLCHAR* StatementText, - SQLINTEGER TextLength); - -SQLRETURN YDB_SQLPrepare(SQLHSTMT StatementHandle, SQLCHAR* StatementText, - SQLINTEGER TextLength); - -SQLRETURN YDB_SQLExecute(SQLHSTMT StatementHandle); - -SQLRETURN YDB_SQLFetch(SQLHSTMT StatementHandle); - -SQLRETURN YDB_SQLCloseCursor(SQLHSTMT StatementHandle); - -// Функции дескриптора -SQLRETURN YDB_SQLGetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, - SQLSMALLINT FieldIdentifier, SQLPOINTER Value, - SQLINTEGER BufferLength, SQLINTEGER* StringLength); - -SQLRETURN YDB_SQLSetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, - SQLSMALLINT FieldIdentifier, SQLPOINTER Value, - SQLINTEGER BufferLength); - -#ifdef __cplusplus -} -#endif diff --git a/odbc/src/connection.c b/odbc/src/connection.c deleted file mode 100644 index d95dc8ade72..00000000000 --- a/odbc/src/connection.c +++ /dev/null @@ -1,132 +0,0 @@ -#include "ydb_odbc.h" -#include "client/driver.h" -#include "client/query.h" - -#include -#include - -SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle) { - YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; - if (!conn) { - return SQL_ERROR; - } - - if (conn->connected) { - if (conn->query_client) { - YDB_DestroyQueryClient(conn->query_client); - conn->query_client = NULL; - } - - if (conn->ydb_driver) { - YDB_DestroyDriver(conn->ydb_driver); - conn->ydb_driver = NULL; - } - - conn->connected = 0; - } - - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, - SQLPOINTER InfoValue, SQLSMALLINT BufferLength, - SQLSMALLINT* StringLength) { - YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; - if (!conn || !conn->connected) { - return SQL_ERROR; - } - - switch (InfoType) { - case SQL_DATABASE_NAME: - if (InfoValue && BufferLength > 0) { - const char* dbName = "YDB"; - strncpy((char*)InfoValue, dbName, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(dbName); - } - return SQL_SUCCESS; - } - break; - - case SQL_SERVER_NAME: - if (InfoValue && BufferLength > 0) { - const char* serverName = "Yandex Database"; - strncpy((char*)InfoValue, serverName, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(serverName); - } - return SQL_SUCCESS; - } - break; - } - - return SQL_ERROR; -} - -SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, - SQLHANDLE* OutputHandle) { - if (!OutputHandle) { - return SQL_ERROR; - } - - switch (HandleType) { - case SQL_HANDLE_DBC: - *OutputHandle = calloc(1, sizeof(YDB_CONNECTION)); - return SQL_SUCCESS; - - case SQL_HANDLE_STMT: - *OutputHandle = calloc(1, sizeof(YDB_STATEMENT)); - return SQL_SUCCESS; - - case SQL_HANDLE_DESC: - *OutputHandle = calloc(1, sizeof(YDB_DESCRIPTOR)); - return SQL_SUCCESS; - - default: - return SQL_ERROR; - } -} - -SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) { - if (!Handle) { - return SQL_ERROR; - } - - switch (HandleType) { - case SQL_HANDLE_DBC: - { - YDB_CONNECTION* conn = (YDB_CONNECTION*)Handle; - if (conn->connected) { - YDB_SQLDisconnect((SQLHDBC)conn); - } - free(conn); - } - return SQL_SUCCESS; - - case SQL_HANDLE_STMT: - { - YDB_STATEMENT* stmt = (YDB_STATEMENT*)Handle; - if (stmt->result) { - YDB_FreeExecuteQueryResult(stmt->result); - } - if (stmt->query_client) { - YDB_DestroyQueryClient(stmt->query_client); - } - free(stmt); - } - return SQL_SUCCESS; - - case SQL_HANDLE_DESC: - { - YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)Handle; - if (desc->descriptors) { - free(desc->descriptors); - } - free(desc); - } - return SQL_SUCCESS; - - default: - return SQL_ERROR; - } -} \ No newline at end of file diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp new file mode 100644 index 00000000000..427b03f7ba1 --- /dev/null +++ b/odbc/src/connection.cpp @@ -0,0 +1,119 @@ +#include "connection.h" +#include "statement.h" +#include +#include +#include +#include +#include + +#include + +namespace NYdb { +namespace NOdbc { + +SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { + // Парсим параметры + std::map params; + size_t pos = 0; + while (pos < connectionString.size()) { + size_t eq = connectionString.find('=', pos); + if (eq == std::string::npos) { + break; + } + + size_t sc = connectionString.find(';', eq); + std::string key = connectionString.substr(pos, eq-pos); + std::string val = connectionString.substr(eq+1, (sc == std::string::npos ? std::string::npos : sc-eq-1)); + params[key] = val; + if (sc == std::string::npos) { + break; + } + pos = sc+1; + } + Endpoint_ = params["Endpoint"]; + Database_ = params["Database"]; + + if (Endpoint_.empty() || Database_.empty()) { + AddError("08001", 0, "Missing Endpoint or Database in connection string"); + return SQL_ERROR; + } + + YdbDriver_ = std::make_unique(NYdb::TDriverConfig() + .SetEndpoint(Endpoint_) + .SetDatabase(Database_)); + + YdbClient_ = std::make_unique(*YdbDriver_); + + return SQL_SUCCESS; +} + +SQLRETURN TConnection::Connect(const std::string& serverName, + const std::string& userName, + const std::string& auth) { + // Получаем параметры из секции DSN через Driver Manager API + char endpoint[256] = {0}; + char database[256] = {0}; + + SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); + SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); + + Endpoint_ = endpoint; + Database_ = database; + + if (Endpoint_.empty() || Database_.empty()) { + AddError("08001", 0, "Missing Endpoint or Database in DSN"); + return SQL_ERROR; + } + + YdbDriver_ = std::make_unique(NYdb::TDriverConfig() + .SetEndpoint(Endpoint_) + .SetDatabase(Database_)); + + YdbClient_ = std::make_unique(*YdbDriver_); + + return SQL_SUCCESS; +} + +SQLRETURN TConnection::Disconnect() { + YdbClient_.reset(); + YdbDriver_.reset(); + return SQL_SUCCESS; +} + +SQLRETURN TConnection::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; + const auto& err = Errors_[recNumber-1]; + if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); + if (nativeError) *nativeError = err.NativeError; + if (messageText && bufferLength > 0) { + strncpy((char*)messageText, err.Message.c_str(), bufferLength); + if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } + return SQL_SUCCESS; +} + +std::unique_ptr TConnection::CreateStatement() { + return std::make_unique(this); +} + +void TConnection::RemoveStatement(TStatement* stmt) { + Statements_.erase(std::remove_if(Statements_.begin(), Statements_.end(), + [stmt](const std::unique_ptr& s) { return s.get() == stmt; }), Statements_.end()); +} + +void TConnection::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { + Errors_.push_back({sqlState, nativeError, message}); +} + +void TConnection::ClearErrors() { + Errors_.clear(); +} + +std::pair TConnection::ParseConnectionString(const std::string& connectionString) { + // Заглушка + return {"", ""}; +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/connection.h b/odbc/src/connection.h new file mode 100644 index 00000000000..cceadc2433f --- /dev/null +++ b/odbc/src/connection.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include "environment.h" + +namespace NYdb { +namespace NOdbc { + +class TStatement; + +class TConnection { +private: + std::unique_ptr YdbDriver_; + std::unique_ptr YdbClient_; + + TErrorList Errors_; + std::vector> Statements_; + std::string Endpoint_; + std::string Database_; + std::string AuthToken_; + +public: + SQLRETURN Connect(const std::string& serverName, + const std::string& userName, + const std::string& auth); + + SQLRETURN DriverConnect(const std::string& connectionString); + SQLRETURN Disconnect(); + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + + std::unique_ptr CreateStatement(); + void RemoveStatement(TStatement* stmt); + + NYdb::NQuery::TQueryClient* GetClient() { return YdbClient_.get(); } + + void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); + void ClearErrors(); + +private: + std::pair ParseConnectionString(const std::string& connectionString); +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/descriptor.c b/odbc/src/descriptor.c deleted file mode 100644 index 15380bf40aa..00000000000 --- a/odbc/src/descriptor.c +++ /dev/null @@ -1,109 +0,0 @@ -#include "ydb_odbc.h" -#include -#include - -// Структура для хранения поля дескриптора -typedef struct { - SQLSMALLINT field_identifier; - char value[1024]; -} YDB_DESCRIPTOR_FIELD; - -// Структура для хранения записи дескриптора -typedef struct { - YDB_DESCRIPTOR_FIELD* fields; - size_t fields_count; -} YDB_DESCRIPTOR_RECORD; - -SQLRETURN YDB_SQLGetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, - SQLSMALLINT FieldIdentifier, SQLPOINTER Value, - SQLINTEGER BufferLength, SQLINTEGER* StringLength) { - YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)DescriptorHandle; - if (!desc || !desc->descriptors || RecNumber < 1 || RecNumber > desc->descriptors_size) { - return SQL_ERROR; - } - - YDB_DESCRIPTOR_RECORD* record = (YDB_DESCRIPTOR_RECORD*)desc->descriptors[RecNumber - 1]; - if (!record) { - return SQL_ERROR; - } - - for (size_t i = 0; i < record->fields_count; i++) { - if (record->fields[i].field_identifier == FieldIdentifier) { - if (Value && BufferLength > 0) { - strncpy((char*)Value, record->fields[i].value, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(record->fields[i].value); - } - return SQL_SUCCESS; - } - break; - } - } - - return SQL_ERROR; -} - -SQLRETURN YDB_SQLSetDescField(SQLHDESC DescriptorHandle, SQLSMALLINT RecNumber, - SQLSMALLINT FieldIdentifier, SQLPOINTER Value, - SQLINTEGER BufferLength) { - YDB_DESCRIPTOR* desc = (YDB_DESCRIPTOR*)DescriptorHandle; - if (!desc || RecNumber < 1) { - return SQL_ERROR; - } - - // Увеличиваем размер массива дескрипторов, если нужно - if (RecNumber > desc->descriptors_size) { - void** new_descriptors = realloc(desc->descriptors, RecNumber * sizeof(void*)); - if (!new_descriptors) { - return SQL_ERROR; - } - - // Инициализируем новые записи - for (size_t i = desc->descriptors_size; i < RecNumber; i++) { - YDB_DESCRIPTOR_RECORD* record = calloc(1, sizeof(YDB_DESCRIPTOR_RECORD)); - if (!record) { - // Освобождаем память в случае ошибки - for (size_t j = desc->descriptors_size; j < i; j++) { - free(new_descriptors[j]); - } - free(new_descriptors); - return SQL_ERROR; - } - new_descriptors[i] = record; - } - - desc->descriptors = new_descriptors; - desc->descriptors_size = RecNumber; - } - - YDB_DESCRIPTOR_RECORD* record = (YDB_DESCRIPTOR_RECORD*)desc->descriptors[RecNumber - 1]; - if (!record) { - record = calloc(1, sizeof(YDB_DESCRIPTOR_RECORD)); - if (!record) { - return SQL_ERROR; - } - desc->descriptors[RecNumber - 1] = record; - } - - // Проверяем, существует ли уже поле с таким идентификатором - for (size_t i = 0; i < record->fields_count; i++) { - if (record->fields[i].field_identifier == FieldIdentifier) { - // Обновляем значение - strncpy(record->fields[i].value, (char*)Value, sizeof(record->fields[i].value) - 1); - return SQL_SUCCESS; - } - } - - // Добавляем новое поле - YDB_DESCRIPTOR_FIELD* new_fields = realloc(record->fields, (record->fields_count + 1) * sizeof(YDB_DESCRIPTOR_FIELD)); - if (!new_fields) { - return SQL_ERROR; - } - - record->fields = new_fields; - record->fields[record->fields_count].field_identifier = FieldIdentifier; - strncpy(record->fields[record->fields_count].value, (char*)Value, sizeof(record->fields[record->fields_count].value) - 1); - record->fields_count++; - - return SQL_SUCCESS; -} \ No newline at end of file diff --git a/odbc/src/driver.c b/odbc/src/driver.c deleted file mode 100644 index 5fc19f7294b..00000000000 --- a/odbc/src/driver.c +++ /dev/null @@ -1,142 +0,0 @@ -#include "ydb_odbc.h" -#include -#include - -// Глобальные переменные для хранения состояния -static YDB_DRIVER_INFO driver_info = { - .name = "YDB ODBC Driver", - .version = "1.0.0", - .description = "ODBC driver for Yandex Database" -}; - -SQLRETURN YDB_SQLGetInfo(SQLSMALLINT InfoType, SQLPOINTER InfoValue, - SQLSMALLINT BufferLength, SQLSMALLINT* StringLength) { - switch (InfoType) { - case SQL_DRIVER_NAME: - if (InfoValue && BufferLength > 0) { - strncpy(InfoValue, driver_info.name, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(driver_info.name); - } - return SQL_SUCCESS; - } - break; - - case SQL_DRIVER_VER: - if (InfoValue && BufferLength > 0) { - strncpy(InfoValue, driver_info.version, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(driver_info.version); - } - return SQL_SUCCESS; - } - break; - } - - return SQL_ERROR; -} - -SQLRETURN YDB_SQLConnect(SQLHDBC ConnectionHandle, SQLCHAR* ServerName, - SQLSMALLINT NameLength1, SQLCHAR* UserName, - SQLSMALLINT NameLength2, SQLCHAR* Authentication, - SQLSMALLINT NameLength3) { - YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; - if (!conn) { - return SQL_ERROR; - } - - // TODO: Реализовать подключение к YDB через C++ SDK - // Здесь нужно будет использовать C++ код через extern "C" функции - - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLDriverConnect(SQLHDBC ConnectionHandle, SQLHWND WindowHandle, - SQLCHAR* InConnectionString, SQLSMALLINT StringLength1, - SQLCHAR* OutConnectionString, SQLSMALLINT BufferLength, - SQLSMALLINT* StringLength2, SQLUSMALLINT DriverCompletion) { - // TODO: Реализовать парсинг строки подключения - return SQL_ERROR; -} - -SQLRETURN YDB_SQLDisconnect(SQLHDBC ConnectionHandle) { - YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; - if (!conn) { - return SQL_ERROR; - } - - // TODO: Реализовать отключение от YDB - - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLGetConnectionInfo(SQLHDBC ConnectionHandle, SQLSMALLINT InfoType, - SQLPOINTER InfoValue, SQLSMALLINT BufferLength, - SQLSMALLINT* StringLength) { - YDB_CONNECTION* conn = (YDB_CONNECTION*)ConnectionHandle; - if (!conn || !conn->connected) { - return SQL_ERROR; - } - - switch (InfoType) { - case SQL_DATABASE_NAME: - if (InfoValue && BufferLength > 0) { - const char* dbName = "YDB"; - strncpy(InfoValue, dbName, BufferLength - 1); - if (StringLength) { - *StringLength = strlen(dbName); - } - return SQL_SUCCESS; - } - break; - } - - return SQL_ERROR; -} - -SQLRETURN YDB_SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, - SQLHANDLE* OutputHandle) { - if (!OutputHandle) { - return SQL_ERROR; - } - - switch (HandleType) { - case SQL_HANDLE_DBC: - *OutputHandle = calloc(1, sizeof(YDB_CONNECTION)); - return SQL_SUCCESS; - - case SQL_HANDLE_STMT: - *OutputHandle = calloc(1, sizeof(YDB_STATEMENT)); - return SQL_SUCCESS; - - case SQL_HANDLE_DESC: - *OutputHandle = calloc(1, sizeof(YDB_DESCRIPTOR)); - return SQL_SUCCESS; - - default: - return SQL_ERROR; - } -} - -SQLRETURN YDB_SQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) { - if (!Handle) { - return SQL_ERROR; - } - - switch (HandleType) { - case SQL_HANDLE_DBC: - free(Handle); - return SQL_SUCCESS; - - case SQL_HANDLE_STMT: - free(Handle); - return SQL_SUCCESS; - - case SQL_HANDLE_DESC: - free(Handle); - return SQL_SUCCESS; - - default: - return SQL_ERROR; - } -} \ No newline at end of file diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp new file mode 100644 index 00000000000..0e1eef594d9 --- /dev/null +++ b/odbc/src/environment.cpp @@ -0,0 +1,38 @@ +#include "environment.h" +#include "connection.h" + +namespace NYdb { +namespace NOdbc { + +TEnvironment::TEnvironment() : OdbcVersion_(SQL_OV_ODBC3) {} +TEnvironment::~TEnvironment() {} + +SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { + // TODO: реализовать обработку атрибутов + OdbcVersion_ = attribute == SQL_ATTR_ODBC_VERSION ? reinterpret_cast(value) : 0; + return SQL_SUCCESS; +} + +SQLRETURN TEnvironment::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { + // Заглушка + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; + const auto& err = Errors_[recNumber-1]; + if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); + if (nativeError) *nativeError = err.NativeError; + if (messageText && bufferLength > 0) { + strncpy((char*)messageText, err.Message.c_str(), bufferLength); + if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } + return SQL_SUCCESS; +} + +void TEnvironment::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { + Errors_.push_back({sqlState, nativeError, message}); +} + +void TEnvironment::ClearErrors() { + Errors_.clear(); +} + +} // namespace NOdbc +} // namespace NYdb \ No newline at end of file diff --git a/odbc/src/environment.h b/odbc/src/environment.h new file mode 100644 index 00000000000..a45d7f0b7ee --- /dev/null +++ b/odbc/src/environment.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +class TConnection; + +struct TErrorInfo { + std::string SqlState; + SQLINTEGER NativeError; + std::string Message; +}; + +using TErrorList = std::vector; + +class TEnvironment { +private: + SQLINTEGER OdbcVersion_; + TErrorList Errors_; + +public: + TEnvironment(); + ~TEnvironment(); + + SQLRETURN SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength); + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + + void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); + void ClearErrors(); +}; + +} // namespace NOdbc +} // namespace NYdb \ No newline at end of file diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp new file mode 100644 index 00000000000..6f3b865035f --- /dev/null +++ b/odbc/src/odbc_driver.cpp @@ -0,0 +1,253 @@ +#include "environment.h" +#include "connection.h" +#include "statement.h" + +#include +#include + +namespace { + std::string GetString(SQLCHAR* str, SQLSMALLINT length) { + if (length == SQL_NTS) { + return std::string(reinterpret_cast(str)); + } + return std::string(reinterpret_cast(str), length); + } +} + +extern "C" { + +SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, + SQLHANDLE inputHandle, + SQLHANDLE* outputHandle) { + if (!outputHandle) { + return SQL_INVALID_HANDLE; + } + + try { + switch (handleType) { + case SQL_HANDLE_ENV: { + if (inputHandle != SQL_NULL_HANDLE) { + return SQL_INVALID_HANDLE; + } + + *outputHandle = new NYdb::NOdbc::TEnvironment(); + return SQL_SUCCESS; + } + + case SQL_HANDLE_DBC: { + if (!inputHandle) { + return SQL_INVALID_HANDLE; + } + + *outputHandle = new NYdb::NOdbc::TConnection(); + return SQL_SUCCESS; + } + + case SQL_HANDLE_STMT: { + auto conn = static_cast(inputHandle); + if (!conn) { + return SQL_INVALID_HANDLE; + } + auto stmt = conn->CreateStatement(); + *outputHandle = stmt.release(); + return SQL_SUCCESS; + } + + default: + return SQL_ERROR; + } + } catch (...) { + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { + if (!handle) { + return SQL_INVALID_HANDLE; + } + + try { + switch (handleType) { + case SQL_HANDLE_ENV: { + auto env = static_cast(handle); + delete env; + return SQL_SUCCESS; + } + + case SQL_HANDLE_DBC: { + auto conn = static_cast(handle); + delete conn; + return SQL_SUCCESS; + } + + case SQL_HANDLE_STMT: { + auto stmt = static_cast(handle); + if (stmt->GetConnection()) { + stmt->GetConnection()->RemoveStatement(stmt); + } + delete stmt; + return SQL_SUCCESS; + } + + default: + return SQL_ERROR; + } + } catch (...) { + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV environmentHandle, + SQLINTEGER attribute, + SQLPOINTER value, + SQLINTEGER stringLength) { + auto env = static_cast(environmentHandle); + if (!env) { + return SQL_INVALID_HANDLE; + } + + return env->SetAttribute(attribute, value, stringLength); +} + +SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, + SQLHWND /*WindowHandle*/, + SQLCHAR* inConnectionString, + SQLSMALLINT stringLength1, + SQLCHAR* /*outConnectionString*/, + SQLSMALLINT /*bufferLength*/, + SQLSMALLINT* /*stringLength2Ptr*/, + SQLUSMALLINT /*driverCompletion*/) { + auto conn = static_cast(connectionHandle); + if (!conn) { + return SQL_INVALID_HANDLE; + } + + return conn->DriverConnect(GetString(inConnectionString, stringLength1)); +} + +SQLRETURN SQL_API SQLConnect(SQLHDBC connectionHandle, + SQLCHAR* serverName, SQLSMALLINT nameLength1, + SQLCHAR* userName, SQLSMALLINT nameLength2, + SQLCHAR* authentication, SQLSMALLINT nameLength3) { + auto conn = static_cast(connectionHandle); + if (!conn) { + return SQL_INVALID_HANDLE; + } + + return conn->Connect(GetString(serverName, nameLength1), + GetString(userName, nameLength2), + GetString(authentication, nameLength3)); +} + +SQLRETURN SQL_API SQLDisconnect(SQLHDBC connectionHandle) { + auto conn = static_cast(connectionHandle); + if (!conn) { + return SQL_INVALID_HANDLE; + } + + return conn->Disconnect(); +} + +SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, + SQLCHAR* statementText, + SQLINTEGER textLength) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + + return stmt->ExecDirect(GetString(statementText, textLength)); +} + +SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + + return stmt->Fetch(); +} + +SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, + SQLUSMALLINT columnNumber, + SQLSMALLINT targetType, + SQLPOINTER targetValue, + SQLLEN bufferLength, + SQLLEN* strLenOrInd) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + + return stmt->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); +} + +SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, + SQLUSMALLINT columnNumber, + SQLSMALLINT targetType, + SQLPOINTER targetValue, + SQLLEN bufferLength, + SQLLEN* strLenOrInd) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->BindCol(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); +} + +SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, + SQLHANDLE handle, + SQLSMALLINT recNumber, + SQLCHAR* sqlState, + SQLINTEGER* nativeError, + SQLCHAR* messageText, + SQLSMALLINT bufferLength, + SQLSMALLINT* textLength) { + if (!handle) { + return SQL_INVALID_HANDLE; + } + + try { + switch (handleType) { + case SQL_HANDLE_ENV: { + auto env = static_cast(handle); + return env->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); + } + + case SQL_HANDLE_DBC: { + auto conn = static_cast(handle); + return conn->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); + } + + case SQL_HANDLE_STMT: { + auto stmt = static_cast(handle); + return stmt->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); + } + + default: + return SQL_ERROR; + } + } catch (...) { + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLBindParameter(SQLHSTMT statementHandle, + SQLUSMALLINT paramNumber, + SQLSMALLINT inputOutputType, + SQLSMALLINT valueType, + SQLSMALLINT parameterType, + SQLULEN columnSize, + SQLSMALLINT decimalDigits, + SQLPOINTER parameterValuePtr, + SQLLEN bufferLength, + SQLLEN* strLenOrIndPtr) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + + return stmt->BindParameter(paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr); +} + +} diff --git a/odbc/src/statement.c b/odbc/src/statement.c deleted file mode 100644 index b9a7a5ffcd7..00000000000 --- a/odbc/src/statement.c +++ /dev/null @@ -1,98 +0,0 @@ -#include "ydb_odbc.h" -#include "client/query.h" -#include -#include - -SQLRETURN YDB_SQLExecDirect(SQLHSTMT StatementHandle, SQLCHAR* StatementText, - SQLINTEGER TextLength) { - YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; - if (!stmt || !stmt->connection || !stmt->connection->connected || !StatementText) { - return SQL_ERROR; - } - - // Если текст запроса не указан, используем длину строки - if (TextLength == SQL_NTS) { - TextLength = strlen((char*)StatementText); - } - - // Создаем клиент запросов, если его еще нет - if (!stmt->query_client) { - stmt->query_client = YDB_CreateQueryClient(stmt->connection->ydb_driver); - if (!stmt->query_client) { - return SQL_ERROR; - } - } - - // Освобождаем предыдущий результат, если он есть - if (stmt->result) { - YDB_FreeExecuteQueryResult(stmt->result); - stmt->result = NULL; - } - - // Выполняем запрос - if (!YDB_ExecuteQuery(stmt->query_client, (char*)StatementText, &stmt->result)) { - return SQL_ERROR; - } - - stmt->current_row = 0; - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLPrepare(SQLHSTMT StatementHandle, SQLCHAR* StatementText, - SQLINTEGER TextLength) { - // YDB не требует предварительной подготовки запросов - // Просто сохраняем текст запроса для последующего выполнения - YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; - if (!stmt || !StatementText) { - return SQL_ERROR; - } - - // Если текст запроса не указан, используем длину строки - if (TextLength == SQL_NTS) { - TextLength = strlen((char*)StatementText); - } - - // TODO: Сохранить текст запроса для последующего выполнения - - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLExecute(SQLHSTMT StatementHandle) { - // В YDB все запросы выполняются сразу - // Эта функция просто вызывает SQLExecDirect с сохраненным текстом запроса - YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; - if (!stmt) { - return SQL_ERROR; - } - - // TODO: Выполнить сохраненный запрос - - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLFetch(SQLHSTMT StatementHandle) { - YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; - if (!stmt || !stmt->result) { - return SQL_ERROR; - } - - // TODO: Преобразовать данные текущей строки в формат ODBC - - stmt->current_row++; - return SQL_SUCCESS; -} - -SQLRETURN YDB_SQLCloseCursor(SQLHSTMT StatementHandle) { - YDB_STATEMENT* stmt = (YDB_STATEMENT*)StatementHandle; - if (!stmt) { - return SQL_ERROR; - } - - if (stmt->result) { - YDB_FreeExecuteQueryResult(stmt->result); - stmt->result = NULL; - } - - stmt->current_row = 0; - return SQL_SUCCESS; -} \ No newline at end of file diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp new file mode 100644 index 00000000000..224cc8dc122 --- /dev/null +++ b/odbc/src/statement.cpp @@ -0,0 +1,342 @@ +#include "statement.h" + +#include +#include +#include + +namespace NYdb { +namespace NOdbc { + +TStatement::TStatement(TConnection* conn) + : Conn_(conn) {} + +SQLRETURN TStatement::ExecDirect(const std::string& statementText) { + ClearStatement(); + + auto* client = Conn_->GetClient(); + if (!client) { + return SQL_ERROR; + } + + NYdb::TParams params = BuildParams(); + if (!Errors_.empty()) { + return SQL_ERROR; + } + // --- конец сборки параметров --- + + auto sessionResult = client->GetSession().ExtractValueSync(); + if (!sessionResult.IsSuccess()) { + return SQL_ERROR; + } + + auto session = sessionResult.GetSession(); + + auto iterator = session.StreamExecuteQuery(statementText, NYdb::NQuery::TTxControl::NoTx(), params).ExtractValueSync(); + if (!iterator.IsSuccess()) { + return SQL_ERROR; + } + + Iterator_ = std::make_unique(std::move(iterator)); + + return SQL_SUCCESS; +} + +SQLRETURN TStatement::Fetch() { + if (!Iterator_) { + ClearStatement(); + return SQL_NO_DATA; + } + + while (true) { + if (ResultSetParser_) { + if (ResultSetParser_->TryNextRow()) { + // Автоматически заполняем связанные буферы + for (const auto& col : BoundColumns_) { + GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + } + return SQL_SUCCESS; + } + + ResultSetParser_.reset(); + } + + auto part = Iterator_->ReadNext().ExtractValueSync(); + if (part.EOS()) { + ClearStatement(); + return SQL_NO_DATA; + } + + if (!part.IsSuccess()) { + // AddError(part.GetStatus().GetStatus().GetCode(), part.GetStatus().GetStatus().GetReason()); + ClearStatement(); + return SQL_ERROR; + } + + if (part.HasResultSet()) { + ResultSetParser_ = std::make_unique(part.ExtractResultSet()); + } + } + + return SQL_SUCCESS; +} + +SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { + if (!ResultSetParser_) { + return SQL_NO_DATA; + } + + if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { + return SQL_ERROR; + } + + return ConvertYdbValue(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); +} + +SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; + const auto& err = Errors_[recNumber-1]; + if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); + if (nativeError) *nativeError = err.NativeError; + if (messageText && bufferLength > 0) { + strncpy((char*)messageText, err.Message.c_str(), bufferLength); + if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } + return SQL_SUCCESS; +} + +SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { + // Удаляем старую связь для этой колонки, если есть + BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), + [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); + // Если targetValue == nullptr, просто удаляем связь + if (!targetValue) { + return SQL_SUCCESS; + } + BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, + SQLSMALLINT inputOutputType, + SQLSMALLINT valueType, + SQLSMALLINT parameterType, + SQLULEN columnSize, + SQLSMALLINT decimalDigits, + SQLPOINTER parameterValuePtr, + SQLLEN bufferLength, + SQLLEN* strLenOrIndPtr) { + if (inputOutputType != SQL_PARAM_INPUT) { + AddError("HYC00", 0, "Only input parameters are supported"); + return SQL_ERROR; + } + // Удаляем старую связь для этого параметра, если есть + BoundParams_.erase(std::remove_if(BoundParams_.begin(), BoundParams_.end(), + [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }), BoundParams_.end()); + // Если parameterValuePtr == nullptr, просто удаляем связь + if (!parameterValuePtr) { + return SQL_SUCCESS; + } + BoundParams_.push_back({paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr}); + return SQL_SUCCESS; +} + +void TStatement::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { + Errors_.push_back({sqlState, nativeError, message}); +} + +void TStatement::ClearErrors() { + Errors_.clear(); +} + +void TStatement::ClearStatement() { + Iterator_.reset(); + ResultSetParser_.reset(); + BoundColumns_.clear(); +} + +SQLRETURN TStatement::ConvertYdbValue(NYdb::TValueParser& valueParser, + SQLSMALLINT targetType, + SQLPOINTER targetValue, + SQLLEN bufferLength, + SQLLEN* strLenOrInd) { + + // 1. Проверка на NULL + if (valueParser.IsNull()) { + if (strLenOrInd) *strLenOrInd = SQL_NULL_DATA; + return SQL_SUCCESS; + } + + if (valueParser.GetKind() == TTypeParser::ETypeKind::Optional) { + valueParser.OpenOptional(); + SQLRETURN ret = ConvertYdbValue(valueParser, targetType, targetValue, bufferLength, strLenOrInd); + valueParser.CloseOptional(); + return ret; + } + + if (valueParser.GetKind() != TTypeParser::ETypeKind::Primitive) { + return SQL_ERROR; + } + + EPrimitiveType ydbType = valueParser.GetPrimitiveType(); + + switch (targetType) { + case SQL_C_SLONG: + { + int32_t v = 0; + switch (ydbType) { + case EPrimitiveType::Int32: v = valueParser.GetInt32(); break; + case EPrimitiveType::Uint32: v = static_cast(valueParser.GetUint32()); break; + case EPrimitiveType::Int64: v = static_cast(valueParser.GetInt64()); break; + case EPrimitiveType::Uint64: v = static_cast(valueParser.GetUint64()); break; + case EPrimitiveType::Bool: v = valueParser.GetBool() ? 1 : 0; break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(int32_t); + return SQL_SUCCESS; + } + case SQL_C_SBIGINT: + { + SQLBIGINT v = 0; + switch (ydbType) { + case EPrimitiveType::Int64: v = valueParser.GetInt64(); break; + case EPrimitiveType::Uint64: v = static_cast(valueParser.GetUint64()); break; + case EPrimitiveType::Int32: v = static_cast(valueParser.GetInt32()); break; + case EPrimitiveType::Uint32: v = static_cast(valueParser.GetUint32()); break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(SQLBIGINT); + return SQL_SUCCESS; + } + case SQL_C_DOUBLE: + { + double v = 0.0; + switch (ydbType) { + case EPrimitiveType::Double: v = valueParser.GetDouble(); break; + case EPrimitiveType::Float: v = valueParser.GetFloat(); break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(double); + return SQL_SUCCESS; + } + case SQL_C_CHAR: + { + std::string str; + switch (ydbType) { + case EPrimitiveType::Utf8: str = valueParser.GetUtf8(); break; + case EPrimitiveType::String: str = valueParser.GetString(); break; + case EPrimitiveType::Json: str = valueParser.GetJson(); break; + case EPrimitiveType::JsonDocument: str = valueParser.GetJsonDocument(); break; + default: return SQL_ERROR; + } + SQLLEN len = str.size(); + if (targetValue && bufferLength > 0) { + SQLLEN copyLen = std::min(len, bufferLength - 1); + memcpy(targetValue, str.data(), copyLen); + reinterpret_cast(targetValue)[copyLen] = 0; + } + if (strLenOrInd) *strLenOrInd = len; + return SQL_SUCCESS; + } + case SQL_C_BIT: + { + char v = valueParser.GetBool() ? 1 : 0; + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(char); + return SQL_SUCCESS; + } + // Добавьте обработку дат/времени, бинарных данных и других типов по необходимости + default: + return SQL_ERROR; + } +} + +NYdb::TParams TStatement::BuildParams() { + Errors_.clear(); + NYdb::TParamsBuilder paramsBuilder; + for (const auto& param : BoundParams_) { + std::string paramName = "$p" + std::to_string(param.ParamNumber); // ODBC нумерует с 1 + auto& builder = paramsBuilder.AddParam(paramName); + // Обработка NULL + if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { + builder.EmptyOptional(); + builder.Build(); + continue; + } + + switch (param.ValueType) { + case SQL_C_SLONG: { + auto value = *static_cast(param.ParameterValuePtr); + switch (param.ParameterType) { + case SQL_INTEGER: + builder.Int32(static_cast(value)); + break; + case SQL_BIGINT: + builder.Int64(static_cast(value)); + break; + case SQL_DOUBLE: + builder.Double(static_cast(value)); + break; + case SQL_FLOAT: + builder.Float(static_cast(value)); + break; + case SQL_VARCHAR: + case SQL_CHAR: + case SQL_LONGVARCHAR: + builder.Utf8(std::to_string(value)); + break; + case SQL_BIT: + builder.Uint8(static_cast(value)); + break; + default: + AddError("07006", 0, "Unsupported SQL type"); + return paramsBuilder.Build(); + } + break; + } + case SQL_C_SBIGINT: { + auto v = *static_cast(param.ParameterValuePtr); + builder.Int32(static_cast(v)); + break; + } + default: { + AddError("07006", 0, "Unsupported C type"); + return paramsBuilder.Build(); + } + } + + switch (param.ParameterType) { + case SQL_INTEGER: + case SQL_BIGINT: + break; + case SQL_DOUBLE: + builder.Double(*reinterpret_cast(param.ParameterValuePtr)); + break; + case SQL_FLOAT: + builder.Double(*reinterpret_cast(param.ParameterValuePtr)); + break; + case SQL_VARCHAR: + case SQL_CHAR: + case SQL_LONGVARCHAR: + builder.Utf8(*reinterpret_cast(param.ParameterValuePtr)); + break; + case SQL_BIT: + builder.Bool(*reinterpret_cast(param.ParameterValuePtr)); + break; + default: + AddError("07006", 0, "Unsupported SQL type"); + return paramsBuilder.Build(); + } + + builder.Build(); + } + + return paramsBuilder.Build(); +} + +} // namespace NOdbc +} // namespace NYdb \ No newline at end of file diff --git a/odbc/src/statement.h b/odbc/src/statement.h new file mode 100644 index 00000000000..f34e0e771e4 --- /dev/null +++ b/odbc/src/statement.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "connection.h" + +namespace NYdb { +namespace NOdbc { + +class TStatement { +private: + TConnection* Conn_; + TErrorList Errors_; + std::unique_ptr Iterator_; + std::unique_ptr ResultSetParser_; + + struct TBoundColumn { + SQLUSMALLINT ColumnNumber; + SQLSMALLINT TargetType; + SQLPOINTER TargetValue; + SQLLEN BufferLength; + SQLLEN* StrLenOrInd; + }; + std::vector BoundColumns_; + + struct TBoundParam { + SQLUSMALLINT ParamNumber; + SQLSMALLINT InputOutputType; + SQLSMALLINT ValueType; + SQLSMALLINT ParameterType; + SQLULEN ColumnSize; + SQLSMALLINT DecimalDigits; + SQLPOINTER ParameterValuePtr; + SQLLEN BufferLength; + SQLLEN* StrLenOrIndPtr; + }; + std::vector BoundParams_; + +public: + TStatement(TConnection* conn); + + SQLRETURN ExecDirect(const std::string& statementText); + SQLRETURN Fetch(); + SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); + SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); + + TConnection* GetConnection() { return Conn_; } + + void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); + void ClearErrors(); + + NYdb::TParams BuildParams(); + +private: + void ClearStatement(); + + SQLRETURN ConvertYdbValue(NYdb::TValueParser& valueParser, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); +}; + +} // namespace NOdbc +} // namespace NYdb \ No newline at end of file From f9a08a5c4569843bf163e794bccd0371da6cf890 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Fri, 16 May 2025 14:46:04 +0000 Subject: [PATCH 04/55] Revert C API --- CMakeLists.txt | 5 +- c_api/CMakeLists.txt | 20 -- c_api/README.md | 51 ----- c_api/include/ydb-cpp-sdk/c_api/driver.h | 47 ---- c_api/include/ydb-cpp-sdk/c_api/query.h | 39 ---- c_api/include/ydb-cpp-sdk/c_api/result.h | 32 --- c_api/include/ydb-cpp-sdk/c_api/value.h | 75 ------ c_api/src/driver.cpp | 172 -------------- c_api/src/impl/driver_impl.h | 22 -- c_api/src/impl/result_impl.h | 14 -- c_api/src/impl/value_impl.h | 15 -- c_api/src/query.cpp | 140 ------------ c_api/src/result.cpp | 95 -------- c_api/src/value.cpp | 276 ----------------------- examples/CMakeLists.txt | 1 - examples/c_api/CMakeLists.txt | 5 - examples/c_api/main.c | 37 --- 17 files changed, 2 insertions(+), 1044 deletions(-) delete mode 100644 c_api/CMakeLists.txt delete mode 100644 c_api/README.md delete mode 100644 c_api/include/ydb-cpp-sdk/c_api/driver.h delete mode 100644 c_api/include/ydb-cpp-sdk/c_api/query.h delete mode 100644 c_api/include/ydb-cpp-sdk/c_api/result.h delete mode 100644 c_api/include/ydb-cpp-sdk/c_api/value.h delete mode 100644 c_api/src/driver.cpp delete mode 100644 c_api/src/impl/driver_impl.h delete mode 100644 c_api/src/impl/result_impl.h delete mode 100644 c_api/src/impl/value_impl.h delete mode 100644 c_api/src/query.cpp delete mode 100644 c_api/src/result.cpp delete mode 100644 c_api/src/value.cpp delete mode 100644 examples/c_api/CMakeLists.txt delete mode 100644 examples/c_api/main.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cdfaccea56..fc21d66c269 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ string(REGEX MATCH "YDB_SDK_VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ ${YDB_SD set(YDB_SDK_VERSION ${CMAKE_MATCH_1}) message(STATUS "YDB C++ SDK version: ${YDB_SDK_VERSION}") -project(ydb-cpp-sdk VERSION ${YDB_SDK_VERSION} LANGUAGES C CXX ASM) +project(YDB-CPP-SDK VERSION ${YDB_SDK_VERSION} LANGUAGES C CXX ASM) option(YDB_SDK_INSTALL "Install YDB C++ SDK" Off) option(YDB_SDK_TESTS "Build YDB C++ SDK tests" Off) @@ -83,7 +83,6 @@ add_subdirectory(include/ydb-cpp-sdk/client) add_subdirectory(src) add_subdirectory(util) add_subdirectory(plugins) -add_subdirectory(c_api) #_ydb_sdk_validate_public_headers() @@ -185,7 +184,7 @@ if (YDB_SDK_INSTALL) install(EXPORT ydb-cpp-sdk-core-targets FILE ydb-cpp-sdk-core-targets.cmake CONFIGURATIONS RELEASE - NAMESPACE ydb-cpp-sdk:: + NAMESPACE YDB-CPP-SDK:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ydb-cpp-sdk/release COMPONENT libydb-cpp ) diff --git a/c_api/CMakeLists.txt b/c_api/CMakeLists.txt deleted file mode 100644 index b2c8fec22a4..00000000000 --- a/c_api/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -_ydb_sdk_add_library(ydb-c-api SHARED) - -target_sources(ydb-c-api PRIVATE - src/driver.cpp - src/query.cpp - src/result.cpp - src/value.cpp -) - -target_include_directories(ydb-c-api PUBLIC include) - -target_link_libraries(ydb-c-api - PRIVATE - yutil - ydb-cpp-sdk::Query - ydb-cpp-sdk::Table - ydb-cpp-sdk::Driver -) - -add_library(ydb-cpp-sdk::c-api ALIAS ydb-c-api) diff --git a/c_api/README.md b/c_api/README.md deleted file mode 100644 index b7f929c5538..00000000000 --- a/c_api/README.md +++ /dev/null @@ -1,51 +0,0 @@ -Синхронный API Асинхронный API - -libpq: -```c -PGconn *conn = PQconnectdb("..."); -// Блокирует выполнение до завершения -``` - -libpq: -```c -PGconn *conn = PQconnectStart("..."); -do { - pollstatus = PQconnectPoll(conn); - // Ожидание событий -} while (pollstatus != PGRES_POLLING_OK); -``` - -MySQL: -
MYSQL *conn = mysql_init(NULL); -
mysql_real_connect(conn, ...); - -MySQL: -
status = mysql_real_connect_nonblocking(mysql, ...); -
while (status == NET_ASYNC_NOT_READY) { -
// Обработка других задач
- status = mysql_real_connect_nonblocking(...);
-} - -Выполнение запросов - -Синхронный API Асинхронный API - -libpq:
PGresult *res = PQexec(conn, "SELECT ...");
Ожидает завершения выполнения - -libpq:
PQsendQuery(conn, "SELECT ...");
// Можно выполнять другую работу
while ((res = PQgetResult(conn)) != NULL) {
// Обработка результатов
} - -MySQL:
mysql_query(conn, "SELECT ...");
result = mysql_store_result(conn); - -MySQL:
status = mysql_real_query_nonblocking(mysql, "...");
// Проверка status и ожидание
status = mysql_store_result_nonblocking(mysql, &result); - -Обработка ошибок - -Синхронный API Асинхронный API - -libpq:
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "%s", PQerrorMessage(conn));
} - -libpq:
Такая же проверка, но в каждом шаге асинхронного процесса:
if (pollstatus == PGRES_POLLING_FAILED) {
fprintf(stderr, "%s", PQerrorMessage(conn));
} - -MySQL:
if (mysql_query(conn, query)) {
fprintf(stderr, "%s", mysql_error(conn));
} - -MySQL:
if (status == NET_ASYNC_ERROR) {
fprintf(stderr, "%s", mysql_error(mysql));
} \ No newline at end of file diff --git a/c_api/include/ydb-cpp-sdk/c_api/driver.h b/c_api/include/ydb-cpp-sdk/c_api/driver.h deleted file mode 100644 index 557994675f0..00000000000 --- a/c_api/include/ydb-cpp-sdk/c_api/driver.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct TYdbDriverConfigImpl TYdbDriverConfig; -typedef struct TYdbDriverImpl TYdbDriver; - -typedef enum { - YDB_DRIVER_CONFIG_OK, - YDB_DRIVER_CONFIG_INVALID, -} EYdbDriverConfigStatus; - -typedef enum { - YDB_DRIVER_OK, - YDB_DRIVER_ERROR, -} EYdbDriverStatus; - -// Создание и уничтожение конфигурации -TYdbDriverConfig* YdbCreateDriverConfig(const char* connectionString); -void YdbDestroyDriverConfig(TYdbDriverConfig* config); - -// Установка параметров конфигурации -TYdbDriverConfig* YdbSetEndpoint(TYdbDriverConfig* config, const char* endpoint); -TYdbDriverConfig* YdbSetDatabase(TYdbDriverConfig* config, const char* database); -TYdbDriverConfig* YdbSetAuthToken(TYdbDriverConfig* config, const char* token); -TYdbDriverConfig* YdbSetSecureConnection(TYdbDriverConfig* config, const char* cert); - -// Получение результата конфигурации -EYdbDriverConfigStatus YdbGetDriverConfigStatus(TYdbDriverConfig* config); -const char* YdbGetDriverConfigErrorMessage(TYdbDriverConfig* config); - -// Создание и уничтожение драйвера -TYdbDriver* YdbCreateDriver(const char* connectionString); -TYdbDriver* YdbCreateDriverFromConfig(TYdbDriverConfig* config); -void YdbDestroyDriver(TYdbDriver* driver); - -// Получение результата драйвера -EYdbDriverStatus YdbGetDriverStatus(TYdbDriver* driver); -const char* YdbGetDriverErrorMessage(TYdbDriver* driver); - -#ifdef __cplusplus -} -#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/query.h b/c_api/include/ydb-cpp-sdk/c_api/query.h deleted file mode 100644 index e3bd3918ac6..00000000000 --- a/c_api/include/ydb-cpp-sdk/c_api/query.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -#include "driver.h" -#include "result.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct TYdbQueryClientImpl TYdbQueryClient; -typedef struct TYdbQueryResultImpl TYdbQueryResult; - -typedef enum { - YDB_QUERY_CLIENT_OK, - YDB_QUERY_CLIENT_ERROR, -} EYdbQueryClientError; - -typedef enum { - YDB_QUERY_RESULT_OK, - YDB_QUERY_RESULT_ERROR, -} EYdbQueryResultError; - -// Создание и уничтожение клиента запросов -TYdbQueryClient* YdbCreateQueryClient(TYdbDriver* driver); -void YdbDestroyQueryClient(TYdbQueryClient* queryClient); - -// Выполнение запроса -TYdbQueryResult* YdbExecuteQuery(TYdbQueryClient* queryClient, const char* query); -void YdbDestroyQueryResult(TYdbQueryResult* result); - -// Получение результата запроса -int YdbGetQueryResultSetsCount(TYdbQueryResult* result); -TYdbResultSet* YdbGetQueryResultSet(TYdbQueryResult* result, size_t index); - -#ifdef __cplusplus -} -#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/result.h b/c_api/include/ydb-cpp-sdk/c_api/result.h deleted file mode 100644 index fc762ccf864..00000000000 --- a/c_api/include/ydb-cpp-sdk/c_api/result.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "value.h" - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct TYdbResultSetImpl TYdbResultSet; - -typedef enum { - YDB_RESULT_SET_OK, - YDB_RESULT_SET_ERROR, -} EYdbResultSetStatus; - -int YdbGetColumnsCount(TYdbResultSet* resultSet); -int YdbGetRowsCount(TYdbResultSet* resultSet); -int YdbIsTruncated(TYdbResultSet* resultSet); - -const char* YdbGetColumnName(TYdbResultSet* resultSet, size_t index); -int YdbGetColumnIndex(TYdbResultSet* resultSet, const char* name); - -TYdbValue* YdbGetValue(TYdbResultSet* resultSet, size_t rowIndex, const char* name); -TYdbValue* YdbGetValueByIndex(TYdbResultSet* resultSet, size_t rowIndex, size_t columnIndex); - -void YdbDestroyResultSet(TYdbResultSet* resultSet); - -#ifdef __cplusplus -} -#endif diff --git a/c_api/include/ydb-cpp-sdk/c_api/value.h b/c_api/include/ydb-cpp-sdk/c_api/value.h deleted file mode 100644 index 5f0ad2caa11..00000000000 --- a/c_api/include/ydb-cpp-sdk/c_api/value.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct TYdbValueImpl TYdbValue; -typedef struct TYdbParamsImpl TYdbParams; - -typedef enum { - YDB_VALUE_OK, - YDB_VALUE_ERROR, -} EYdbValueStatus; - -typedef enum { - YDB_TYPE_KIND_UNDEFINED, - YDB_TYPE_KIND_PRIMITIVE, - YDB_TYPE_KIND_OPTIONAL, - YDB_TYPE_KIND_LIST, - YDB_TYPE_KIND_TUPLE, - YDB_TYPE_KIND_STRUCT, - YDB_TYPE_KIND_DICT, - YDB_TYPE_KIND_VARIANT, -} EYdbTypeKind; - -typedef enum { - YDB_PRIMITIVE_TYPE_UNDEFINED, - YDB_PRIMITIVE_TYPE_BOOL, - YDB_PRIMITIVE_TYPE_INT8, - YDB_PRIMITIVE_TYPE_UINT8, - YDB_PRIMITIVE_TYPE_INT16, - YDB_PRIMITIVE_TYPE_UINT16, - YDB_PRIMITIVE_TYPE_INT32, - YDB_PRIMITIVE_TYPE_UINT32, - YDB_PRIMITIVE_TYPE_INT64, - YDB_PRIMITIVE_TYPE_UINT64, - YDB_PRIMITIVE_TYPE_FLOAT, - YDB_PRIMITIVE_TYPE_DOUBLE, - YDB_PRIMITIVE_TYPE_STRING, - YDB_PRIMITIVE_TYPE_UTF8, - YDB_PRIMITIVE_TYPE_YSON, - YDB_PRIMITIVE_TYPE_JSON, - YDB_PRIMITIVE_TYPE_JSON_DOCUMENT, - YDB_PRIMITIVE_TYPE_DYNUMBER, -} EYdbPrimitiveType; - -EYdbTypeKind YdbGetTypeKind(TYdbValue* value); -EYdbPrimitiveType YdbGetPrimitiveType(TYdbValue* value); - -EYdbValueStatus YdbGetBool(TYdbValue* value, bool* result); -EYdbValueStatus YdbGetInt8(TYdbValue* value, int8_t* result); -EYdbValueStatus YdbGetUint8(TYdbValue* value, uint8_t* result); -EYdbValueStatus YdbGetInt16(TYdbValue* value, int16_t* result); -EYdbValueStatus YdbGetUint16(TYdbValue* value, uint16_t* result); -EYdbValueStatus YdbGetInt32(TYdbValue* value, int32_t* result); -EYdbValueStatus YdbGetUint32(TYdbValue* value, uint32_t* result); -EYdbValueStatus YdbGetInt64(TYdbValue* value, int64_t* result); -EYdbValueStatus YdbGetUint64(TYdbValue* value, uint64_t* result); -EYdbValueStatus YdbGetFloat(TYdbValue* value, float* result); -EYdbValueStatus YdbGetDouble(TYdbValue* value, double* result); -EYdbValueStatus YdbGetString(TYdbValue* value, char** result); -EYdbValueStatus YdbGetUtf8(TYdbValue* value, char** result); -EYdbValueStatus YdbGetYson(TYdbValue* value, char** result); -EYdbValueStatus YdbGetJson(TYdbValue* value, char** result); -EYdbValueStatus YdbGetJsonDocument(TYdbValue* value, char** result); -EYdbValueStatus YdbGetDyNumber(TYdbValue* value, char** result); - -void YdbDestroyValue(TYdbValue* value); - -#ifdef __cplusplus -} -#endif diff --git a/c_api/src/driver.cpp b/c_api/src/driver.cpp deleted file mode 100644 index 27e1093d68e..00000000000 --- a/c_api/src/driver.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include - -#include "impl/driver_impl.h" // NOLINT - -#include - -extern "C" { - -// Создание и уничтожение конфигурации -TYdbDriverConfig* YdbCreateDriverConfig(const char* connectionString) { - try { - if (!connectionString) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config pointer"}; - } - - try { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_OK, "", NYdb::TDriverConfig(connectionString)}; - } catch (const std::exception& e) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; - } - } catch (const std::exception& e) { - return nullptr; - } -} - -void YdbDestroyDriverConfig(TYdbDriverConfig* config) { - if (config) { - delete config; - } -} - -// Установка параметров конфигурации -TYdbDriverConfig* YdbSetEndpoint(TYdbDriverConfig* config, const char* endpoint) { - try { - if (!config) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; - } - if (!endpoint) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid endpoint"}; - } - - try { - config->config.SetEndpoint(std::string(endpoint)); - return config; - } catch (const std::exception& e) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -TYdbDriverConfig* YdbSetDatabase(TYdbDriverConfig* config, const char* database) { - try { - if (!config) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; - } - if (!database) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid database"}; - } - - try { - config->config.SetDatabase(std::string(database)); - return config; - } catch (const std::exception& e) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -TYdbDriverConfig* YdbSetAuthToken(TYdbDriverConfig* config, const char* token) { - try { - if (!config) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; - } - if (!token) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid token"}; - } - - try { - config->config.SetAuthToken(std::string(token)); - return config; - } catch (const std::exception& e) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -TYdbDriverConfig* YdbSetSecureConnection(TYdbDriverConfig* config, const char* cert) { - try { - if (!config) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid config"}; - } - if (!cert) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, "Invalid certificate"}; - } - - try { - config->config.UseSecureConnection(std::string(cert)); - return config; - } catch (const std::exception& e) { - return new TYdbDriverConfig{YDB_DRIVER_CONFIG_INVALID, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -EYdbDriverConfigStatus YdbGetDriverConfigStatus(TYdbDriverConfig* config) { - if (!config) { - return YDB_DRIVER_CONFIG_INVALID; - } - - return config->errorCode; -} - -const char* YdbGetDriverConfigErrorMessage(TYdbDriverConfig* config) { - if (!config) { - return "Invalid config"; - } - - return config->errorMessage.c_str(); -} - -// Создание и уничтожение драйвера -TYdbDriver* YdbCreateDriverFromConfig(TYdbDriverConfig* config) { - try { - if (!config) { - return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid config"}; - } - - if (config->errorCode != 0) { - return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid config: " + config->errorMessage}; - } - - try { - return new TYdbDriver{YDB_DRIVER_OK, "", NYdb::TDriver(config->config)}; - } catch (const std::exception& e) { - return new TYdbDriver{YDB_DRIVER_ERROR, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -TYdbDriver* YdbCreateDriver(const char* connectionString) { - try { - if (!connectionString) { - return new TYdbDriver{YDB_DRIVER_ERROR, "Invalid connection string"}; - } - - try { - return new TYdbDriver{YDB_DRIVER_OK, "", NYdb::TDriver(std::string(connectionString))}; - } catch (const std::exception& e) { - return new TYdbDriver{YDB_DRIVER_ERROR, e.what()}; - } - } catch (...) { - return nullptr; - } -} - -void YdbDestroyDriver(TYdbDriver* driver) { - if (driver) { - delete driver; - } -} - -} diff --git a/c_api/src/impl/driver_impl.h b/c_api/src/impl/driver_impl.h deleted file mode 100644 index 87644f15f26..00000000000 --- a/c_api/src/impl/driver_impl.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include - -struct TYdbDriverConfigImpl { - EYdbDriverConfigStatus errorCode; - std::string errorMessage; - - NYdb::TDriverConfig config; -}; - -struct TYdbDriverImpl { - EYdbDriverStatus errorCode; - std::string errorMessage; - - std::optional driver; -}; diff --git a/c_api/src/impl/result_impl.h b/c_api/src/impl/result_impl.h deleted file mode 100644 index b662ea4f452..00000000000 --- a/c_api/src/impl/result_impl.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -#include - -#include - -struct TYdbResultSetImpl { - EYdbResultSetStatus errorCode; - std::string errorMessage; - - std::optional result; -}; diff --git a/c_api/src/impl/value_impl.h b/c_api/src/impl/value_impl.h deleted file mode 100644 index d7a7a4eba9e..00000000000 --- a/c_api/src/impl/value_impl.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include - -struct TYdbValueImpl { - EYdbValueStatus errorCode; - std::string errorMessage; - - std::optional value; -}; diff --git a/c_api/src/query.cpp b/c_api/src/query.cpp deleted file mode 100644 index 0f11c52c17c..00000000000 --- a/c_api/src/query.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include - -#include - -#include "impl/driver_impl.h" // NOLINT -#include "impl/result_impl.h" // NOLINT - -#include - -struct TYdbQueryClientImpl { - EYdbQueryClientError errorCode; - std::string errorMessage; - - std::optional client; -}; - -struct TYdbQueryResultImpl { - EYdbQueryResultError errorCode; - std::string errorMessage; - - std::optional result; -}; - -extern "C" { - -TYdbQueryClient* YdbCreateQueryClient(TYdbDriver* driver) { - try { - if (!driver || !driver->driver.has_value()) { - return new TYdbQueryClient{ - YDB_QUERY_CLIENT_ERROR, - "Invalid driver" - }; - } - - try { - return new TYdbQueryClient{ - YDB_QUERY_CLIENT_OK, - "", - NYdb::NQuery::TQueryClient(*driver->driver) - }; - } catch (const std::exception& e) { - return new TYdbQueryClient{ - YDB_QUERY_CLIENT_ERROR, - e.what() - }; - } - } catch (...) { - return nullptr; - } -} - -void YdbDestroyQueryClient(TYdbQueryClient* queryClient) { - if (queryClient) { - delete queryClient; - } -} - -TYdbQueryResult* YdbExecuteQuery(TYdbQueryClient* queryClient, const char* query) { - try { - if (!queryClient || !queryClient->client.has_value()) { - return new TYdbQueryResult{ - YDB_QUERY_RESULT_ERROR, - "Invalid query client" - }; - } - - if (!query) { - return new TYdbQueryResult{ - YDB_QUERY_RESULT_ERROR, - "Invalid query" - }; - } - - try { - auto client = *queryClient->client; - auto executeResult = client.ExecuteQuery( - std::string(query), - NYdb::NQuery::TTxControl::NoTx() - ).GetValueSync(); - - if (!executeResult.IsSuccess()) { - return new TYdbQueryResult{ - YDB_QUERY_RESULT_ERROR, - "Query execution failed: " + executeResult.GetIssues().ToString() - }; - } - - return new TYdbQueryResult{ - YDB_QUERY_RESULT_OK, - "", - executeResult - }; - } catch (const std::exception& e) { - return new TYdbQueryResult{ - YDB_QUERY_RESULT_ERROR, - e.what() - }; - } - } catch (...) { - return nullptr; - } -} - -void YdbDestroyQueryResult(TYdbQueryResult* result) { - if (result) { - delete result; - } -} - -TYdbResultSet* YdbGetQueryResultSet(TYdbQueryResult* result, size_t index) { - try { - if (!result || !result->result.has_value()) { - return nullptr; - } - - try { - return new TYdbResultSet{ - YDB_RESULT_SET_OK, - "", - result->result->GetResultSet(index) - }; - } catch (const std::exception& e) { - return new TYdbResultSet{ - YDB_RESULT_SET_ERROR, - e.what() - }; - } - } catch (...) { - return nullptr; - } -} - -int YdbGetQueryResultSetsCount(TYdbQueryResult* result) { - if (!result || !result->result.has_value()) { - return -1; - } - return result->result->GetResultSets().size(); -} - -} diff --git a/c_api/src/result.cpp b/c_api/src/result.cpp deleted file mode 100644 index c6c52bc7373..00000000000 --- a/c_api/src/result.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include - -#include -#include - -#include "impl/result_impl.h" // NOLINT -#include "impl/value_impl.h" // NOLINT - -extern "C" { - -int YdbGetColumnsCount(TYdbResultSet* resultSet) { - if (!resultSet || !resultSet->result.has_value()) { - return -1; - } - - return resultSet->result->ColumnsCount(); -} - -int YdbGetRowsCount(TYdbResultSet* resultSet) { - if (!resultSet || !resultSet->result.has_value()) { - return -1; - } - - return resultSet->result->RowsCount(); -} - -int YdbIsTruncated(TYdbResultSet* resultSet) { - if (!resultSet || !resultSet->result.has_value()) { - return -1; - } - - return resultSet->result->Truncated(); -} - -const char* YdbGetColumnName(TYdbResultSet* resultSet, size_t index) { - if (!resultSet || !resultSet->result.has_value()) { - return nullptr; - } - - return resultSet->result->GetColumnsMeta()[index].Name.c_str(); -} - -int YdbGetColumnIndex(TYdbResultSet* resultSet, const char* name) { - try { - if (!resultSet || !resultSet->result.has_value()) { - return -1; - } - - NYdb::TResultSetParser parser(*resultSet->result); - return parser.ColumnIndex(name); - } catch (...) { - return -1; - } -} - -TYdbValue* YdbGetValue(TYdbResultSet* resultSet, size_t rowIndex, const char* name) { - try { - if (!resultSet || !resultSet->result.has_value()) { - return new TYdbValue{YDB_VALUE_ERROR, "Invalid result set"}; - } - - NYdb::TResultSetParser parser(*resultSet->result); - int columnIndex = parser.ColumnIndex(name); - if (columnIndex == -1) { - return new TYdbValue{YDB_VALUE_ERROR, "Invalid column name"}; - } - - return YdbGetValueByIndex(resultSet, rowIndex, columnIndex); - } catch (...) { - return nullptr; - } -} - -TYdbValue* YdbGetValueByIndex(TYdbResultSet* resultSet, size_t rowIndex, size_t columnIndex) { - try { - if (!resultSet || !resultSet->result.has_value()) { - return nullptr; - } - - auto proto = NYdb::TProtoAccessor::GetProto(*resultSet->result); - - auto type = resultSet->result->GetColumnsMeta()[columnIndex].Type; - auto value = proto.rows(rowIndex).items(columnIndex); - - return new TYdbValue{YDB_VALUE_OK, "", NYdb::TValue{type, value}}; - } catch (...) { - return nullptr; - } -} - -void YdbDestroyResultSet(TYdbResultSet* resultSet) { - delete resultSet; -} - -} diff --git a/c_api/src/value.cpp b/c_api/src/value.cpp deleted file mode 100644 index 2aa25ce4183..00000000000 --- a/c_api/src/value.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include - -#include - -#include "impl/value_impl.h" // NOLINT - -extern "C" { - -EYdbTypeKind YdbGetTypeKind(TYdbValue* value) { - if (!value || !value->value.has_value()) { - return YDB_TYPE_KIND_UNDEFINED; - } - - NYdb::TValueParser valueParser(*value->value); - - switch (valueParser.GetKind()) { - case NYdb::TTypeParser::ETypeKind::Primitive: - return YDB_TYPE_KIND_PRIMITIVE; - case NYdb::TTypeParser::ETypeKind::Optional: - return YDB_TYPE_KIND_OPTIONAL; - case NYdb::TTypeParser::ETypeKind::List: - return YDB_TYPE_KIND_LIST; - case NYdb::TTypeParser::ETypeKind::Struct: - return YDB_TYPE_KIND_STRUCT; - case NYdb::TTypeParser::ETypeKind::Tuple: - return YDB_TYPE_KIND_TUPLE; - case NYdb::TTypeParser::ETypeKind::Dict: - return YDB_TYPE_KIND_DICT; - case NYdb::TTypeParser::ETypeKind::Variant: - return YDB_TYPE_KIND_VARIANT; - default: - return YDB_TYPE_KIND_UNDEFINED; - } -} - -EYdbPrimitiveType YdbGetPrimitiveType(TYdbValue* value) { - if (!value || !value->value.has_value()) { - return YDB_PRIMITIVE_TYPE_UNDEFINED; - } - - try { - NYdb::TValueParser valueParser(*value->value); - - switch (valueParser.GetPrimitiveType()) { - case NYdb::EPrimitiveType::Int8: - return YDB_PRIMITIVE_TYPE_INT8; - case NYdb::EPrimitiveType::Uint8: - return YDB_PRIMITIVE_TYPE_UINT8; - case NYdb::EPrimitiveType::Int16: - return YDB_PRIMITIVE_TYPE_INT16; - case NYdb::EPrimitiveType::Uint16: - return YDB_PRIMITIVE_TYPE_UINT16; - case NYdb::EPrimitiveType::Int32: - return YDB_PRIMITIVE_TYPE_INT32; - case NYdb::EPrimitiveType::Uint32: - return YDB_PRIMITIVE_TYPE_UINT32; - case NYdb::EPrimitiveType::Int64: - return YDB_PRIMITIVE_TYPE_INT64; - case NYdb::EPrimitiveType::Uint64: - return YDB_PRIMITIVE_TYPE_UINT64; - case NYdb::EPrimitiveType::Float: - return YDB_PRIMITIVE_TYPE_FLOAT; - case NYdb::EPrimitiveType::Double: - return YDB_PRIMITIVE_TYPE_DOUBLE; - case NYdb::EPrimitiveType::String: - return YDB_PRIMITIVE_TYPE_STRING; - case NYdb::EPrimitiveType::Utf8: - return YDB_PRIMITIVE_TYPE_UTF8; - case NYdb::EPrimitiveType::Yson: - return YDB_PRIMITIVE_TYPE_YSON; - case NYdb::EPrimitiveType::Json: - return YDB_PRIMITIVE_TYPE_JSON; - case NYdb::EPrimitiveType::JsonDocument: - return YDB_PRIMITIVE_TYPE_JSON_DOCUMENT; - case NYdb::EPrimitiveType::DyNumber: - return YDB_PRIMITIVE_TYPE_DYNUMBER; - default: - return YDB_PRIMITIVE_TYPE_UNDEFINED; - } - } catch (...) { - return YDB_PRIMITIVE_TYPE_UNDEFINED; - } -} - -EYdbValueStatus YdbGetInt8(TYdbValue* value, int8_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetInt8(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetUint8(TYdbValue* value, uint8_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetUint8(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetInt16(TYdbValue* value, int16_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetInt16(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetUint16(TYdbValue* value, uint16_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetUint16(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetInt32(TYdbValue* value, int32_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetInt32(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetUint32(TYdbValue* value, uint32_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetUint32(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetInt64(TYdbValue* value, int64_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetInt64(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetUint64(TYdbValue* value, uint64_t* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetUint64(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetFloat(TYdbValue* value, float* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetFloat(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetDouble(TYdbValue* value, double* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetDouble(); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetString(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetString().c_str(), valueParser.GetString().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetUtf8(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetUtf8().c_str(), valueParser.GetUtf8().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetYson(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetYson().c_str(), valueParser.GetYson().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetJson(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetJson().c_str(), valueParser.GetJson().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetJsonDocument(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetJsonDocument().c_str(), valueParser.GetJsonDocument().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetDyNumber(TYdbValue* value, char** result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = strndup(valueParser.GetDyNumber().c_str(), valueParser.GetDyNumber().size()); - return YDB_VALUE_OK; -} - -EYdbValueStatus YdbGetBool(TYdbValue* value, bool* result) { - if (!value || !value->value.has_value()) { - return YDB_VALUE_ERROR; - } - - NYdb::TValueParser valueParser(*value->value); - - *result = valueParser.GetBool(); - return YDB_VALUE_OK; -} - -void YdbDestroyValue(TYdbValue* value) { - delete value; -} - -} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0c712974cf3..f7f2028139f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,5 @@ add_subdirectory(basic_example) add_subdirectory(bulk_upsert_simple) -add_subdirectory(c_api) add_subdirectory(pagination) add_subdirectory(secondary_index) add_subdirectory(secondary_index_builtin) diff --git a/examples/c_api/CMakeLists.txt b/examples/c_api/CMakeLists.txt deleted file mode 100644 index 5060c4c25d3..00000000000 --- a/examples/c_api/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -add_executable(c_api_example - main.c -) - -target_link_libraries(c_api_example ydb-cpp-sdk::c-api) diff --git a/examples/c_api/main.c b/examples/c_api/main.c deleted file mode 100644 index ca1e2018d0f..00000000000 --- a/examples/c_api/main.c +++ /dev/null @@ -1,37 +0,0 @@ -#include - -#include - -#include - -int main() { - TYdbDriver* driver = YdbCreateDriver("grpc://localhost:2136/?database=/local"); - - TYdbQueryClient* query = YdbCreateQueryClient(driver); - - TYdbQueryResult* result = YdbExecuteQuery(query, "SELECT 1"); - - int resultSetsCount = YdbGetQueryResultSetsCount(result); - for (int i = 0; i < resultSetsCount; i++) { - TYdbResultSet* resultSet = YdbGetQueryResultSet(result, i); - int rowsCount = YdbGetRowsCount(resultSet); - for (int j = 0; j < rowsCount; j++) { - TYdbValue* value = YdbGetValueByIndex(resultSet, j, 0); - - EYdbPrimitiveType primitiveType = YdbGetPrimitiveType(value); - if (primitiveType == YDB_PRIMITIVE_TYPE_INT32) { - int32_t int32Value; - YdbGetInt32(value, &int32Value); - printf("%" PRId32 "\n", int32Value); - } else { - printf("Unknown primitive type\n"); - } - YdbDestroyValue(value); - } - } - - YdbDestroyQueryResult(result); - YdbDestroyQueryClient(query); - YdbDestroyDriver(driver); - return 0; -} From f27d288cbdbaeaffc5b4f7101fcd4c772d531dbc Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 26 May 2025 18:34:06 +0000 Subject: [PATCH 05/55] Parameter bind --- odbc/CMakeLists.txt | 1 + odbc/examples/basic/main.cpp | 23 +- odbc/odbcinst.ini | 7 +- odbc/src/connection.cpp | 26 +- odbc/src/connection.h | 11 +- odbc/src/environment.cpp | 30 +- odbc/src/environment.h | 2 +- odbc/src/statement.cpp | 118 ++------ odbc/src/statement.h | 52 ++-- odbc/src/utils/convert.cpp | 286 ++++++++++++++++++ odbc/src/utils/convert.h | 26 ++ tests/integration/sessions/CMakeLists.txt | 4 +- .../integration/sessions_pool/CMakeLists.txt | 2 +- 13 files changed, 430 insertions(+), 158 deletions(-) create mode 100644 odbc/src/utils/convert.cpp create mode 100644 odbc/src/utils/convert.h diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 46747768327..c45baef5f54 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,5 +1,6 @@ # Добавляем исходники add_library(ydb-odbc SHARED + src/utils/convert.cpp src/odbc_driver.cpp src/connection.cpp src/statement.cpp diff --git a/odbc/examples/basic/main.cpp b/odbc/examples/basic/main.cpp index 9b8123ef7c1..364b9e11123 100644 --- a/odbc/examples/basic/main.cpp +++ b/odbc/examples/basic/main.cpp @@ -2,9 +2,6 @@ #include #include -#include -#include -#include void PrintOdbcError(SQLSMALLINT handleType, SQLHANDLE handle) { SQLCHAR sqlState[6] = {0}; @@ -65,16 +62,16 @@ int main() { std::cout << "6. Executing query" << std::endl; SQLCHAR query[] = R"( - DECLARE $p1 AS Int64; - SELECT $p1 + 1, 'test1' as String; - SELECT $p1 + 2, 'test2' as String; - SELECT $p1 + 3, 'test3' as String; - SELECT $p1 + 4, 'test4' as String; - SELECT $p1 + 5, 'test5' as String; - SELECT $p1 + 6, 'test6' as String; - SELECT $p1 + 7, 'test7' as String; - SELECT $p1 + 8, 'test8' as String; - SELECT $p1 + 9, 'test9' as String; + DECLARE $p1 AS Int64?; + SELECT $p1 + 1, 'test1'; + SELECT $p1 + 2, 'test2'; + SELECT $p1 + 3, 'test3'; + SELECT $p1 + 4, 'test4'; + SELECT $p1 + 5, 'test5'; + SELECT $p1 + 6, 'test6'; + SELECT $p1 + 7, 'test7'; + SELECT $p1 + 8, 'test8'; + SELECT $p1 + 9, 'test9'; )"; int64_t paramValue = 42; diff --git a/odbc/odbcinst.ini b/odbc/odbcinst.ini index fade7b6fb92..fd0b3f27650 100644 --- a/odbc/odbcinst.ini +++ b/odbc/odbcinst.ini @@ -1,7 +1,4 @@ [YDB] Description=YDB ODBC Driver -Driver=/usr/local/lib/libydb-odbc.so -Setup=/usr/local/lib/libydb-odbc.so -Threading=2 -FileUsage=1 -UsageCount=1 \ No newline at end of file +Driver=/home/brgayazov/ydbwork/ydb-cpp-sdk/build/odbc/libydb-odbc.so +Setup=/home/brgayazov/ydbwork/ydb-cpp-sdk/build/odbc/libydb-odbc.so \ No newline at end of file diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 427b03f7ba1..a2c0df7c545 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -1,8 +1,10 @@ #include "connection.h" #include "statement.h" + #include #include #include + #include #include @@ -12,7 +14,6 @@ namespace NYdb { namespace NOdbc { SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { - // Парсим параметры std::map params; size_t pos = 0; while (pos < connectionString.size()) { @@ -50,7 +51,7 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { SQLRETURN TConnection::Connect(const std::string& serverName, const std::string& userName, const std::string& auth) { - // Получаем параметры из секции DSN через Driver Manager API + char endpoint[256] = {0}; char database[256] = {0}; @@ -82,13 +83,24 @@ SQLRETURN TConnection::Disconnect() { SQLRETURN TConnection::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { + return SQL_NO_DATA; + } + const auto& err = Errors_[recNumber-1]; - if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); - if (nativeError) *nativeError = err.NativeError; + if (sqlState) { + strncpy((char*)sqlState, err.SqlState.c_str(), 6); + } + + if (nativeError) { + *nativeError = err.NativeError; + } + if (messageText && bufferLength > 0) { strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + if (textLength) { + *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } } return SQL_SUCCESS; } @@ -111,7 +123,7 @@ void TConnection::ClearErrors() { } std::pair TConnection::ParseConnectionString(const std::string& connectionString) { - // Заглушка + // TODO: Implement return {"", ""}; } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index cceadc2433f..95c872f04ba 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -1,16 +1,17 @@ #pragma once +#include "environment.h" + +#include +#include + #include #include + #include #include #include -#include -#include - -#include "environment.h" - namespace NYdb { namespace NOdbc { diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index 0e1eef594d9..a09a634879b 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -13,15 +13,31 @@ SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQL return SQL_SUCCESS; } -SQLRETURN TEnvironment::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - // Заглушка - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; +SQLRETURN TEnvironment::GetDiagRec(SQLSMALLINT recNumber, + SQLCHAR* sqlState, + SQLINTEGER* nativeError, + SQLCHAR* messageText, + SQLSMALLINT bufferLength, + SQLSMALLINT* textLength) { + + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { + return SQL_NO_DATA; + } + const auto& err = Errors_[recNumber-1]; - if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); - if (nativeError) *nativeError = err.NativeError; + if (sqlState) { + strncpy((char*)sqlState, err.SqlState.c_str(), 6); + } + + if (nativeError) { + *nativeError = err.NativeError; + } + if (messageText && bufferLength > 0) { strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + if (textLength) { + *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } } return SQL_SUCCESS; } @@ -35,4 +51,4 @@ void TEnvironment::ClearErrors() { } } // namespace NOdbc -} // namespace NYdb \ No newline at end of file +} // namespace NYdb diff --git a/odbc/src/environment.h b/odbc/src/environment.h index a45d7f0b7ee..0190b913831 100644 --- a/odbc/src/environment.h +++ b/odbc/src/environment.h @@ -37,4 +37,4 @@ class TEnvironment { }; } // namespace NOdbc -} // namespace NYdb \ No newline at end of file +} // namespace NYdb diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 224cc8dc122..ab8318b0f99 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -1,6 +1,5 @@ #include "statement.h" -#include #include #include @@ -22,7 +21,6 @@ SQLRETURN TStatement::ExecDirect(const std::string& statementText) { if (!Errors_.empty()) { return SQL_ERROR; } - // --- конец сборки параметров --- auto sessionResult = client->GetSession().ExtractValueSync(); if (!sessionResult.IsSuccess()) { @@ -50,7 +48,6 @@ SQLRETURN TStatement::Fetch() { while (true) { if (ResultSetParser_) { if (ResultSetParser_->TryNextRow()) { - // Автоматически заполняем связанные буферы for (const auto& col : BoundColumns_) { GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); } @@ -95,22 +92,38 @@ SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) return SQL_NO_DATA; + + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { + return SQL_NO_DATA; + } + const auto& err = Errors_[recNumber-1]; - if (sqlState) strncpy((char*)sqlState, err.SqlState.c_str(), 6); - if (nativeError) *nativeError = err.NativeError; + if (sqlState) { + strncpy((char*)sqlState, err.SqlState.c_str(), 6); + } + + if (nativeError) { + *nativeError = err.NativeError; + } + if (messageText && bufferLength > 0) { strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + if (textLength) { + *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } } return SQL_SUCCESS; } -SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - // Удаляем старую связь для этой колонки, если есть +SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, + SQLSMALLINT targetType, + SQLPOINTER targetValue, + SQLLEN bufferLength, + SQLLEN* strLenOrInd) { + BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); - // Если targetValue == nullptr, просто удаляем связь + if (!targetValue) { return SQL_SUCCESS; } @@ -127,14 +140,15 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr) { + if (inputOutputType != SQL_PARAM_INPUT) { AddError("HYC00", 0, "Only input parameters are supported"); return SQL_ERROR; } - // Удаляем старую связь для этого параметра, если есть + BoundParams_.erase(std::remove_if(BoundParams_.begin(), BoundParams_.end(), [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }), BoundParams_.end()); - // Если parameterValuePtr == nullptr, просто удаляем связь + if (!parameterValuePtr) { return SQL_SUCCESS; } @@ -162,7 +176,6 @@ SQLRETURN TStatement::ConvertYdbValue(NYdb::TValueParser& valueParser, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - // 1. Проверка на NULL if (valueParser.IsNull()) { if (strLenOrInd) *strLenOrInd = SQL_NULL_DATA; return SQL_SUCCESS; @@ -249,7 +262,6 @@ SQLRETURN TStatement::ConvertYdbValue(NYdb::TValueParser& valueParser, if (strLenOrInd) *strLenOrInd = sizeof(char); return SQL_SUCCESS; } - // Добавьте обработку дат/времени, бинарных данных и других типов по необходимости default: return SQL_ERROR; } @@ -259,84 +271,12 @@ NYdb::TParams TStatement::BuildParams() { Errors_.clear(); NYdb::TParamsBuilder paramsBuilder; for (const auto& param : BoundParams_) { - std::string paramName = "$p" + std::to_string(param.ParamNumber); // ODBC нумерует с 1 - auto& builder = paramsBuilder.AddParam(paramName); - // Обработка NULL - if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { - builder.EmptyOptional(); - builder.Build(); - continue; - } - - switch (param.ValueType) { - case SQL_C_SLONG: { - auto value = *static_cast(param.ParameterValuePtr); - switch (param.ParameterType) { - case SQL_INTEGER: - builder.Int32(static_cast(value)); - break; - case SQL_BIGINT: - builder.Int64(static_cast(value)); - break; - case SQL_DOUBLE: - builder.Double(static_cast(value)); - break; - case SQL_FLOAT: - builder.Float(static_cast(value)); - break; - case SQL_VARCHAR: - case SQL_CHAR: - case SQL_LONGVARCHAR: - builder.Utf8(std::to_string(value)); - break; - case SQL_BIT: - builder.Uint8(static_cast(value)); - break; - default: - AddError("07006", 0, "Unsupported SQL type"); - return paramsBuilder.Build(); - } - break; - } - case SQL_C_SBIGINT: { - auto v = *static_cast(param.ParameterValuePtr); - builder.Int32(static_cast(v)); - break; - } - default: { - AddError("07006", 0, "Unsupported C type"); - return paramsBuilder.Build(); - } - } - - switch (param.ParameterType) { - case SQL_INTEGER: - case SQL_BIGINT: - break; - case SQL_DOUBLE: - builder.Double(*reinterpret_cast(param.ParameterValuePtr)); - break; - case SQL_FLOAT: - builder.Double(*reinterpret_cast(param.ParameterValuePtr)); - break; - case SQL_VARCHAR: - case SQL_CHAR: - case SQL_LONGVARCHAR: - builder.Utf8(*reinterpret_cast(param.ParameterValuePtr)); - break; - case SQL_BIT: - builder.Bool(*reinterpret_cast(param.ParameterValuePtr)); - break; - default: - AddError("07006", 0, "Unsupported SQL type"); - return paramsBuilder.Build(); - } - - builder.Build(); + std::string paramName = "$p" + std::to_string(param.ParamNumber); + ConvertValue(param, paramsBuilder.AddParam(paramName)); } return paramsBuilder.Build(); } } // namespace NOdbc -} // namespace NYdb \ No newline at end of file +} // namespace NYdb diff --git a/odbc/src/statement.h b/odbc/src/statement.h index f34e0e771e4..8f51be6759b 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -1,25 +1,23 @@ #pragma once +#include "connection.h" +#include "utils/convert.h" + +#include + #include #include + #include #include #include -#include - -#include "connection.h" namespace NYdb { namespace NOdbc { class TStatement { private: - TConnection* Conn_; - TErrorList Errors_; - std::unique_ptr Iterator_; - std::unique_ptr ResultSetParser_; - struct TBoundColumn { SQLUSMALLINT ColumnNumber; SQLSMALLINT TargetType; @@ -27,21 +25,7 @@ class TStatement { SQLLEN BufferLength; SQLLEN* StrLenOrInd; }; - std::vector BoundColumns_; - - struct TBoundParam { - SQLUSMALLINT ParamNumber; - SQLSMALLINT InputOutputType; - SQLSMALLINT ValueType; - SQLSMALLINT ParameterType; - SQLULEN ColumnSize; - SQLSMALLINT DecimalDigits; - SQLPOINTER ParameterValuePtr; - SQLLEN BufferLength; - SQLLEN* StrLenOrIndPtr; - }; - std::vector BoundParams_; - + public: TStatement(TConnection* conn); @@ -49,24 +33,36 @@ class TStatement { SQLRETURN Fetch(); SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); - - TConnection* GetConnection() { return Conn_; } - + + TConnection* GetConnection() { + return Conn_; + } + void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); void ClearErrors(); NYdb::TParams BuildParams(); - + private: void ClearStatement(); SQLRETURN ConvertYdbValue(NYdb::TValueParser& valueParser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); + + TConnection* Conn_; + TErrorList Errors_; + std::unique_ptr Iterator_; + std::unique_ptr ResultSetParser_; + + std::vector BoundColumns_; + std::vector BoundParams_; }; } // namespace NOdbc -} // namespace NYdb \ No newline at end of file +} // namespace NYdb diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp new file mode 100644 index 00000000000..2c62d56347e --- /dev/null +++ b/odbc/src/utils/convert.cpp @@ -0,0 +1,286 @@ +#include "convert.h" + +#include + +namespace NYdb { +namespace NOdbc { + +template +struct TSqlTypeTraits; + +template<> struct TSqlTypeTraits { using Type = std::string; }; +template<> struct TSqlTypeTraits { using Type = SQLBIGINT; }; +template<> struct TSqlTypeTraits { using Type = SQLUBIGINT; }; +template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; +template<> struct TSqlTypeTraits { using Type = SQLUINTEGER; }; +template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; +template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; +template<> struct TSqlTypeTraits { using Type = SQLUSMALLINT; }; +template<> struct TSqlTypeTraits { using Type = SQLSCHAR; }; +template<> struct TSqlTypeTraits { using Type = SQLCHAR; }; +template<> struct TSqlTypeTraits { using Type = SQLDOUBLE; }; +template<> struct TSqlTypeTraits { using Type = SQLFLOAT; }; +template<> struct TSqlTypeTraits { using Type = SQLCHAR; }; + +template +struct TTypedValue { + using TSrcType = typename TSqlTypeTraits::Type; + + TSrcType Data; + + TTypedValue(const TBoundParam& param) { + Data = *static_cast(param.ParameterValuePtr); + } +}; + +template<> +TTypedValue::TTypedValue(const TBoundParam& param) { + Data = std::string(static_cast(param.ParameterValuePtr), param.BufferLength); +} + +class IConverter { +public: + virtual void AddToBuilder(const TBoundParam& param, TParamValueBuilder& builder) = 0; + + virtual ~IConverter() = default; +}; + +template +class TConverter : public IConverter { +public: + virtual void AddToBuilder(const TBoundParam& param, TParamValueBuilder& builder) override { + TTypedValue value(param); + Convert(param, std::move(value.Data), builder); + if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { + builder.EmptyOptional(GetType()); + } + builder.Build(); + } + +private: + void Convert(const TBoundParam& param, TTypedValue::TSrcType&& data, TParamValueBuilder& builder); + TType GetType(); +}; + +class TConverterRegistry { +public: + static TConverterRegistry& GetInstance() { + static TConverterRegistry instance; + return instance; + } + + void RegisterConverter(SQLSMALLINT cType, SQLSMALLINT sqlType, std::unique_ptr converter) { + Converters_.emplace(std::make_pair(cType, sqlType), std::move(converter)); + } + + IConverter* GetConverter(SQLSMALLINT cType, SQLSMALLINT sqlType) { + auto it = Converters_.find(std::make_pair(cType, sqlType)); + if (it != Converters_.end()) { + return it->second.get(); + } + return nullptr; + } + +private: + std::map, std::unique_ptr> Converters_; +}; + +#define REGISTER_CONVERTER(CType, SqlType, YdbType) \ + struct TConverterRegistration##CType##SqlType { \ + TConverterRegistration##CType##SqlType() { \ + TConverterRegistry::GetInstance().RegisterConverter(CType, SqlType, std::make_unique>()); \ + } \ + }; \ + static const TConverterRegistration##CType##SqlType converterRegistration##CType##SqlType; \ + template<> \ + TType TConverter::GetType() { \ + return TTypeBuilder().Primitive(YdbType).Build(); \ + } \ + template<> \ + void TConverter::Convert(const TBoundParam& param, TTypedValue::TSrcType&& data, TParamValueBuilder& builder) + +// Integer types + +REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_BIGINT, EPrimitiveType::Int64) { + builder.OptionalInt64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_LONG, SQL_BIGINT, EPrimitiveType::Int64) { + builder.OptionalInt64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SHORT, SQL_BIGINT, EPrimitiveType::Int64) { + builder.OptionalInt64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_TINYINT, SQL_BIGINT, EPrimitiveType::Int64) { + builder.OptionalInt64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_BIGINT, EPrimitiveType::Uint64) { + builder.OptionalUint64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_ULONG, SQL_BIGINT, EPrimitiveType::Uint64) { + builder.OptionalUint64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_USHORT, SQL_BIGINT, EPrimitiveType::Uint64) { + builder.OptionalUint64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_BIGINT, EPrimitiveType::Uint64) { + builder.OptionalUint64(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_INTEGER, EPrimitiveType::Int32) { + builder.OptionalInt32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_LONG, SQL_INTEGER, EPrimitiveType::Int32) { + builder.OptionalInt32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SHORT, SQL_INTEGER, EPrimitiveType::Int32) { + builder.OptionalInt32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_TINYINT, SQL_INTEGER, EPrimitiveType::Int32) { + builder.OptionalInt32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_INTEGER, EPrimitiveType::Uint32) { + builder.OptionalUint32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_ULONG, SQL_INTEGER, EPrimitiveType::Uint32) { + builder.OptionalUint32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_USHORT, SQL_INTEGER, EPrimitiveType::Uint32) { + builder.OptionalUint32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_INTEGER, EPrimitiveType::Uint32) { + builder.OptionalUint32(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_SMALLINT, EPrimitiveType::Int16) { + builder.OptionalInt16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_LONG, SQL_SMALLINT, EPrimitiveType::Int16) { + builder.OptionalInt16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SHORT, SQL_SMALLINT, EPrimitiveType::Int16) { + builder.OptionalInt16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_TINYINT, SQL_SMALLINT, EPrimitiveType::Int16) { + builder.OptionalInt16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_SMALLINT, EPrimitiveType::Uint16) { + builder.OptionalUint16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_ULONG, SQL_SMALLINT, EPrimitiveType::Uint16) { + builder.OptionalUint16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_USHORT, SQL_SMALLINT, EPrimitiveType::Uint16) { + builder.OptionalUint16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_SMALLINT, EPrimitiveType::Uint16) { + builder.OptionalUint16(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_TINYINT, EPrimitiveType::Int8) { + builder.OptionalInt8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_LONG, SQL_TINYINT, EPrimitiveType::Int8) { + builder.OptionalInt8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_SHORT, SQL_TINYINT, EPrimitiveType::Int8) { + builder.OptionalInt8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_TINYINT, SQL_TINYINT, EPrimitiveType::Int8) { + builder.OptionalInt8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_TINYINT, EPrimitiveType::Uint8) { + builder.OptionalUint8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_ULONG, SQL_TINYINT, EPrimitiveType::Uint8) { + builder.OptionalUint8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_USHORT, SQL_TINYINT, EPrimitiveType::Uint8) { + builder.OptionalUint8(static_cast(data)); +} + +REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_TINYINT, EPrimitiveType::Uint8) { + builder.OptionalUint8(static_cast(data)); +} + +// Floating point types + +REGISTER_CONVERTER(SQL_C_FLOAT, SQL_REAL, EPrimitiveType::Float) { + builder.OptionalFloat(data); +} + +REGISTER_CONVERTER(SQL_C_DOUBLE, SQL_FLOAT, EPrimitiveType::Double) { + builder.OptionalDouble(data); +} + +REGISTER_CONVERTER(SQL_C_DOUBLE, SQL_DOUBLE, EPrimitiveType::Double) { + builder.OptionalDouble(data); +} + +// String types + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_CHAR, EPrimitiveType::Utf8) { + builder.OptionalUtf8(std::move(data)); +} + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_VARCHAR, EPrimitiveType::Utf8) { + builder.OptionalUtf8(std::move(data)); +} + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_LONGVARCHAR, EPrimitiveType::Utf8) { + builder.OptionalUtf8(std::move(data)); +} + +// Binary types + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_BINARY, EPrimitiveType::String) { + builder.OptionalString(std::move(data)); +} + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_VARBINARY, EPrimitiveType::String) { + builder.OptionalString(std::move(data)); +} + +REGISTER_CONVERTER(SQL_C_CHAR, SQL_LONGVARBINARY, EPrimitiveType::String) { + builder.OptionalString(std::move(data)); +} + +#undef REGISTER_CONVERTER + +void ConvertValue(const TBoundParam& param, TParamValueBuilder& builder) { + auto converter = TConverterRegistry::GetInstance().GetConverter(param.ValueType, param.ParameterType); + if (converter) { + converter->AddToBuilder(param, builder); + } else { + throw 1; // TODO: throw exception + } +} + +} // namespace NYdb +} // namespace NOdbc diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h new file mode 100644 index 00000000000..525a43c79aa --- /dev/null +++ b/odbc/src/utils/convert.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +struct TBoundParam { + SQLUSMALLINT ParamNumber; + SQLSMALLINT InputOutputType; + SQLSMALLINT ValueType; + SQLSMALLINT ParameterType; + SQLULEN ColumnSize; + SQLSMALLINT DecimalDigits; + SQLPOINTER ParameterValuePtr; + SQLLEN BufferLength; + SQLLEN* StrLenOrIndPtr; +}; + +void ConvertValue(const TBoundParam& param, TParamValueBuilder& builder); + +} // namespace NYdb +} // namespace NOdbc diff --git a/tests/integration/sessions/CMakeLists.txt b/tests/integration/sessions/CMakeLists.txt index 100c8ace2bc..0cc47bfd4cf 100644 --- a/tests/integration/sessions/CMakeLists.txt +++ b/tests/integration/sessions/CMakeLists.txt @@ -3,8 +3,8 @@ add_ydb_test(NAME sessions_it GTEST main.cpp LINK_LIBRARIES yutil - YDB-CPP-SDK::Table - YDB-CPP-SDK::Query + ydb-cpp-sdk::Table + ydb-cpp-sdk::Query api-grpc grpc-client LABELS diff --git a/tests/integration/sessions_pool/CMakeLists.txt b/tests/integration/sessions_pool/CMakeLists.txt index 6e7a6a70ab7..d37d9d500eb 100644 --- a/tests/integration/sessions_pool/CMakeLists.txt +++ b/tests/integration/sessions_pool/CMakeLists.txt @@ -3,7 +3,7 @@ add_ydb_test(NAME sessions_pool_it GTEST main.cpp LINK_LIBRARIES yutil - YDB-CPP-SDK::Table + ydb-cpp-sdk::Table api-grpc LABELS integration From c3456dbe9ab2ca8998a6e19d137203f211a539ed Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 26 May 2025 19:07:52 +0000 Subject: [PATCH 06/55] Add unit test --- cmake/testing.cmake | 30 ++++++++ odbc/CMakeLists.txt | 10 +-- odbc/tests/CMakeLists.txt | 2 + odbc/tests/unit/CMakeLists.txt | 10 +++ odbc/tests/unit/convert_ut.cpp | 122 +++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 odbc/tests/CMakeLists.txt create mode 100644 odbc/tests/unit/CMakeLists.txt create mode 100644 odbc/tests/unit/convert_ut.cpp diff --git a/cmake/testing.cmake b/cmake/testing.cmake index 999e6a596d6..e45eb0a787f 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -122,3 +122,33 @@ function(add_ydb_test) vcs_info(${YDB_TEST_NAME}) endfunction() + +if (YDB_SDK_ODBC) + function(add_odbc_test) + set(opts "") + set(oneval_args NAME WORKING_DIRECTORY OUTPUT_DIRECTORY) + set(multival_args SOURCES LINK_LIBRARIES LABELS) + cmake_parse_arguments(ODBC_TEST + "${opts}" + "${oneval_args}" + "${multival_args}" + ${ARGN} + ) + + add_ydb_test(GTEST + NAME ${ODBC_TEST_NAME} + SOURCES ${ODBC_TEST_SOURCES} + LINK_LIBRARIES + ${ODBC_TEST_LINK_LIBRARIES} + ODBC::ODBC + LABELS ${ODBC_TEST_LABELS} + ) + + target_compile_definitions(${ODBC_TEST_NAME} + PRIVATE + ODBC_DRIVER_PATH="$" + ) + + add_dependencies(${ODBC_TEST_NAME} ydb-odbc) + endfunction() +endif() diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index c45baef5f54..95ce1702d43 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,4 +1,3 @@ -# Добавляем исходники add_library(ydb-odbc SHARED src/utils/convert.cpp src/odbc_driver.cpp @@ -7,14 +6,12 @@ add_library(ydb-odbc SHARED src/environment.cpp ) -# Добавляем заголовочные файлы target_include_directories(ydb-odbc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${ODBC_INCLUDE_DIRS} ) -# Линкуем с YDB SDK и ODBC target_link_libraries(ydb-odbc PRIVATE ydb-cpp-sdk::Query @@ -27,22 +24,17 @@ set_target_properties(ydb-odbc PROPERTIES POSITION_INDEPENDENT_CODE ON ) -# Устанавливаем драйвер install(TARGETS ydb-odbc LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) -# Устанавливаем заголовочные файлы install(DIRECTORY include/ DESTINATION include/ydb-odbc ) add_subdirectory(examples) +add_subdirectory(tests) -# Добавляем тесты -# add_subdirectory(tests) - -# Правила установки include(GNUInstallDirs) install(FILES diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt new file mode 100644 index 00000000000..446b6139f92 --- /dev/null +++ b/odbc/tests/CMakeLists.txt @@ -0,0 +1,2 @@ +#add_subdirectory(integration) +add_subdirectory(unit) diff --git a/odbc/tests/unit/CMakeLists.txt b/odbc/tests/unit/CMakeLists.txt new file mode 100644 index 00000000000..d1eac199615 --- /dev/null +++ b/odbc/tests/unit/CMakeLists.txt @@ -0,0 +1,10 @@ +add_ydb_test(NAME odbc-convert_ut GTEST + SOURCES + convert_ut.cpp + LINK_LIBRARIES + yutil + api-protos + ydb-odbc + LABELS + unit +) diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp new file mode 100644 index 00000000000..6df6be54f14 --- /dev/null +++ b/odbc/tests/unit/convert_ut.cpp @@ -0,0 +1,122 @@ +#include +#undef BOOL + +#include + +#include + +#include + +#include + +using namespace NYdb::NOdbc; +using namespace NYdb; + +void CheckProtoValue(const Ydb::Value& value, const std::string& expected) { + std::string protoStr; + google::protobuf::TextFormat::PrintToString(value, &protoStr); + ASSERT_EQ(protoStr, expected); +} + +TEST(OdbcConvert, Int64ToYdb) { + SQLBIGINT v = 42; + TBoundParam param{ + 1, // ParamNumber + SQL_PARAM_INPUT, // InputOutputType + SQL_C_SBIGINT, // ValueType + SQL_BIGINT, // ParameterType + 0, 0, // ColumnSize, DecimalDigits + &v, // ParameterValuePtr + sizeof(v), // BufferLength + nullptr // StrLenOrIndPtr + }; + + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProtoValue(value->GetProto(), "int64_value: 42\n"); +} + +TEST(OdbcConvert, Uint64ToYdb) { + SQLUBIGINT v = 123; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_UBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProtoValue(value->GetProto(), "uint64_value: 123\n"); +} + +TEST(OdbcConvert, DoubleToYdb) { + SQLDOUBLE v = 3.14; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProtoValue(value->GetProto(), "double_value: 3.14\n"); +} + +TEST(OdbcConvert, StringToYdbUtf8) { + const char* str = "hello"; + SQLLEN len = 5; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, len, nullptr + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProtoValue(value->GetProto(), "text_value: \"hello\"\n"); +} + +TEST(OdbcConvert, StringToYdbBinary) { + const char* str = "bin\x01\x02"; + SQLLEN len = 5; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_BINARY, 0, 0, (SQLPOINTER)str, len, nullptr + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + ASSERT_EQ(value->GetProto().bytes_value(), std::string(str, len)); +} + +TEST(OdbcConvert, Int64NullToYdb) { + SQLBIGINT v = 42; + SQLLEN nullInd = SQL_NULL_DATA; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), &nullInd + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + ASSERT_EQ(value->GetProto().null_flag_value(), ::google::protobuf::NullValue::NULL_VALUE); +} + +TEST(OdbcConvert, StringNullToYdb) { + const char* str = "test"; + SQLLEN nullInd = SQL_NULL_DATA; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, 4, &nullInd + }; + TParamsBuilder paramsBuilder; + ConvertValue(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + ASSERT_EQ(value->GetProto().null_flag_value(), ::google::protobuf::NullValue::NULL_VALUE); +} From a0decddbab78adcce8d03498d6c37a6cb5182104 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 26 May 2025 19:10:35 +0000 Subject: [PATCH 07/55] fix --- cmake/common.cmake | 2 +- cmake/ydb-cpp-sdk-config.cmake.in | 2 +- examples/basic_example/CMakeLists.txt | 6 +++--- examples/bulk_upsert_simple/CMakeLists.txt | 2 +- examples/pagination/CMakeLists.txt | 2 +- examples/secondary_index/CMakeLists.txt | 2 +- examples/secondary_index_builtin/CMakeLists.txt | 2 +- examples/topic_reader/eventloop/CMakeLists.txt | 2 +- examples/topic_reader/simple/CMakeLists.txt | 2 +- examples/topic_reader/transaction/CMakeLists.txt | 2 +- examples/ttl/CMakeLists.txt | 2 +- examples/vector_index/CMakeLists.txt | 2 +- odbc/CMakeLists.txt | 6 +++--- tests/integration/basic_example/CMakeLists.txt | 6 +++--- tests/integration/bulk_upsert/CMakeLists.txt | 2 +- tests/integration/server_restart/CMakeLists.txt | 2 +- tests/integration/sessions/CMakeLists.txt | 4 ++-- tests/integration/sessions_pool/CMakeLists.txt | 2 +- tests/unit/client/CMakeLists.txt | 4 ++-- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cmake/common.cmake b/cmake/common.cmake index 54d21a418a2..083c8a2c3d4 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -168,7 +168,7 @@ function(resources Tgt Output) endfunction() function(_ydb_sdk_make_client_component CmpName Tgt) - add_library(ydb-cpp-sdk::${CmpName} ALIAS ${Tgt}) + add_library(YDB-CPP-SDK::${CmpName} ALIAS ${Tgt}) if (CmpName STREQUAL "Iam" OR CmpName STREQUAL "IamPrivate") set(PKG_COMP_NAME "libydb-cpp-iam") diff --git a/cmake/ydb-cpp-sdk-config.cmake.in b/cmake/ydb-cpp-sdk-config.cmake.in index 9b303352b18..c6817d87998 100644 --- a/cmake/ydb-cpp-sdk-config.cmake.in +++ b/cmake/ydb-cpp-sdk-config.cmake.in @@ -103,7 +103,7 @@ function(_find_ydb_sdk_component CompName) message(FATAL_ERROR "${CompName} is not available component") endif() list(GET YDB-CPP-SDK_COMPONENT_TARGETS ${CompId} Tgt) - add_library(ydb-cpp-sdk::${CompName} ALIAS ydb-cpp-sdk::${Tgt}) + add_library(YDB-CPP-SDK::${CompName} ALIAS YDB-CPP-SDK::${Tgt}) set(${_ydb_sdk_package_name}_${CompName}_FOUND TRUE PARENT_SCOPE) endfunction() diff --git a/examples/basic_example/CMakeLists.txt b/examples/basic_example/CMakeLists.txt index 8e513439417..75d2d1b2538 100644 --- a/examples/basic_example/CMakeLists.txt +++ b/examples/basic_example/CMakeLists.txt @@ -3,9 +3,9 @@ add_executable(basic_example) target_link_libraries(basic_example PUBLIC yutil getopt - ydb-cpp-sdk::Query - ydb-cpp-sdk::Params - ydb-cpp-sdk::Driver + YDB-CPP-SDK::Query + YDB-CPP-SDK::Params + YDB-CPP-SDK::Driver ) target_sources(basic_example PRIVATE diff --git a/examples/bulk_upsert_simple/CMakeLists.txt b/examples/bulk_upsert_simple/CMakeLists.txt index 34b8ed62c3c..4f7c3eca7f9 100644 --- a/examples/bulk_upsert_simple/CMakeLists.txt +++ b/examples/bulk_upsert_simple/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(bulk_upsert_simple) target_link_libraries(bulk_upsert_simple PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(bulk_upsert_simple PRIVATE diff --git a/examples/pagination/CMakeLists.txt b/examples/pagination/CMakeLists.txt index 2b29726f007..0936f385585 100644 --- a/examples/pagination/CMakeLists.txt +++ b/examples/pagination/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(pagination) target_link_libraries(pagination PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(pagination PRIVATE diff --git a/examples/secondary_index/CMakeLists.txt b/examples/secondary_index/CMakeLists.txt index 47364c55979..6030de5f7f0 100644 --- a/examples/secondary_index/CMakeLists.txt +++ b/examples/secondary_index/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(secondary_index) target_link_libraries(secondary_index PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(secondary_index PRIVATE diff --git a/examples/secondary_index_builtin/CMakeLists.txt b/examples/secondary_index_builtin/CMakeLists.txt index e03e675827a..b46cc79159c 100644 --- a/examples/secondary_index_builtin/CMakeLists.txt +++ b/examples/secondary_index_builtin/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(secondary_index_builtin) target_link_libraries(secondary_index_builtin PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(secondary_index_builtin PRIVATE diff --git a/examples/topic_reader/eventloop/CMakeLists.txt b/examples/topic_reader/eventloop/CMakeLists.txt index 114a0ae35ba..2cdc984955f 100644 --- a/examples/topic_reader/eventloop/CMakeLists.txt +++ b/examples/topic_reader/eventloop/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(persqueue_reader_eventloop) target_link_libraries(persqueue_reader_eventloop PUBLIC yutil - ydb-cpp-sdk::Topic + YDB-CPP-SDK::Topic getopt ) diff --git a/examples/topic_reader/simple/CMakeLists.txt b/examples/topic_reader/simple/CMakeLists.txt index 2b7da165f05..68846ab215c 100644 --- a/examples/topic_reader/simple/CMakeLists.txt +++ b/examples/topic_reader/simple/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(simple_persqueue_reader) target_link_libraries(simple_persqueue_reader PUBLIC yutil - ydb-cpp-sdk::Topic + YDB-CPP-SDK::Topic getopt ) diff --git a/examples/topic_reader/transaction/CMakeLists.txt b/examples/topic_reader/transaction/CMakeLists.txt index 77fd8ab446e..64d30b4d8c6 100644 --- a/examples/topic_reader/transaction/CMakeLists.txt +++ b/examples/topic_reader/transaction/CMakeLists.txt @@ -2,7 +2,7 @@ add_executable(read_from_topic_in_transaction) target_link_libraries(read_from_topic_in_transaction PUBLIC yutil - ydb-cpp-sdk::Topic + YDB-CPP-SDK::Topic getopt ) diff --git a/examples/ttl/CMakeLists.txt b/examples/ttl/CMakeLists.txt index 9a0655e7d1b..48a004b4cc6 100644 --- a/examples/ttl/CMakeLists.txt +++ b/examples/ttl/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(ttl) target_link_libraries(ttl PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(ttl PRIVATE diff --git a/examples/vector_index/CMakeLists.txt b/examples/vector_index/CMakeLists.txt index 792d11cb3b1..19249951e37 100644 --- a/examples/vector_index/CMakeLists.txt +++ b/examples/vector_index/CMakeLists.txt @@ -4,7 +4,7 @@ target_link_libraries(vector_index PUBLIC yutil getopt - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table ) target_sources(vector_index PRIVATE diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 95ce1702d43..6133906d9c0 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -14,9 +14,9 @@ target_include_directories(ydb-odbc target_link_libraries(ydb-odbc PRIVATE - ydb-cpp-sdk::Query - ydb-cpp-sdk::Table - ydb-cpp-sdk::Driver + YDB-CPP-SDK::Query + YDB-CPP-SDK::Table + YDB-CPP-SDK::Driver ODBC::ODBC ) diff --git a/tests/integration/basic_example/CMakeLists.txt b/tests/integration/basic_example/CMakeLists.txt index 9eec918ec0e..55bdd05341b 100644 --- a/tests/integration/basic_example/CMakeLists.txt +++ b/tests/integration/basic_example/CMakeLists.txt @@ -6,9 +6,9 @@ add_ydb_test(NAME basic_example_it GTEST LINK_LIBRARIES yutil api-protos - ydb-cpp-sdk::Driver - ydb-cpp-sdk::Proto - ydb-cpp-sdk::Table + YDB-CPP-SDK::Driver + YDB-CPP-SDK::Proto + YDB-CPP-SDK::Table LABELS integration ) diff --git a/tests/integration/bulk_upsert/CMakeLists.txt b/tests/integration/bulk_upsert/CMakeLists.txt index 535d21f2d61..46848877c69 100644 --- a/tests/integration/bulk_upsert/CMakeLists.txt +++ b/tests/integration/bulk_upsert/CMakeLists.txt @@ -5,7 +5,7 @@ add_ydb_test(NAME bulk_upsert_it GTEST bulk_upsert.h LINK_LIBRARIES yutil - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table LABELS integration ) diff --git a/tests/integration/server_restart/CMakeLists.txt b/tests/integration/server_restart/CMakeLists.txt index 66d1c00d641..2d485de4e4a 100644 --- a/tests/integration/server_restart/CMakeLists.txt +++ b/tests/integration/server_restart/CMakeLists.txt @@ -4,7 +4,7 @@ add_ydb_test(NAME server_restart_it GTEST LINK_LIBRARIES yutil api-grpc - ydb-cpp-sdk::Query + YDB-CPP-SDK::Query gRPC::grpc++ LABELS integration diff --git a/tests/integration/sessions/CMakeLists.txt b/tests/integration/sessions/CMakeLists.txt index 0cc47bfd4cf..100c8ace2bc 100644 --- a/tests/integration/sessions/CMakeLists.txt +++ b/tests/integration/sessions/CMakeLists.txt @@ -3,8 +3,8 @@ add_ydb_test(NAME sessions_it GTEST main.cpp LINK_LIBRARIES yutil - ydb-cpp-sdk::Table - ydb-cpp-sdk::Query + YDB-CPP-SDK::Table + YDB-CPP-SDK::Query api-grpc grpc-client LABELS diff --git a/tests/integration/sessions_pool/CMakeLists.txt b/tests/integration/sessions_pool/CMakeLists.txt index d37d9d500eb..6e7a6a70ab7 100644 --- a/tests/integration/sessions_pool/CMakeLists.txt +++ b/tests/integration/sessions_pool/CMakeLists.txt @@ -3,7 +3,7 @@ add_ydb_test(NAME sessions_pool_it GTEST main.cpp LINK_LIBRARIES yutil - ydb-cpp-sdk::Table + YDB-CPP-SDK::Table api-grpc LABELS integration diff --git a/tests/unit/client/CMakeLists.txt b/tests/unit/client/CMakeLists.txt index 93c5610ba2c..95d7afa2284 100644 --- a/tests/unit/client/CMakeLists.txt +++ b/tests/unit/client/CMakeLists.txt @@ -35,8 +35,8 @@ add_ydb_test(NAME client-driver_ut driver/driver_ut.cpp LINK_LIBRARIES yutil - ydb-cpp-sdk::Driver - ydb-cpp-sdk::Table + YDB-CPP-SDK::Driver + YDB-CPP-SDK::Table LABELS unit ) From 958e565ab089cdbbd09b556ace792c53b996d0e3 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 26 May 2025 20:56:06 +0000 Subject: [PATCH 08/55] added tx support --- odbc/src/connection.cpp | 46 +++++++++++++++++++++++++-- odbc/src/connection.h | 17 +++++++--- odbc/src/odbc_driver.cpp | 58 ++++++++++++++++++++++++++++++++++ odbc/src/statement.cpp | 19 ++++++++--- odbc/src/utils/convert.cpp | 12 +++++-- odbc/tests/unit/convert_ut.cpp | 26 +++++++++------ 6 files changed, 154 insertions(+), 24 deletions(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index a2c0df7c545..6fbd8cf1e32 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -122,9 +122,49 @@ void TConnection::ClearErrors() { Errors_.clear(); } -std::pair TConnection::ParseConnectionString(const std::string& connectionString) { - // TODO: Implement - return {"", ""}; +SQLRETURN TConnection::SetAutocommit(bool value) { + Autocommit_ = value; + if (Autocommit_ && Tx_) { + auto status = Tx_->Commit().ExtractValueSync(); + if (!status.IsSuccess()) { + AddError("08001", 0, "Failed to commit transaction"); + return SQL_ERROR; + } + Tx_.reset(); + } + return SQL_SUCCESS; +} + +bool TConnection::GetAutocommit() const { + return Autocommit_; +} + +const std::optional& TConnection::GetTx() { + return Tx_; +} + +void TConnection::SetTx(const NQuery::TTransaction& tx) { + Tx_ = tx; +} + +SQLRETURN TConnection::CommitTx() { + auto status = Tx_->Commit().ExtractValueSync(); + if (!status.IsSuccess()) { + AddError("08001", 0, "Failed to commit transaction"); + return SQL_ERROR; + } + Tx_.reset(); + return SQL_SUCCESS; +} + +SQLRETURN TConnection::RollbackTx() { + auto status = Tx_->Rollback().ExtractValueSync(); + if (!status.IsSuccess()) { + AddError("08001", 0, "Failed to rollback transaction"); + return SQL_ERROR; + } + Tx_.reset(); + return SQL_SUCCESS; } } // namespace NOdbc diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 95c872f04ba..2b2fe22c81c 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -19,8 +19,9 @@ class TStatement; class TConnection { private: - std::unique_ptr YdbDriver_; - std::unique_ptr YdbClient_; + std::unique_ptr YdbDriver_; + std::unique_ptr YdbClient_; + std::optional Tx_; TErrorList Errors_; std::vector> Statements_; @@ -28,6 +29,8 @@ class TConnection { std::string Database_; std::string AuthToken_; + bool Autocommit_ = true; + public: SQLRETURN Connect(const std::string& serverName, const std::string& userName, @@ -46,8 +49,14 @@ class TConnection { void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); void ClearErrors(); -private: - std::pair ParseConnectionString(const std::string& connectionString); + SQLRETURN SetAutocommit(bool value); + bool GetAutocommit() const; + + const std::optional& GetTx(); + void SetTx(const NQuery::TTransaction& tx); + + SQLRETURN CommitTx(); + SQLRETURN RollbackTx(); }; } // namespace NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 6f3b865035f..26de48e6af5 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -250,4 +250,62 @@ SQLRETURN SQL_API SQLBindParameter(SQLHSTMT statementHandle, return stmt->BindParameter(paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr); } +SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT completionType) { + if (!handle) { + return SQL_INVALID_HANDLE; + } + try { + switch (handleType) { + case SQL_HANDLE_DBC: { + auto conn = static_cast(handle); + if (completionType == SQL_COMMIT) { + return conn->CommitTx(); + } else if (completionType == SQL_ROLLBACK) { + return conn->RollbackTx(); + } else { + return SQL_ERROR; + } + } + case SQL_HANDLE_STMT: { + auto stmt = static_cast(handle); + auto conn = stmt->GetConnection(); + if (!conn) return SQL_INVALID_HANDLE; + if (completionType == SQL_COMMIT) { + return conn->CommitTx(); + } else if (completionType == SQL_ROLLBACK) { + return conn->RollbackTx(); + } else { + return SQL_ERROR; + } + } + case SQL_HANDLE_ENV: { + // TODO: if's list of connections in ENV, go through them and commit/rollback transactions + return SQL_SUCCESS; + } + default: + return SQL_ERROR; + } + } catch (...) { + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { + auto conn = static_cast(connectionHandle); + if (!conn) { + return SQL_INVALID_HANDLE; + } + if (attribute == SQL_ATTR_AUTOCOMMIT) { + if ((intptr_t)value == SQL_AUTOCOMMIT_ON) { + return conn->SetAutocommit(true); + } else if ((intptr_t)value == SQL_AUTOCOMMIT_OFF) { + return conn->SetAutocommit(false); + } else { + return SQL_ERROR; + } + } + // TODO: other attributes + return SQL_ERROR; +} + } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index ab8318b0f99..3d19988a0bd 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -22,14 +22,23 @@ SQLRETURN TStatement::ExecDirect(const std::string& statementText) { return SQL_ERROR; } - auto sessionResult = client->GetSession().ExtractValueSync(); - if (!sessionResult.IsSuccess()) { - return SQL_ERROR; + if (!Conn_->GetTx()) { + auto sessionResult = client->GetSession().ExtractValueSync(); + if (!sessionResult.IsSuccess()) { + return SQL_ERROR; + } + auto session = sessionResult.GetSession(); + auto beginTxResult = session.BeginTransaction(NQuery::TTxSettings::SerializableRW()).ExtractValueSync(); + if (!beginTxResult.IsSuccess()) { + return SQL_ERROR; + } + Conn_->SetTx(beginTxResult.GetTransaction()); } - auto session = sessionResult.GetSession(); + auto session = Conn_->GetTx()->GetSession(); + auto iterator = session.StreamExecuteQuery(statementText, + NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(Conn_->GetAutocommit()), params).ExtractValueSync(); - auto iterator = session.StreamExecuteQuery(statementText, NYdb::NQuery::TTxControl::NoTx(), params).ExtractValueSync(); if (!iterator.IsSuccess()) { return SQL_ERROR; } diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 2c62d56347e..432418315a8 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -9,6 +9,7 @@ template struct TSqlTypeTraits; template<> struct TSqlTypeTraits { using Type = std::string; }; +template<> struct TSqlTypeTraits { using Type = std::string; }; template<> struct TSqlTypeTraits { using Type = SQLBIGINT; }; template<> struct TSqlTypeTraits { using Type = SQLUBIGINT; }; template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; @@ -38,6 +39,11 @@ TTypedValue::TTypedValue(const TBoundParam& param) { Data = std::string(static_cast(param.ParameterValuePtr), param.BufferLength); } +template<> +TTypedValue::TTypedValue(const TBoundParam& param) { + Data = std::string(static_cast(param.ParameterValuePtr), param.BufferLength); +} + class IConverter { public: virtual void AddToBuilder(const TBoundParam& param, TParamValueBuilder& builder) = 0; @@ -259,15 +265,15 @@ REGISTER_CONVERTER(SQL_C_CHAR, SQL_LONGVARCHAR, EPrimitiveType::Utf8) { // Binary types -REGISTER_CONVERTER(SQL_C_CHAR, SQL_BINARY, EPrimitiveType::String) { +REGISTER_CONVERTER(SQL_C_BINARY, SQL_BINARY, EPrimitiveType::String) { builder.OptionalString(std::move(data)); } -REGISTER_CONVERTER(SQL_C_CHAR, SQL_VARBINARY, EPrimitiveType::String) { +REGISTER_CONVERTER(SQL_C_BINARY, SQL_VARBINARY, EPrimitiveType::String) { builder.OptionalString(std::move(data)); } -REGISTER_CONVERTER(SQL_C_CHAR, SQL_LONGVARBINARY, EPrimitiveType::String) { +REGISTER_CONVERTER(SQL_C_BINARY, SQL_LONGVARBINARY, EPrimitiveType::String) { builder.OptionalString(std::move(data)); } diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp index 6df6be54f14..5c351a86771 100644 --- a/odbc/tests/unit/convert_ut.cpp +++ b/odbc/tests/unit/convert_ut.cpp @@ -12,7 +12,8 @@ using namespace NYdb::NOdbc; using namespace NYdb; -void CheckProtoValue(const Ydb::Value& value, const std::string& expected) { +template +void CheckProto(const T& value, const std::string& expected) { std::string protoStr; google::protobuf::TextFormat::PrintToString(value, &protoStr); ASSERT_EQ(protoStr, expected); @@ -36,7 +37,8 @@ TEST(OdbcConvert, Int64ToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - CheckProtoValue(value->GetProto(), "int64_value: 42\n"); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: INT64\n }\n}\n"); + CheckProto(value->GetProto(), "int64_value: 42\n"); } TEST(OdbcConvert, Uint64ToYdb) { @@ -49,7 +51,8 @@ TEST(OdbcConvert, Uint64ToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - CheckProtoValue(value->GetProto(), "uint64_value: 123\n"); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: UINT64\n }\n}\n"); + CheckProto(value->GetProto(), "uint64_value: 123\n"); } TEST(OdbcConvert, DoubleToYdb) { @@ -62,7 +65,8 @@ TEST(OdbcConvert, DoubleToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - CheckProtoValue(value->GetProto(), "double_value: 3.14\n"); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: DOUBLE\n }\n}\n"); + CheckProto(value->GetProto(), "double_value: 3.14\n"); } TEST(OdbcConvert, StringToYdbUtf8) { @@ -76,21 +80,23 @@ TEST(OdbcConvert, StringToYdbUtf8) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - CheckProtoValue(value->GetProto(), "text_value: \"hello\"\n"); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: UTF8\n }\n}\n"); + CheckProto(value->GetProto(), "text_value: \"hello\"\n"); } TEST(OdbcConvert, StringToYdbBinary) { const char* str = "bin\x01\x02"; SQLLEN len = 5; TBoundParam param{ - 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_BINARY, 0, 0, (SQLPOINTER)str, len, nullptr + 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 0, 0, (SQLPOINTER)str, len, nullptr }; TParamsBuilder paramsBuilder; ConvertValue(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - ASSERT_EQ(value->GetProto().bytes_value(), std::string(str, len)); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: STRING\n }\n}\n"); + CheckProto(value->GetProto(), "bytes_value: \"bin\\001\\002\"\n"); } TEST(OdbcConvert, Int64NullToYdb) { @@ -104,7 +110,8 @@ TEST(OdbcConvert, Int64NullToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - ASSERT_EQ(value->GetProto().null_flag_value(), ::google::protobuf::NullValue::NULL_VALUE); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: INT64\n }\n}\n"); + CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); } TEST(OdbcConvert, StringNullToYdb) { @@ -118,5 +125,6 @@ TEST(OdbcConvert, StringNullToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - ASSERT_EQ(value->GetProto().null_flag_value(), ::google::protobuf::NullValue::NULL_VALUE); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: UTF8\n }\n}\n"); + CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); } From 7ab1b67143b2ba5122f84475bc888254135ecd0e Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Fri, 30 May 2025 02:16:38 +0000 Subject: [PATCH 09/55] step --- odbc/CMakeLists.txt | 5 + odbc/examples/CMakeLists.txt | 1 + odbc/examples/scheme/CMakeLists.txt | 14 ++ odbc/examples/scheme/main.cpp | 143 +++++++++++ odbc/src/connection.cpp | 4 + odbc/src/connection.h | 6 + odbc/src/odbc_driver.cpp | 53 ++-- odbc/src/statement.cpp | 358 ++++++++++++++++------------ odbc/src/statement.h | 33 +-- odbc/src/utils/convert.cpp | 103 +++++++- odbc/src/utils/convert.h | 11 +- odbc/src/utils/result.cpp | 140 +++++++++++ odbc/src/utils/result.h | 38 +++ odbc/src/utils/types.cpp | 60 +++++ odbc/src/utils/types.h | 15 ++ odbc/src/utils/util.cpp | 12 + odbc/src/utils/util.h | 12 + odbc/tests/unit/convert_ut.cpp | 14 +- 18 files changed, 835 insertions(+), 187 deletions(-) create mode 100644 odbc/examples/scheme/CMakeLists.txt create mode 100644 odbc/examples/scheme/main.cpp create mode 100644 odbc/src/utils/result.cpp create mode 100644 odbc/src/utils/result.h create mode 100644 odbc/src/utils/types.cpp create mode 100644 odbc/src/utils/types.h create mode 100644 odbc/src/utils/util.cpp create mode 100644 odbc/src/utils/util.h diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 6133906d9c0..eb47edc922a 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,4 +1,7 @@ add_library(ydb-odbc SHARED + src/utils/result.cpp + src/utils/types.cpp + src/utils/util.cpp src/utils/convert.cpp src/odbc_driver.cpp src/connection.cpp @@ -9,6 +12,7 @@ add_library(ydb-odbc SHARED target_include_directories(ydb-odbc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src ${ODBC_INCLUDE_DIRS} ) @@ -16,6 +20,7 @@ target_link_libraries(ydb-odbc PRIVATE YDB-CPP-SDK::Query YDB-CPP-SDK::Table + YDB-CPP-SDK::Scheme YDB-CPP-SDK::Driver ODBC::ODBC ) diff --git a/odbc/examples/CMakeLists.txt b/odbc/examples/CMakeLists.txt index 6f4cd2f5a31..88b1f27cc60 100644 --- a/odbc/examples/CMakeLists.txt +++ b/odbc/examples/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(basic) +add_subdirectory(scheme) diff --git a/odbc/examples/scheme/CMakeLists.txt b/odbc/examples/scheme/CMakeLists.txt new file mode 100644 index 00000000000..abca75eb735 --- /dev/null +++ b/odbc/examples/scheme/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(odbc_scheme + main.cpp +) + +target_link_libraries(odbc_scheme + PRIVATE + ODBC::ODBC +) +target_compile_definitions(odbc_scheme + PRIVATE + ODBC_DRIVER_PATH="$" +) + +add_dependencies(odbc_scheme ydb-odbc) diff --git a/odbc/examples/scheme/main.cpp b/odbc/examples/scheme/main.cpp new file mode 100644 index 00000000000..3228ba2470b --- /dev/null +++ b/odbc/examples/scheme/main.cpp @@ -0,0 +1,143 @@ +#include +#include + +#include + +void PrintOdbcError(SQLSMALLINT handleType, SQLHANDLE handle) { + SQLCHAR sqlState[6] = {0}; + SQLINTEGER nativeError = 0; + SQLCHAR message[256] = {0}; + SQLSMALLINT textLength = 0; + SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError, message, sizeof(message), &textLength); + std::cerr << "ODBC error: [" << sqlState << "] " << message << std::endl; +} + +int main() { + SQLHENV henv = nullptr; + SQLHDBC hdbc = nullptr; + SQLHSTMT hstmt = nullptr; + SQLRETURN ret; + + std::cout << "1. Allocating environment handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating environment handle" << std::endl; + return 1; + } + SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); + + std::cout << "2. Allocating connection handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating connection handle" << std::endl; + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "3. Building connection string" << std::endl; + std::string connStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; + SQLCHAR outConnStr[1024] = {0}; + SQLSMALLINT outConnStrLen = 0; + + std::cout << "4. Connecting with SQLDriverConnect" << std::endl; + ret = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)connStr.c_str(), SQL_NTS, + outConnStr, sizeof(outConnStr), &outConnStrLen, SQL_DRIVER_COMPLETE); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error connecting with SQLDriverConnect" << std::endl; + PrintOdbcError(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "5. Allocating statement handle" << std::endl; + ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error allocating statement handle" << std::endl; + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + // std::cout << "6. Executing query" << std::endl; + // SQLCHAR query[] = R"( + // DECLARE $p1 AS Int64?; + // SELECT $p1 + 1, 'test1'; + // SELECT $p1 + 2, 'test2'; + // SELECT $p1 + 3, 'test3'; + // SELECT $p1 + 4, 'test4'; + // SELECT $p1 + 5, 'test5'; + // SELECT $p1 + 6, 'test6'; + // SELECT $p1 + 7, 'test7'; + // SELECT $p1 + 8, 'test8'; + // SELECT $p1 + 9, 'test9'; + // )"; + + // int64_t paramValue = 42; + // SQLLEN paramInd = 0; + // ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, ¶mValue, 0, ¶mInd); + // if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + // std::cerr << "Error binding parameter" << std::endl; + // PrintOdbcError(SQL_HANDLE_STMT, hstmt); + // SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + // SQLDisconnect(hdbc); + // SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + // SQLFreeHandle(SQL_HANDLE_ENV, henv); + // return 1; + // } + + std::cout << "6. Getting tables" << std::endl; + + SQLCHAR pattern[] = "/local"; + SQLCHAR tableType[] = "TABLE"; + + ret = SQLTables(hstmt, NULL, 0, NULL, 0, pattern, SQL_NTS, tableType, SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + std::cerr << "Error executing query" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + return 1; + } + + std::cout << "7. Fetching result" << std::endl; + + SQLLEN ind = 0; + SQLCHAR value1[1024] = {0}; + if (SQLBindCol(hstmt, 3, SQL_C_CHAR, &value1, 1024, &ind) != SQL_SUCCESS) { + std::cerr << "Error binding column 1" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + SQLCHAR value2[1024] = {0}; + if (SQLBindCol(hstmt, 4, SQL_C_CHAR, &value2, 1024, &ind) != SQL_SUCCESS) { + std::cerr << "Error binding column 2" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + while ((ret = SQLFetch(hstmt)) == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) { + if (ret != SQL_SUCCESS) { + std::cerr << "Error fetching result" << std::endl; + PrintOdbcError(SQL_HANDLE_STMT, hstmt); + return 1; + } + + std::cout << "Result column 1: " << value1 << std::endl; + std::cout << "Result column 2: " << value2 << std::endl; + + std::cout << "--------------------------------" << std::endl; + } + + std::cout << "8. Cleaning up" << std::endl; + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + SQLDisconnect(hdbc); + SQLFreeHandle(SQL_HANDLE_DBC, hdbc); + SQLFreeHandle(SQL_HANDLE_ENV, henv); + + return 0; +} diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 6fbd8cf1e32..7806096bc63 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -44,6 +44,8 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { .SetDatabase(Database_)); YdbClient_ = std::make_unique(*YdbDriver_); + YdbSchemeClient_ = std::make_unique(*YdbDriver_); + YdbTableClient_ = std::make_unique(*YdbDriver_); return SQL_SUCCESS; } @@ -71,6 +73,8 @@ SQLRETURN TConnection::Connect(const std::string& serverName, .SetDatabase(Database_)); YdbClient_ = std::make_unique(*YdbDriver_); + YdbSchemeClient_ = std::make_unique(*YdbDriver_); + YdbTableClient_ = std::make_unique(*YdbDriver_); return SQL_SUCCESS; } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 2b2fe22c81c..fad81527772 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include @@ -21,6 +23,8 @@ class TConnection { private: std::unique_ptr YdbDriver_; std::unique_ptr YdbClient_; + std::unique_ptr YdbTableClient_; + std::unique_ptr YdbSchemeClient_; std::optional Tx_; TErrorList Errors_; @@ -45,6 +49,8 @@ class TConnection { void RemoveStatement(TStatement* stmt); NYdb::NQuery::TQueryClient* GetClient() { return YdbClient_.get(); } + NYdb::NTable::TTableClient* GetTableClient() { return YdbTableClient_.get(); } + NScheme::TSchemeClient* GetSchemeClient() { return YdbSchemeClient_.get(); } void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); void ClearErrors(); diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 26de48e6af5..9e7a7d3aee7 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -2,18 +2,11 @@ #include "connection.h" #include "statement.h" +#include "utils/util.h" + #include #include -namespace { - std::string GetString(SQLCHAR* str, SQLSMALLINT length) { - if (length == SQL_NTS) { - return std::string(reinterpret_cast(str)); - } - return std::string(reinterpret_cast(str), length); - } -} - extern "C" { SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, @@ -122,7 +115,7 @@ SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, return SQL_INVALID_HANDLE; } - return conn->DriverConnect(GetString(inConnectionString, stringLength1)); + return conn->DriverConnect(NYdb::NOdbc::GetString(inConnectionString, stringLength1)); } SQLRETURN SQL_API SQLConnect(SQLHDBC connectionHandle, @@ -134,9 +127,9 @@ SQLRETURN SQL_API SQLConnect(SQLHDBC connectionHandle, return SQL_INVALID_HANDLE; } - return conn->Connect(GetString(serverName, nameLength1), - GetString(userName, nameLength2), - GetString(authentication, nameLength3)); + return conn->Connect(NYdb::NOdbc::GetString(serverName, nameLength1), + NYdb::NOdbc::GetString(userName, nameLength2), + NYdb::NOdbc::GetString(authentication, nameLength3)); } SQLRETURN SQL_API SQLDisconnect(SQLHDBC connectionHandle) { @@ -156,7 +149,7 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, return SQL_INVALID_HANDLE; } - return stmt->ExecDirect(GetString(statementText, textLength)); + return stmt->ExecDirect(NYdb::NOdbc::GetString(statementText, textLength)); } SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { @@ -308,4 +301,36 @@ SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribu return SQL_ERROR; } +SQLRETURN SQL_API SQLColumns(SQLHSTMT statementHandle, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3, + SQLCHAR* columnName, SQLSMALLINT nameLength4) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->Columns( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + NYdb::NOdbc::GetString(columnName, nameLength4)); +} + +SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3, + SQLCHAR* tableType, SQLSMALLINT nameLength4) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->Tables( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + NYdb::NOdbc::GetString(tableType, nameLength4)); +} + } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 3d19988a0bd..856eb5b3417 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -1,5 +1,7 @@ #include "statement.h" +#include "utils/types.h" + #include #include @@ -43,60 +45,25 @@ SQLRETURN TStatement::ExecDirect(const std::string& statementText) { return SQL_ERROR; } - Iterator_ = std::make_unique(std::move(iterator)); + ResultSet_ = CreateExecResultSet(std::move(iterator)); return SQL_SUCCESS; } SQLRETURN TStatement::Fetch() { - if (!Iterator_) { + if (!ResultSet_) { ClearStatement(); return SQL_NO_DATA; } - - while (true) { - if (ResultSetParser_) { - if (ResultSetParser_->TryNextRow()) { - for (const auto& col : BoundColumns_) { - GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); - } - return SQL_SUCCESS; - } - - ResultSetParser_.reset(); - } - - auto part = Iterator_->ReadNext().ExtractValueSync(); - if (part.EOS()) { - ClearStatement(); - return SQL_NO_DATA; - } - - if (!part.IsSuccess()) { - // AddError(part.GetStatus().GetStatus().GetCode(), part.GetStatus().GetStatus().GetReason()); - ClearStatement(); - return SQL_ERROR; - } - - if (part.HasResultSet()) { - ResultSetParser_ = std::make_unique(part.ExtractResultSet()); - } - } - - return SQL_SUCCESS; + return ResultSet_->Fetch() ? SQL_SUCCESS : SQL_NO_DATA; } SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - if (!ResultSetParser_) { + if (!ResultSet_) { return SQL_NO_DATA; } - - if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { - return SQL_ERROR; - } - - return ConvertYdbValue(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); + return ResultSet_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); } SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, @@ -124,20 +91,11 @@ SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLIN return SQL_SUCCESS; } -SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, - SQLSMALLINT targetType, - SQLPOINTER targetValue, - SQLLEN bufferLength, - SQLLEN* strLenOrInd) { - - BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), - [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); - - if (!targetValue) { - return SQL_SUCCESS; +SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { + if (!ResultSet_) { + return SQL_NO_DATA; } - BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); - return SQL_SUCCESS; + return ResultSet_->BindCol(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); } SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, @@ -174,117 +132,225 @@ void TStatement::ClearErrors() { } void TStatement::ClearStatement() { - Iterator_.reset(); - ResultSetParser_.reset(); - BoundColumns_.clear(); + ResultSet_.reset(); } -SQLRETURN TStatement::ConvertYdbValue(NYdb::TValueParser& valueParser, - SQLSMALLINT targetType, - SQLPOINTER targetValue, - SQLLEN bufferLength, - SQLLEN* strLenOrInd) { - - if (valueParser.IsNull()) { - if (strLenOrInd) *strLenOrInd = SQL_NULL_DATA; - return SQL_SUCCESS; +NYdb::TParams TStatement::BuildParams() { + Errors_.clear(); + NYdb::TParamsBuilder paramsBuilder; + for (const auto& param : BoundParams_) { + std::string paramName = "$p" + std::to_string(param.ParamNumber); + ConvertParam(param, paramsBuilder.AddParam(paramName)); } - if (valueParser.GetKind() == TTypeParser::ETypeKind::Optional) { - valueParser.OpenOptional(); - SQLRETURN ret = ConvertYdbValue(valueParser, targetType, targetValue, bufferLength, strLenOrInd); - valueParser.CloseOptional(); - return ret; - } + return paramsBuilder.Build(); +} + +SQLRETURN TStatement::Columns(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& columnName) { + ClearErrors(); + ClearStatement(); - if (valueParser.GetKind() != TTypeParser::ETypeKind::Primitive) { + std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"TYPE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_SIZE", SQL_INTEGER, 0, SQL_NULLABLE}, + {"BUFFER_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, + {"DECIMAL_DIGITS", SQL_INTEGER, 0, SQL_NULLABLE}, + {"NUM_PREC_RADIX", SQL_INTEGER, 0, SQL_NULLABLE}, + {"NULLABLE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"REMARKS", SQL_VARCHAR, 762, SQL_NULLABLE}, + {"COLUMN_DEF", SQL_VARCHAR, 254, SQL_NULLABLE}, + {"SQL_DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"SQL_DATETIME_SUB", SQL_INTEGER, 0, SQL_NULLABLE}, + {"CHAR_OCTET_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, + {"ORDINAL_POSITION", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"IS_NULLABLE", SQL_VARCHAR, 254, SQL_NO_NULLS} + }; + + auto entries = GetPatternEntries(tableName); + if (entries.empty()) { + AddError("HYC00", 0, "No tables found"); return SQL_ERROR; } - EPrimitiveType ydbType = valueParser.GetPrimitiveType(); - - switch (targetType) { - case SQL_C_SLONG: - { - int32_t v = 0; - switch (ydbType) { - case EPrimitiveType::Int32: v = valueParser.GetInt32(); break; - case EPrimitiveType::Uint32: v = static_cast(valueParser.GetUint32()); break; - case EPrimitiveType::Int64: v = static_cast(valueParser.GetInt64()); break; - case EPrimitiveType::Uint64: v = static_cast(valueParser.GetUint64()); break; - case EPrimitiveType::Bool: v = valueParser.GetBool() ? 1 : 0; break; - default: return SQL_ERROR; - } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(int32_t); - return SQL_SUCCESS; - } - case SQL_C_SBIGINT: - { - SQLBIGINT v = 0; - switch (ydbType) { - case EPrimitiveType::Int64: v = valueParser.GetInt64(); break; - case EPrimitiveType::Uint64: v = static_cast(valueParser.GetUint64()); break; - case EPrimitiveType::Int32: v = static_cast(valueParser.GetInt32()); break; - case EPrimitiveType::Uint32: v = static_cast(valueParser.GetUint32()); break; - default: return SQL_ERROR; - } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(SQLBIGINT); - return SQL_SUCCESS; - } - case SQL_C_DOUBLE: - { - double v = 0.0; - switch (ydbType) { - case EPrimitiveType::Double: v = valueParser.GetDouble(); break; - case EPrimitiveType::Float: v = valueParser.GetFloat(); break; - default: return SQL_ERROR; - } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(double); - return SQL_SUCCESS; + TTable table; + table.reserve(entries.size()); + + for (const auto& entry : entries) { + if (entry.Type != NScheme::ESchemeEntryType::Table && + entry.Type != NScheme::ESchemeEntryType::ColumnTable) { + continue; } - case SQL_C_CHAR: - { - std::string str; - switch (ydbType) { - case EPrimitiveType::Utf8: str = valueParser.GetUtf8(); break; - case EPrimitiveType::String: str = valueParser.GetString(); break; - case EPrimitiveType::Json: str = valueParser.GetJson(); break; - case EPrimitiveType::JsonDocument: str = valueParser.GetJsonDocument(); break; - default: return SQL_ERROR; + + auto status = Conn_->GetTableClient()->RetryOperationSync([path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { + auto result = session.DescribeTable(path).ExtractValueSync(); + if (!result.IsSuccess()) { + return result; } - SQLLEN len = str.size(); - if (targetValue && bufferLength > 0) { - SQLLEN copyLen = std::min(len, bufferLength - 1); - memcpy(targetValue, str.data(), copyLen); - reinterpret_cast(targetValue)[copyLen] = 0; + auto columns = result.GetTableDescription().GetTableColumns(); + + auto columnIt = std::find_if(columns.begin(), columns.end(), [&columnName](const NTable::TTableColumn& column) { + return column.Name == columnName; + }); + + if (columnIt == columns.end()) { + return TStatus(EStatus::NOT_FOUND, { NYdb::NIssue::TIssue("Column not found") }); } - if (strLenOrInd) *strLenOrInd = len; - return SQL_SUCCESS; + + auto column = *columnIt; + + TTypeParser typeParser(column.Type); + + table.push_back({ + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Utf8(path).Build(), + TValueBuilder().Utf8(column.Name).Build(), + TValueBuilder().Int16(GetTypeId(column.Type)).Build(), + TValueBuilder().Utf8(column.Type.ToString()).Build(), + TValueBuilder().OptionalInt32(std::nullopt).Build(), + TValueBuilder().OptionalInt32(std::nullopt).Build(), + TValueBuilder().OptionalInt16(GetDecimalDigits(column.Type)).Build(), + TValueBuilder().OptionalInt16(GetRadix(column.Type)).Build(), + TValueBuilder().Int16(column.NotNull && *column.NotNull ? SQL_NO_NULLS : SQL_NULLABLE).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Int16(GetTypeId(column.Type)).Build(), + TValueBuilder().OptionalInt16(std::nullopt).Build(), + TValueBuilder().OptionalInt32(8).Build(), + TValueBuilder().OptionalInt32(columnIt - columns.begin() + 1).Build(), + TValueBuilder().Utf8(column.NotNull && *column.NotNull ? "NO" : "YES").Build(), + }); + return TStatus(EStatus::SUCCESS, {}); + }); + + if (!status.IsSuccess()) { + return SQL_ERROR; } - case SQL_C_BIT: - { - char v = valueParser.GetBool() ? 1 : 0; - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(char); - return SQL_SUCCESS; + } + + ResultSet_ = CreateVirtualResultSet(columns, table); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::Tables(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& tableType) { + ClearErrors(); + ClearStatement(); + + std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"TABLE_TYPE", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"REMARKS", SQL_VARCHAR, 254, SQL_NULLABLE} + }; + + auto entries = GetPatternEntries(tableName); + if (entries.empty()) { + AddError("HYC00", 0, "No tables found"); + return SQL_ERROR; + } + + TTable table; + table.reserve(entries.size()); + + for (const auto& entry : entries) { + auto tableType = GetTableType(entry.Type); + if (!tableType) { + continue; } - default: - return SQL_ERROR; + + std::cout << "Table name: " << entry.Name << " type: " << *tableType << std::endl; + + table.push_back({ + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Utf8(entry.Name).Build(), + TValueBuilder().Utf8(*tableType).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + }); } + + ResultSet_ = CreateVirtualResultSet(columns, table); + return SQL_SUCCESS; } -NYdb::TParams TStatement::BuildParams() { - Errors_.clear(); - NYdb::TParamsBuilder paramsBuilder; - for (const auto& param : BoundParams_) { - std::string paramName = "$p" + std::to_string(param.ParamNumber); - ConvertValue(param, paramsBuilder.AddParam(paramName)); +std::vector TStatement::GetPatternEntries(const std::string& pattern) { + std::vector entries; + VisitEntry("", pattern, entries); + return entries; +} + +SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries) { + auto schemeClient = Conn_->GetSchemeClient(); + auto listDirectoryResult = schemeClient->ListDirectory(path + "/").ExtractValueSync(); + if (!listDirectoryResult.IsSuccess()) { + return SQL_ERROR; + } + for (const auto& entry : listDirectoryResult.GetChildren()) { + std::string fullPath = path + "/" + entry.Name; + if (entry.Type == NScheme::ESchemeEntryType::Directory || + entry.Type == NScheme::ESchemeEntryType::SubDomain) { + VisitEntry(fullPath, pattern, resultEntries); + } else if (IsPatternMatch(fullPath, pattern)) { + NScheme::TSchemeEntry entryCopy = entry; + entryCopy.Name = fullPath; + resultEntries.push_back(entryCopy); + } } + return SQL_SUCCESS; +} - return paramsBuilder.Build(); +bool TStatement::IsPatternMatch(const std::string& path, const std::string& pattern) { + return path.starts_with(pattern); +} + +std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { + switch (type) { + case NScheme::ESchemeEntryType::Table: + return "TABLE"; + case NScheme::ESchemeEntryType::View: + return "VIEW"; + case NScheme::ESchemeEntryType::ColumnStore: + return "COLUMN_STORE"; + case NScheme::ESchemeEntryType::ColumnTable: + return "COLUMN_TABLE"; + case NScheme::ESchemeEntryType::Sequence: + return "SEQUENCE"; + case NScheme::ESchemeEntryType::Replication: + return "REPLICATION"; + case NScheme::ESchemeEntryType::Topic: + return "TOPIC"; + case NScheme::ESchemeEntryType::ExternalTable: + return "EXTERNAL_TABLE"; + case NScheme::ESchemeEntryType::ExternalDataSource: + return "EXTERNAL_DATA_SOURCE"; + case NScheme::ESchemeEntryType::ResourcePool: + return "RESOURCE_POOL"; + case NScheme::ESchemeEntryType::PqGroup: + return "PQ_GROUP"; + case NScheme::ESchemeEntryType::RtmrVolume: + return "RTMR_VOLUME"; + case NScheme::ESchemeEntryType::BlockStoreVolume: + return "BLOCK_STORE_VOLUME"; + case NScheme::ESchemeEntryType::CoordinationNode: + return "COORDINATION_NODE"; + case NScheme::ESchemeEntryType::Unknown: + return "UNKNOWN"; + case NScheme::ESchemeEntryType::Directory: + case NScheme::ESchemeEntryType::SubDomain: + return std::nullopt; + } } } // namespace NOdbc diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 8f51be6759b..b4568b97fb8 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -1,6 +1,7 @@ #pragma once #include "connection.h" +#include "utils/result.h" #include "utils/convert.h" #include @@ -17,15 +18,6 @@ namespace NYdb { namespace NOdbc { class TStatement { -private: - struct TBoundColumn { - SQLUSMALLINT ColumnNumber; - SQLSMALLINT TargetType; - SQLPOINTER TargetValue; - SQLLEN BufferLength; - SQLLEN* StrLenOrInd; - }; - public: TStatement(TConnection* conn); @@ -40,6 +32,16 @@ class TStatement { SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); + SQLRETURN Columns(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& columnName); + + SQLRETURN Tables(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& tableType); + TConnection* GetConnection() { return Conn_; } @@ -49,16 +51,19 @@ class TStatement { NYdb::TParams BuildParams(); -private: void ClearStatement(); - SQLRETURN ConvertYdbValue(NYdb::TValueParser& valueParser, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); +private: + std::vector GetPatternEntries(const std::string& pattern); + SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); + bool IsPatternMatch(const std::string& path, const std::string& pattern); + + std::optional GetTableType(NScheme::ESchemeEntryType type); TConnection* Conn_; TErrorList Errors_; - std::unique_ptr Iterator_; - std::unique_ptr ResultSetParser_; + + std::unique_ptr ResultSet_; std::vector BoundColumns_; std::vector BoundParams_; diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 432418315a8..b10f839dc11 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -279,12 +279,105 @@ REGISTER_CONVERTER(SQL_C_BINARY, SQL_LONGVARBINARY, EPrimitiveType::String) { #undef REGISTER_CONVERTER -void ConvertValue(const TBoundParam& param, TParamValueBuilder& builder) { +SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder) { auto converter = TConverterRegistry::GetInstance().GetConverter(param.ValueType, param.ParameterType); - if (converter) { - converter->AddToBuilder(param, builder); - } else { - throw 1; // TODO: throw exception + if (!converter) { + return SQL_ERROR; + } + + converter->AddToBuilder(param, builder); + return SQL_SUCCESS; +} + +SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { + if (parser.IsNull()) { + if (strLenOrInd) *strLenOrInd = SQL_NULL_DATA; + return SQL_SUCCESS; + } + + if (parser.GetKind() == TTypeParser::ETypeKind::Optional) { + parser.OpenOptional(); + SQLRETURN ret = ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); + parser.CloseOptional(); + return ret; + } + + if (parser.GetKind() != TTypeParser::ETypeKind::Primitive) { + return SQL_ERROR; + } + + EPrimitiveType ydbType = parser.GetPrimitiveType(); + + switch (targetType) { + case SQL_C_SLONG: + { + int32_t v = 0; + switch (ydbType) { + case EPrimitiveType::Int32: v = parser.GetInt32(); break; + case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; + case EPrimitiveType::Int64: v = static_cast(parser.GetInt64()); break; + case EPrimitiveType::Uint64: v = static_cast(parser.GetUint64()); break; + case EPrimitiveType::Bool: v = parser.GetBool() ? 1 : 0; break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(int32_t); + return SQL_SUCCESS; + } + case SQL_C_SBIGINT: + { + SQLBIGINT v = 0; + switch (ydbType) { + case EPrimitiveType::Int64: v = parser.GetInt64(); break; + case EPrimitiveType::Uint64: v = static_cast(parser.GetUint64()); break; + case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; + case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(SQLBIGINT); + return SQL_SUCCESS; + } + case SQL_C_DOUBLE: + { + double v = 0.0; + switch (ydbType) { + case EPrimitiveType::Double: v = parser.GetDouble(); break; + case EPrimitiveType::Float: v = parser.GetFloat(); break; + default: return SQL_ERROR; + } + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(double); + return SQL_SUCCESS; + } + case SQL_C_CHAR: + { + std::string str; + switch (ydbType) { + case EPrimitiveType::Utf8: str = parser.GetUtf8(); break; + case EPrimitiveType::String: str = parser.GetString(); break; + case EPrimitiveType::Json: str = parser.GetJson(); break; + case EPrimitiveType::JsonDocument: str = parser.GetJsonDocument(); break; + default: return SQL_ERROR; + } + SQLLEN len = str.size(); + if (targetValue && bufferLength > 0) { + SQLLEN copyLen = std::min(len, bufferLength - 1); + memcpy(targetValue, str.data(), copyLen); + reinterpret_cast(targetValue)[copyLen] = 0; + } + if (strLenOrInd) *strLenOrInd = len; + return SQL_SUCCESS; + } + case SQL_C_BIT: + { + char v = parser.GetBool() ? 1 : 0; + if (targetValue) *reinterpret_cast(targetValue) = v; + if (strLenOrInd) *strLenOrInd = sizeof(char); + return SQL_SUCCESS; + } + default: + return SQL_ERROR; } } diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h index 525a43c79aa..dba81c2b345 100644 --- a/odbc/src/utils/convert.h +++ b/odbc/src/utils/convert.h @@ -20,7 +20,16 @@ struct TBoundParam { SQLLEN* StrLenOrIndPtr; }; -void ConvertValue(const TBoundParam& param, TParamValueBuilder& builder); +struct TBoundColumn { + SQLUSMALLINT ColumnNumber; + SQLSMALLINT TargetType; + SQLPOINTER TargetValue; + SQLLEN BufferLength; + SQLLEN* StrLenOrInd; +}; + +SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder); +SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); } // namespace NYdb } // namespace NOdbc diff --git a/odbc/src/utils/result.cpp b/odbc/src/utils/result.cpp new file mode 100644 index 00000000000..ca80f5b0b20 --- /dev/null +++ b/odbc/src/utils/result.cpp @@ -0,0 +1,140 @@ +#include "result.h" + +#include "convert.h" + +namespace NYdb { +namespace NOdbc { + +class TCommonResultSet : public IResultSet { +public: + SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), + [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); + if (!targetValue) { + return SQL_SUCCESS; + } + BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); + return SQL_SUCCESS; + } + +protected: + void FillBoundColumns() { + for (const auto& col : BoundColumns_) { + GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + } + } + +protected: + std::vector BoundColumns_; +}; + +class TExecResultSet : public TCommonResultSet { +public: + TExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator) + : Iterator_(std::move(iterator)) {} + + bool Fetch() override { + while (true) { + if (ResultSetParser_) { + if (ResultSetParser_->TryNextRow()) { + FillBoundColumns(); + return true; + } + ResultSetParser_.reset(); + } + auto part = Iterator_.ReadNext().ExtractValueSync(); + if (part.EOS()) { + return false; + } + if (!part.IsSuccess()) { + return false; + } + if (part.HasResultSet()) { + ResultSetParser_ = std::make_unique(part.ExtractResultSet()); + } + } + return false; + } + + SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + if (!ResultSetParser_) { + return SQL_NO_DATA; + } + if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { + return SQL_ERROR; + } + return ConvertColumn(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); + } + + size_t ColumnsCount() const override { + return ResultSetParser_ ? ResultSetParser_->ColumnsCount() : 0; + } + + const TColumnMeta& GetColumnMeta(size_t index) const override { + // TODO: implement return column metadata + static TColumnMeta dummy; + return dummy; + } + +private: + NYdb::NQuery::TExecuteQueryIterator Iterator_; + std::unique_ptr ResultSetParser_; +}; + +class TVirtualResultSet : public TCommonResultSet { +public: + TVirtualResultSet(const std::vector& columns, const TTable& table) + : Columns_(columns), Table_(table) { + std::cout << "TVirtualResultSet constructor" << std::endl; + std::cout << "Columns count: " << Columns_.size() << std::endl; + std::cout << "Table size: " << Table_.size() << std::endl; + } + + bool Fetch() override { + std::cout << "Fetching row " << Cursor_ << std::endl; + Cursor_++; + if (Cursor_ >= static_cast(Table_.size())) { + return false; + } + FillBoundColumns(); + return true; + } + + SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + if (Cursor_ >= static_cast(Table_.size())) { + return SQL_NO_DATA; + } + if (Cursor_ < 0 || columnNumber < 1 || columnNumber > Columns_.size()) { + return SQL_ERROR; + } + TValueParser parser{Table_[Cursor_][columnNumber - 1]}; + return ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); + } + + size_t ColumnsCount() const override { + return Columns_.size(); + } + + const TColumnMeta& GetColumnMeta(size_t index) const override { + return Columns_[index]; + } + +private: + std::vector Columns_; + TTable Table_; + int64_t Cursor_ = -1; +}; + +std::unique_ptr CreateExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator) { + return std::make_unique(std::move(iterator)); +} + +std::unique_ptr CreateVirtualResultSet(const std::vector& columns, const TTable& table) { + return std::make_unique(columns, table); +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/result.h b/odbc/src/utils/result.h new file mode 100644 index 00000000000..e5334038a25 --- /dev/null +++ b/odbc/src/utils/result.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +struct TColumnMeta { + std::string Name; + SQLSMALLINT SqlType; + SQLULEN Size; + SQLSMALLINT Nullable; +}; + +using TTable = std::vector>; + +class IResultSet { +public: + virtual ~IResultSet() = default; + virtual bool Fetch() = 0; + virtual SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; + virtual SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; + virtual size_t ColumnsCount() const = 0; + virtual const TColumnMeta& GetColumnMeta(size_t index) const = 0; +}; + +std::unique_ptr CreateExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator); +std::unique_ptr CreateVirtualResultSet(const std::vector& columns, const TTable& table); + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/types.cpp b/odbc/src/utils/types.cpp new file mode 100644 index 00000000000..aa038420e53 --- /dev/null +++ b/odbc/src/utils/types.cpp @@ -0,0 +1,60 @@ +#include "types.h" + +namespace NYdb { +namespace NOdbc { + +SQLINTEGER GetTypeId(const TType& type) { + return 0; +} + +std::optional GetDecimalDigits(const TType& type) { + TTypeParser typeParser(type); + if (typeParser.GetKind() != TTypeParser::ETypeKind::Primitive) { + return std::nullopt; + } + + switch (typeParser.GetPrimitive()) { + case EPrimitiveType::Int64: + return 64; + case EPrimitiveType::Uint64: + return 64; + case EPrimitiveType::Int32: + return 32; + case EPrimitiveType::Uint32: + return 32; + case EPrimitiveType::Int16: + return 16; + case EPrimitiveType::Uint16: + return 16; + case EPrimitiveType::Int8: + return 8; + case EPrimitiveType::Uint8: + return 8; + default: + return std::nullopt; + } +} + +std::optional GetRadix(const TType& type) { + TTypeParser typeParser(type); + if (typeParser.GetKind() != TTypeParser::ETypeKind::Primitive) { + return std::nullopt; + } + + switch (typeParser.GetPrimitive()) { + case EPrimitiveType::Int64: + case EPrimitiveType::Uint64: + case EPrimitiveType::Int32: + case EPrimitiveType::Uint32: + case EPrimitiveType::Int16: + case EPrimitiveType::Uint16: + case EPrimitiveType::Int8: + case EPrimitiveType::Uint8: + return 10; + default: + return std::nullopt; + } +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/types.h b/odbc/src/utils/types.h new file mode 100644 index 00000000000..0b9dd76aa13 --- /dev/null +++ b/odbc/src/utils/types.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +namespace NYdb { +namespace NOdbc { + +SQLINTEGER GetTypeId(const TType& type); +std::optional GetDecimalDigits(const TType& type); +std::optional GetRadix(const TType& type); + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp new file mode 100644 index 00000000000..9097ce80dbf --- /dev/null +++ b/odbc/src/utils/util.cpp @@ -0,0 +1,12 @@ +#include "util.h" + +namespace NYdb::NOdbc { + +std::string GetString(SQLCHAR* str, SQLSMALLINT length) { + if (length == SQL_NTS) { + return std::string(reinterpret_cast(str)); + } + return std::string(reinterpret_cast(str), length); +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/util.h b/odbc/src/utils/util.h new file mode 100644 index 00000000000..b17fe2c235f --- /dev/null +++ b/odbc/src/utils/util.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +#include + +namespace NYdb::NOdbc { + +std::string GetString(SQLCHAR* str, SQLSMALLINT length); + +} // namespace NYdb::NOdbc diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp index 5c351a86771..f4bad34a366 100644 --- a/odbc/tests/unit/convert_ut.cpp +++ b/odbc/tests/unit/convert_ut.cpp @@ -33,7 +33,7 @@ TEST(OdbcConvert, Int64ToYdb) { }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -47,7 +47,7 @@ TEST(OdbcConvert, Uint64ToYdb) { 1, SQL_PARAM_INPUT, SQL_C_UBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), nullptr }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -61,7 +61,7 @@ TEST(OdbcConvert, DoubleToYdb) { 1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &v, sizeof(v), nullptr }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -76,7 +76,7 @@ TEST(OdbcConvert, StringToYdbUtf8) { 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, len, nullptr }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -91,7 +91,7 @@ TEST(OdbcConvert, StringToYdbBinary) { 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 0, 0, (SQLPOINTER)str, len, nullptr }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -106,7 +106,7 @@ TEST(OdbcConvert, Int64NullToYdb) { 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), &nullInd }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); @@ -121,7 +121,7 @@ TEST(OdbcConvert, StringNullToYdb) { 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, 4, &nullInd }; TParamsBuilder paramsBuilder; - ConvertValue(param, paramsBuilder.AddParam("$p1")); + ConvertParam(param, paramsBuilder.AddParam("$p1")); auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); From d15d3b971fc1954222132a7c8118c864382026e7 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 2 Jun 2025 23:27:43 +0000 Subject: [PATCH 10/55] step --- odbc/CMakeLists.txt | 2 +- odbc/examples/basic/CMakeLists.txt | 10 +- odbc/examples/basic/main.cpp | 14 +-- odbc/examples/scheme/CMakeLists.txt | 10 +- odbc/examples/scheme/main.cpp | 31 +----- odbc/src/odbc_driver.cpp | 83 ++++++++++++++- odbc/src/statement.cpp | 122 ++++++++++++++++------ odbc/src/statement.h | 28 ++++-- odbc/src/utils/bindings.h | 37 +++++++ odbc/src/utils/convert.cpp | 40 ++++++-- odbc/src/utils/convert.h | 22 +--- odbc/src/utils/cursor.cpp | 119 ++++++++++++++++++++++ odbc/src/utils/{result.h => cursor.h} | 15 ++- odbc/src/utils/result.cpp | 140 -------------------------- odbc/src/utils/types.cpp | 12 ++- odbc/src/utils/types.h | 4 +- 16 files changed, 417 insertions(+), 272 deletions(-) create mode 100644 odbc/src/utils/bindings.h create mode 100644 odbc/src/utils/cursor.cpp rename odbc/src/utils/{result.h => cursor.h} (50%) delete mode 100644 odbc/src/utils/result.cpp diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index eb47edc922a..f814f003138 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(ydb-odbc SHARED - src/utils/result.cpp + src/utils/cursor.cpp src/utils/types.cpp src/utils/util.cpp src/utils/convert.cpp diff --git a/odbc/examples/basic/CMakeLists.txt b/odbc/examples/basic/CMakeLists.txt index a34cbd9301b..b99d1175f43 100644 --- a/odbc/examples/basic/CMakeLists.txt +++ b/odbc/examples/basic/CMakeLists.txt @@ -1,14 +1,14 @@ add_executable(odbc_basic - main.cpp + main.cpp ) target_link_libraries(odbc_basic - PRIVATE - ODBC::ODBC + PRIVATE + ODBC::ODBC ) target_compile_definitions(odbc_basic - PRIVATE - ODBC_DRIVER_PATH="$" + PRIVATE + ODBC_DRIVER_PATH="$" ) add_dependencies(odbc_basic ydb-odbc) diff --git a/odbc/examples/basic/main.cpp b/odbc/examples/basic/main.cpp index 364b9e11123..8084e32f3d1 100644 --- a/odbc/examples/basic/main.cpp +++ b/odbc/examples/basic/main.cpp @@ -63,18 +63,10 @@ int main() { std::cout << "6. Executing query" << std::endl; SQLCHAR query[] = R"( DECLARE $p1 AS Int64?; - SELECT $p1 + 1, 'test1'; - SELECT $p1 + 2, 'test2'; - SELECT $p1 + 3, 'test3'; - SELECT $p1 + 4, 'test4'; - SELECT $p1 + 5, 'test5'; - SELECT $p1 + 6, 'test6'; - SELECT $p1 + 7, 'test7'; - SELECT $p1 + 8, 'test8'; - SELECT $p1 + 9, 'test9'; + SELECT id, data from test_table WHERE id == $p1; )"; - int64_t paramValue = 42; + int64_t paramValue = 1; SQLLEN paramInd = 0; ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, ¶mValue, 0, ¶mInd); if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { @@ -129,6 +121,8 @@ int main() { } std::cout << "8. Cleaning up" << std::endl; + + SQLCloseCursor(hstmt); SQLFreeHandle(SQL_HANDLE_STMT, hstmt); SQLDisconnect(hdbc); SQLFreeHandle(SQL_HANDLE_DBC, hdbc); diff --git a/odbc/examples/scheme/CMakeLists.txt b/odbc/examples/scheme/CMakeLists.txt index abca75eb735..ffab881aed5 100644 --- a/odbc/examples/scheme/CMakeLists.txt +++ b/odbc/examples/scheme/CMakeLists.txt @@ -1,14 +1,14 @@ add_executable(odbc_scheme - main.cpp + main.cpp ) target_link_libraries(odbc_scheme - PRIVATE - ODBC::ODBC + PRIVATE + ODBC::ODBC ) target_compile_definitions(odbc_scheme - PRIVATE - ODBC_DRIVER_PATH="$" + PRIVATE + ODBC_DRIVER_PATH="$" ) add_dependencies(odbc_scheme ydb-odbc) diff --git a/odbc/examples/scheme/main.cpp b/odbc/examples/scheme/main.cpp index 3228ba2470b..3ae2cd6fe40 100644 --- a/odbc/examples/scheme/main.cpp +++ b/odbc/examples/scheme/main.cpp @@ -60,33 +60,6 @@ int main() { return 1; } - // std::cout << "6. Executing query" << std::endl; - // SQLCHAR query[] = R"( - // DECLARE $p1 AS Int64?; - // SELECT $p1 + 1, 'test1'; - // SELECT $p1 + 2, 'test2'; - // SELECT $p1 + 3, 'test3'; - // SELECT $p1 + 4, 'test4'; - // SELECT $p1 + 5, 'test5'; - // SELECT $p1 + 6, 'test6'; - // SELECT $p1 + 7, 'test7'; - // SELECT $p1 + 8, 'test8'; - // SELECT $p1 + 9, 'test9'; - // )"; - - // int64_t paramValue = 42; - // SQLLEN paramInd = 0; - // ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, ¶mValue, 0, ¶mInd); - // if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { - // std::cerr << "Error binding parameter" << std::endl; - // PrintOdbcError(SQL_HANDLE_STMT, hstmt); - // SQLFreeHandle(SQL_HANDLE_STMT, hstmt); - // SQLDisconnect(hdbc); - // SQLFreeHandle(SQL_HANDLE_DBC, hdbc); - // SQLFreeHandle(SQL_HANDLE_ENV, henv); - // return 1; - // } - std::cout << "6. Getting tables" << std::endl; SQLCHAR pattern[] = "/local"; @@ -127,8 +100,8 @@ int main() { return 1; } - std::cout << "Result column 1: " << value1 << std::endl; - std::cout << "Result column 2: " << value2 << std::endl; + std::cout << "Table name: " << value1 << std::endl; + std::cout << "Table type: " << value2 << std::endl; std::cout << "--------------------------------" << std::endl; } diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 9e7a7d3aee7..3dec17d6aff 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -149,7 +149,29 @@ SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, return SQL_INVALID_HANDLE; } - return stmt->ExecDirect(NYdb::NOdbc::GetString(statementText, textLength)); + 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) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); +} + +SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->Execute(); } SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { @@ -333,4 +355,63 @@ SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, NYdb::NOdbc::GetString(tableType, nameLength4)); } +SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + + return stmt->Close(false); +} + +SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT statementHandle, SQLUSMALLINT option) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + switch (option) { + case SQL_CLOSE: + return stmt->Close(true); + case SQL_DROP: + return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); + case SQL_UNBIND: + stmt->UnbindColumns(); + return SQL_SUCCESS; + case SQL_RESET_PARAMS: + stmt->ResetParams(); + return SQL_SUCCESS; + default: + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT statementHandle, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + if (fetchOrientation == SQL_FETCH_NEXT) { + return stmt->Fetch(); + } else { + stmt->AddError("HYC00", 0, "Only SQL_FETCH_NEXT is supported"); + return SQL_ERROR; + } +} + +SQLRETURN SQL_API SQLRowCount(SQLHSTMT statementHandle, SQLLEN* rowCount) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->RowCount(rowCount); +} + +SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, SQLSMALLINT* colCount) { + auto stmt = static_cast(statementHandle); + if (!stmt) { + return SQL_INVALID_HANDLE; + } + return stmt->NumResultCols(colCount); +} + } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 856eb5b3417..2bf4c78fd13 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -1,5 +1,6 @@ #include "statement.h" +#include "utils/convert.h" #include "utils/types.h" #include @@ -11,19 +12,27 @@ namespace NOdbc { TStatement::TStatement(TConnection* conn) : Conn_(conn) {} -SQLRETURN TStatement::ExecDirect(const std::string& statementText) { - ClearStatement(); +SQLRETURN TStatement::Prepare(const std::string& statementText) { + Cursor_.reset(); + PreparedQuery_ = statementText; + IsPrepared_ = true; + return SQL_SUCCESS; +} +SQLRETURN TStatement::Execute() { + if (!IsPrepared_ || PreparedQuery_.empty()) { + AddError("HY007", 0, "No prepared statement"); + return SQL_ERROR; + } + Cursor_.reset(); auto* client = Conn_->GetClient(); if (!client) { return SQL_ERROR; } - NYdb::TParams params = BuildParams(); if (!Errors_.empty()) { return SQL_ERROR; } - if (!Conn_->GetTx()) { auto sessionResult = client->GetSession().ExtractValueSync(); if (!sessionResult.IsSuccess()) { @@ -36,34 +45,41 @@ SQLRETURN TStatement::ExecDirect(const std::string& statementText) { } Conn_->SetTx(beginTxResult.GetTransaction()); } - auto session = Conn_->GetTx()->GetSession(); - auto iterator = session.StreamExecuteQuery(statementText, + auto iterator = session.StreamExecuteQuery(PreparedQuery_, NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(Conn_->GetAutocommit()), params).ExtractValueSync(); - if (!iterator.IsSuccess()) { return SQL_ERROR; } - - ResultSet_ = CreateExecResultSet(std::move(iterator)); - + Cursor_ = CreateExecCursor(this, std::move(iterator)); + IsPrepared_ = false; + PreparedQuery_.clear(); return SQL_SUCCESS; } SQLRETURN TStatement::Fetch() { - if (!ResultSet_) { - ClearStatement(); + if (!Cursor_) { + Cursor_.reset(); return SQL_NO_DATA; } - return ResultSet_->Fetch() ? SQL_SUCCESS : SQL_NO_DATA; + return Cursor_->Fetch() ? SQL_SUCCESS : SQL_NO_DATA; } SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - if (!ResultSet_) { + if (!Cursor_) { return SQL_NO_DATA; } - return ResultSet_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + return Cursor_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); +} + +void TStatement::FillBoundColumns() { + if (!Cursor_) { + return; + } + for (const auto& col : BoundColumns_) { + Cursor_->GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + } } SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, @@ -92,10 +108,18 @@ SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLIN } SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - if (!ResultSet_) { + if (!Cursor_) { return SQL_NO_DATA; } - return ResultSet_->BindCol(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + + BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), + [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); + + if (!targetValue) { + return SQL_SUCCESS; + } + BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); + return SQL_SUCCESS; } SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, @@ -127,14 +151,6 @@ void TStatement::AddError(const std::string& sqlState, SQLINTEGER nativeError, c Errors_.push_back({sqlState, nativeError, message}); } -void TStatement::ClearErrors() { - Errors_.clear(); -} - -void TStatement::ClearStatement() { - ResultSet_.reset(); -} - NYdb::TParams TStatement::BuildParams() { Errors_.clear(); NYdb::TParamsBuilder paramsBuilder; @@ -150,8 +166,8 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& schemaName, const std::string& tableName, const std::string& columnName) { - ClearErrors(); - ClearStatement(); + Errors_.clear(); + Cursor_.reset(); std::vector columns = { {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, @@ -236,7 +252,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, } } - ResultSet_ = CreateVirtualResultSet(columns, table); + Cursor_ = CreateVirtualCursor(this, columns, table); return SQL_SUCCESS; } @@ -244,8 +260,8 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, const std::string& schemaName, const std::string& tableName, const std::string& tableType) { - ClearErrors(); - ClearStatement(); + Errors_.clear(); + Cursor_.reset(); std::vector columns = { {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, @@ -270,8 +286,6 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, continue; } - std::cout << "Table name: " << entry.Name << " type: " << *tableType << std::endl; - table.push_back({ TValueBuilder().OptionalUtf8(std::nullopt).Build(), TValueBuilder().OptionalUtf8(std::nullopt).Build(), @@ -281,7 +295,7 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, }); } - ResultSet_ = CreateVirtualResultSet(columns, table); + Cursor_ = CreateVirtualCursor(this, columns, table); return SQL_SUCCESS; } @@ -353,5 +367,47 @@ std::optional TStatement::GetTableType(NScheme::ESchemeEntryType ty } } +SQLRETURN TStatement::Close(bool force) { + if (!force && !Cursor_) { + AddError("24000", 0, "Invalid handle"); + return SQL_ERROR; + } + + Cursor_.reset(); + PreparedQuery_.clear(); + IsPrepared_ = false; + Errors_.clear(); + return SQL_SUCCESS; +} + +void TStatement::UnbindColumns() { + BoundColumns_.clear(); +} + +void TStatement::ResetParams() { + BoundParams_.clear(); +} + +SQLRETURN TStatement::RowCount(SQLLEN* rowCount) { + if (!rowCount) { + return SQL_ERROR; + } + + *rowCount = -1; + return SQL_SUCCESS; +} + +SQLRETURN TStatement::NumResultCols(SQLSMALLINT* colCount) { + if (!colCount) { + return SQL_ERROR; + } + if (!Cursor_) { + *colCount = 0; + return SQL_SUCCESS; + } + *colCount = static_cast(Cursor_->GetColumnMeta().size()); + return SQL_SUCCESS; +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/statement.h b/odbc/src/statement.h index b4568b97fb8..d6b47a11462 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -1,8 +1,9 @@ #pragma once #include "connection.h" -#include "utils/result.h" -#include "utils/convert.h" + +#include "utils/bindings.h" +#include "utils/cursor.h" #include @@ -17,15 +18,23 @@ namespace NYdb { namespace NOdbc { -class TStatement { +class TStatement : public IBindingFiller { public: TStatement(TConnection* conn); - SQLRETURN ExecDirect(const std::string& statementText); + SQLRETURN Prepare(const std::string& statementText); + SQLRETURN Execute(); + SQLRETURN Fetch(); SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); + void FillBoundColumns() override; + + SQLRETURN Close(bool force = false); + void UnbindColumns(); + void ResetParams(); + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); @@ -42,17 +51,17 @@ class TStatement { const std::string& tableName, const std::string& tableType); + SQLRETURN RowCount(SQLLEN* rowCount); + SQLRETURN NumResultCols(SQLSMALLINT* colCount); + TConnection* GetConnection() { return Conn_; } void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); - void ClearErrors(); NYdb::TParams BuildParams(); - void ClearStatement(); - private: std::vector GetPatternEntries(const std::string& pattern); SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); @@ -63,10 +72,13 @@ class TStatement { TConnection* Conn_; TErrorList Errors_; - std::unique_ptr ResultSet_; + std::unique_ptr Cursor_; std::vector BoundColumns_; std::vector BoundParams_; + + std::string PreparedQuery_; + bool IsPrepared_ = false; }; } // namespace NOdbc diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h new file mode 100644 index 00000000000..df76de4e951 --- /dev/null +++ b/odbc/src/utils/bindings.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace NYdb { +namespace NOdbc { + +struct TBoundParam { + SQLUSMALLINT ParamNumber; + SQLSMALLINT InputOutputType; + SQLSMALLINT ValueType; + SQLSMALLINT ParameterType; + SQLULEN ColumnSize; + SQLSMALLINT DecimalDigits; + SQLPOINTER ParameterValuePtr; + SQLLEN BufferLength; + SQLLEN* StrLenOrIndPtr; +}; + +struct TBoundColumn { + SQLUSMALLINT ColumnNumber; + SQLSMALLINT TargetType; + SQLPOINTER TargetValue; + SQLLEN BufferLength; + SQLLEN* StrLenOrInd; +}; + +class IBindingFiller { +public: + virtual void FillBoundColumns() = 0; + + virtual ~IBindingFiller() = default; +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index b10f839dc11..87548622876 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -291,7 +291,9 @@ SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder) { SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { if (parser.IsNull()) { - if (strLenOrInd) *strLenOrInd = SQL_NULL_DATA; + if (strLenOrInd) { + *strLenOrInd = SQL_NULL_DATA; + } return SQL_SUCCESS; } @@ -320,8 +322,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case EPrimitiveType::Bool: v = parser.GetBool() ? 1 : 0; break; default: return SQL_ERROR; } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(int32_t); + if (targetValue) { + *reinterpret_cast(targetValue) = v; + } + if (strLenOrInd) { + *strLenOrInd = sizeof(int32_t); + } return SQL_SUCCESS; } case SQL_C_SBIGINT: @@ -334,8 +340,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; default: return SQL_ERROR; } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(SQLBIGINT); + if (targetValue) { + *reinterpret_cast(targetValue) = v; + } + if (strLenOrInd) { + *strLenOrInd = sizeof(SQLBIGINT); + } return SQL_SUCCESS; } case SQL_C_DOUBLE: @@ -346,8 +356,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case EPrimitiveType::Float: v = parser.GetFloat(); break; default: return SQL_ERROR; } - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(double); + if (targetValue) { + *reinterpret_cast(targetValue) = v; + } + if (strLenOrInd) { + *strLenOrInd = sizeof(double); + } return SQL_SUCCESS; } case SQL_C_CHAR: @@ -366,14 +380,20 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER memcpy(targetValue, str.data(), copyLen); reinterpret_cast(targetValue)[copyLen] = 0; } - if (strLenOrInd) *strLenOrInd = len; + if (strLenOrInd) { + *strLenOrInd = len; + } return SQL_SUCCESS; } case SQL_C_BIT: { char v = parser.GetBool() ? 1 : 0; - if (targetValue) *reinterpret_cast(targetValue) = v; - if (strLenOrInd) *strLenOrInd = sizeof(char); + if (targetValue) { + *reinterpret_cast(targetValue) = v; + } + if (strLenOrInd) { + *strLenOrInd = sizeof(char); + } return SQL_SUCCESS; } default: diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h index dba81c2b345..9b8140665e8 100644 --- a/odbc/src/utils/convert.h +++ b/odbc/src/utils/convert.h @@ -1,5 +1,7 @@ #pragma once +#include "bindings.h" + #include #include @@ -8,26 +10,6 @@ namespace NYdb { namespace NOdbc { -struct TBoundParam { - SQLUSMALLINT ParamNumber; - SQLSMALLINT InputOutputType; - SQLSMALLINT ValueType; - SQLSMALLINT ParameterType; - SQLULEN ColumnSize; - SQLSMALLINT DecimalDigits; - SQLPOINTER ParameterValuePtr; - SQLLEN BufferLength; - SQLLEN* StrLenOrIndPtr; -}; - -struct TBoundColumn { - SQLUSMALLINT ColumnNumber; - SQLSMALLINT TargetType; - SQLPOINTER TargetValue; - SQLLEN BufferLength; - SQLLEN* StrLenOrInd; -}; - SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder); SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp new file mode 100644 index 00000000000..fbd10588aba --- /dev/null +++ b/odbc/src/utils/cursor.cpp @@ -0,0 +1,119 @@ +#include "cursor.h" + +#include "convert.h" +#include "types.h" + +namespace NYdb { +namespace NOdbc { + +class TExecCursor : public ICursor { +public: + TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator) + : BindingFiller_(bindingFiller) + , Iterator_(std::move(iterator)) + {} + + bool Fetch() override { + while (true) { + if (ResultSetParser_) { + if (ResultSetParser_->TryNextRow()) { + BindingFiller_->FillBoundColumns(); + return true; + } + ResultSetParser_.reset(); + } + auto part = Iterator_.ReadNext().ExtractValueSync(); + if (part.EOS()) { + return false; + } + if (!part.IsSuccess()) { + return false; + } + if (part.HasResultSet()) { + ResultSetParser_ = std::make_unique(part.ExtractResultSet()); + } + } + return false; + } + + SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + if (!ResultSetParser_) { + return SQL_NO_DATA; + } + if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { + return SQL_ERROR; + } + return ConvertColumn(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); + } + + const std::vector& GetColumnMeta() const override { + return Columns_; + } + +private: + // void GetNextPart() { + // auto part = Iterator_.ReadNext().ExtractValueSync(); + // while (!part.EOS() && part.IsSuccess() && !part.HasResultSet()) { + // part = Iterator_.ReadNext().ExtractValueSync(); + // } + // Part_ = std::move(part); + // } + + IBindingFiller* BindingFiller_; + NQuery::TExecuteQueryIterator Iterator_; + // std::optional Part_; + std::unique_ptr ResultSetParser_; + std::vector Columns_; +}; + +class TVirtualCursor : public ICursor { +public: + TVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) + : BindingFiller_(bindingFiller) + , Columns_(columns) + , Table_(table) + {} + + bool Fetch() override { + Cursor_++; + if (Cursor_ >= static_cast(Table_.size())) { + return false; + } + BindingFiller_->FillBoundColumns(); + return true; + } + + SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + if (Cursor_ >= static_cast(Table_.size())) { + return SQL_NO_DATA; + } + if (Cursor_ < 0 || columnNumber < 1 || columnNumber > Columns_.size()) { + return SQL_ERROR; + } + TValueParser parser{Table_[Cursor_][columnNumber - 1]}; + return ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); + } + + const std::vector& GetColumnMeta() const override { + return Columns_; + } + +private: + IBindingFiller* BindingFiller_; + std::vector Columns_; + TTable Table_; + int64_t Cursor_ = -1; +}; + +std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator) { + return std::make_unique(bindingFiller, std::move(iterator)); +} + +std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) { + return std::make_unique(bindingFiller, columns, table); +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/result.h b/odbc/src/utils/cursor.h similarity index 50% rename from odbc/src/utils/result.h rename to odbc/src/utils/cursor.h index e5334038a25..e4b13ed5215 100644 --- a/odbc/src/utils/result.h +++ b/odbc/src/utils/cursor.h @@ -1,5 +1,7 @@ #pragma once +#include "bindings.h" + #include #include @@ -19,20 +21,17 @@ struct TColumnMeta { using TTable = std::vector>; -class IResultSet { +class ICursor { public: - virtual ~IResultSet() = default; + virtual ~ICursor() = default; virtual bool Fetch() = 0; virtual SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; - virtual SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; - virtual size_t ColumnsCount() const = 0; - virtual const TColumnMeta& GetColumnMeta(size_t index) const = 0; + virtual const std::vector& GetColumnMeta() const = 0; }; -std::unique_ptr CreateExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator); -std::unique_ptr CreateVirtualResultSet(const std::vector& columns, const TTable& table); +std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, NYdb::NQuery::TExecuteQueryIterator iterator); +std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table); } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/utils/result.cpp b/odbc/src/utils/result.cpp deleted file mode 100644 index ca80f5b0b20..00000000000 --- a/odbc/src/utils/result.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "result.h" - -#include "convert.h" - -namespace NYdb { -namespace NOdbc { - -class TCommonResultSet : public IResultSet { -public: - SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { - BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), - [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); - if (!targetValue) { - return SQL_SUCCESS; - } - BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); - return SQL_SUCCESS; - } - -protected: - void FillBoundColumns() { - for (const auto& col : BoundColumns_) { - GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); - } - } - -protected: - std::vector BoundColumns_; -}; - -class TExecResultSet : public TCommonResultSet { -public: - TExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator) - : Iterator_(std::move(iterator)) {} - - bool Fetch() override { - while (true) { - if (ResultSetParser_) { - if (ResultSetParser_->TryNextRow()) { - FillBoundColumns(); - return true; - } - ResultSetParser_.reset(); - } - auto part = Iterator_.ReadNext().ExtractValueSync(); - if (part.EOS()) { - return false; - } - if (!part.IsSuccess()) { - return false; - } - if (part.HasResultSet()) { - ResultSetParser_ = std::make_unique(part.ExtractResultSet()); - } - } - return false; - } - - SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { - if (!ResultSetParser_) { - return SQL_NO_DATA; - } - if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { - return SQL_ERROR; - } - return ConvertColumn(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); - } - - size_t ColumnsCount() const override { - return ResultSetParser_ ? ResultSetParser_->ColumnsCount() : 0; - } - - const TColumnMeta& GetColumnMeta(size_t index) const override { - // TODO: implement return column metadata - static TColumnMeta dummy; - return dummy; - } - -private: - NYdb::NQuery::TExecuteQueryIterator Iterator_; - std::unique_ptr ResultSetParser_; -}; - -class TVirtualResultSet : public TCommonResultSet { -public: - TVirtualResultSet(const std::vector& columns, const TTable& table) - : Columns_(columns), Table_(table) { - std::cout << "TVirtualResultSet constructor" << std::endl; - std::cout << "Columns count: " << Columns_.size() << std::endl; - std::cout << "Table size: " << Table_.size() << std::endl; - } - - bool Fetch() override { - std::cout << "Fetching row " << Cursor_ << std::endl; - Cursor_++; - if (Cursor_ >= static_cast(Table_.size())) { - return false; - } - FillBoundColumns(); - return true; - } - - SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { - if (Cursor_ >= static_cast(Table_.size())) { - return SQL_NO_DATA; - } - if (Cursor_ < 0 || columnNumber < 1 || columnNumber > Columns_.size()) { - return SQL_ERROR; - } - TValueParser parser{Table_[Cursor_][columnNumber - 1]}; - return ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); - } - - size_t ColumnsCount() const override { - return Columns_.size(); - } - - const TColumnMeta& GetColumnMeta(size_t index) const override { - return Columns_[index]; - } - -private: - std::vector Columns_; - TTable Table_; - int64_t Cursor_ = -1; -}; - -std::unique_ptr CreateExecResultSet(NYdb::NQuery::TExecuteQueryIterator iterator) { - return std::make_unique(std::move(iterator)); -} - -std::unique_ptr CreateVirtualResultSet(const std::vector& columns, const TTable& table) { - return std::make_unique(columns, table); -} - -} // namespace NOdbc -} // namespace NYdb diff --git a/odbc/src/utils/types.cpp b/odbc/src/utils/types.cpp index aa038420e53..ce5ead462cc 100644 --- a/odbc/src/utils/types.cpp +++ b/odbc/src/utils/types.cpp @@ -3,10 +3,20 @@ namespace NYdb { namespace NOdbc { -SQLINTEGER GetTypeId(const TType& type) { +SQLSMALLINT GetTypeId(const TType& type) { + // TODO: implement return 0; } +SQLSMALLINT IsNullable(const TType& type) { + TTypeParser typeParser(type); + if (typeParser.GetKind() == TTypeParser::ETypeKind::Optional || typeParser.GetKind() == TTypeParser::ETypeKind::Null) { + return SQL_NULLABLE; + } + + return SQL_NO_NULLS; +} + std::optional GetDecimalDigits(const TType& type) { TTypeParser typeParser(type); if (typeParser.GetKind() != TTypeParser::ETypeKind::Primitive) { diff --git a/odbc/src/utils/types.h b/odbc/src/utils/types.h index 0b9dd76aa13..3f481702902 100644 --- a/odbc/src/utils/types.h +++ b/odbc/src/utils/types.h @@ -7,7 +7,9 @@ namespace NYdb { namespace NOdbc { -SQLINTEGER GetTypeId(const TType& type); +SQLSMALLINT GetTypeId(const TType& type); +SQLSMALLINT IsNullable(const TType& type); + std::optional GetDecimalDigits(const TType& type); std::optional GetRadix(const TType& type); From 5f3537d87c563feaa018dbc04ad24262e0e1f172 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Tue, 3 Jun 2025 00:17:18 +0000 Subject: [PATCH 11/55] step --- cmake/testing.cmake | 4 +- odbc/README.md | 81 ++++++----------- odbc/src/connection.cpp | 4 +- odbc/src/utils/convert.cpp | 7 +- odbc/tests/CMakeLists.txt | 2 +- odbc/tests/integration/CMakeLists.txt | 4 + odbc/tests/integration/basic_it.cpp | 123 ++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 58 deletions(-) create mode 100644 odbc/tests/integration/CMakeLists.txt create mode 100644 odbc/tests/integration/basic_it.cpp diff --git a/cmake/testing.cmake b/cmake/testing.cmake index e45eb0a787f..8a69fd184d8 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -141,7 +141,9 @@ if (YDB_SDK_ODBC) LINK_LIBRARIES ${ODBC_TEST_LINK_LIBRARIES} ODBC::ODBC - LABELS ${ODBC_TEST_LABELS} + LABELS + integration + ${ODBC_TEST_LABELS} ) target_compile_definitions(${ODBC_TEST_NAME} diff --git a/odbc/README.md b/odbc/README.md index 4d502aaad73..c73f9b8704a 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -1,105 +1,80 @@ # YDB ODBC Driver -ODBC драйвер для YDB. +ODBC driver for YDB. -## Требования +## Requirements -- CMake 3.10 или выше -- Компилятор C/C++ с поддержкой C11 и C++20 +- CMake 3.10 or higher +- C/C++ compiler with C11 and C++20 support - YDB C++ SDK -- unixODBC (для Linux/macOS) +- unixODBC (for Linux/macOS) -## Сборка +## Build ```bash -mkdir build && cd build -cmake .. -make +cmake -DYDB_SDK_ODBC=1 --preset release-clang +cmake --build --preset default ``` -## Установка +## Configuration -```bash -sudo make install -``` - -Это установит: -- Библиотеку драйвера в `/usr/local/lib/` -- Конфигурацию драйвера в `/etc/odbcinst.d/` -- Конфигурацию источников данных в `/etc/odbc.ini` - -## Настройка - -1. Убедитесь, что драйвер зарегистрирован: +1. Make sure the driver is registered: ```bash odbcinst -q -d ``` -2. Проверьте доступные источники данных: +2. Check available data sources: ```bash odbcinst -q -s ``` -3. Отредактируйте `/etc/odbc.ini` для настройки подключения: +3. Edit `/etc/odbc.ini` to configure the connection: ```ini [YDB] Driver=YDB Description=YDB Database Connection -Server=grpc://your-server:2136 -Database=your-database -AuthMode=none # или token для аутентификации по токену +Server=your-server:port +Database=/path/to/database ``` -## Использование +## Usage -Пример подключения через isql: +Example of connecting via isql: ```bash isql -v YDB ``` -Пример использования в C: +Example usage in C: ```c SQLHENV env; SQLHDBC dbc; SQLHSTMT stmt; -// Инициализация окружения +// Initialize environment SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); -// Подключение +// Connect SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); SQLConnect(dbc, (SQLCHAR*)"YDB", SQL_NTS, (SQLCHAR*)"", SQL_NTS, (SQLCHAR*)"", SQL_NTS); -// Выполнение запроса +// Execute query SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM mytable", SQL_NTS); -// Очистка +// Cleanup SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); ``` -## Поддерживаемые функции - -- SQLAllocHandle -- SQLConnect -- SQLDisconnect -- SQLExecDirect -- SQLFetch -- SQLGetData -- SQLPrepare -- SQLExecute -- SQLCloseCursor -- SQLFreeHandle -- SQLGetInfo -- SQLGetDescField -- SQLSetDescField - -## Лицензия - -Apache License 2.0 \ No newline at end of file +## Parameters + +Use names $p1, $p2, ... for parameter names + +## License + +Apache License 2.0 diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 7806096bc63..eba32c74ef6 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -57,8 +57,8 @@ SQLRETURN TConnection::Connect(const std::string& serverName, char endpoint[256] = {0}; char database[256] = {0}; - SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); - SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); + //SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); + //SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); Endpoint_ = endpoint; Database_ = database; diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 87548622876..224f228e498 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -312,10 +312,15 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER switch (targetType) { case SQL_C_SLONG: + case SQL_C_LONG: { int32_t v = 0; switch (ydbType) { - case EPrimitiveType::Int32: v = parser.GetInt32(); break; + case EPrimitiveType::Int16: v = static_cast(parser.GetInt16()); break; + case EPrimitiveType::Uint16: v = static_cast(parser.GetUint16()); break; + case EPrimitiveType::Int8: v = static_cast(parser.GetInt8()); break; + case EPrimitiveType::Uint8: v = static_cast(parser.GetUint8()); break; + case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; case EPrimitiveType::Int64: v = static_cast(parser.GetInt64()); break; case EPrimitiveType::Uint64: v = static_cast(parser.GetUint64()); break; diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 446b6139f92..729c6ee0778 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -1,2 +1,2 @@ -#add_subdirectory(integration) +add_subdirectory(integration) add_subdirectory(unit) diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt new file mode 100644 index 00000000000..e1aad9d3913 --- /dev/null +++ b/odbc/tests/integration/CMakeLists.txt @@ -0,0 +1,4 @@ +add_odbc_test(NAME odbc-basic_it + SOURCES + basic_it.cpp +) diff --git a/odbc/tests/integration/basic_it.cpp b/odbc/tests/integration/basic_it.cpp new file mode 100644 index 00000000000..b4c7078ac4e --- /dev/null +++ b/odbc/tests/integration/basic_it.cpp @@ -0,0 +1,123 @@ +#include + +#include +#include + +#include + + +#define CHECK_ODBC_OK(rc, handle, type) \ + ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) + +std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { + SQLCHAR sqlState[6], message[256]; + SQLINTEGER nativeError; + SQLSMALLINT textLength; + SQLRETURN rc = SQLGetDiagRec(type, handle, 1, sqlState, &nativeError, message, sizeof(message), &textLength); + if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { + return std::string((char*)sqlState) + ": " + (char*)message; + } + return "Unknown ODBC error"; +} + +const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; + +TEST(OdbcBasic, SimpleQuery) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + CHECK_ODBC_OK(SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + // Simple query + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS one, 'abc' AS str", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLINTEGER ival = 0; + char sval[16] = {0}; + SQLLEN ival_ind = 0, sval_ind = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &ival, 0, &ival_ind), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, sval, sizeof(sval), &sval_ind), SQL_SUCCESS); + ASSERT_EQ(ival, 1); + ASSERT_STREQ(sval, "abc"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcBasic, ParameterizedQuery) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + CHECK_ODBC_OK(SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR query[] = R"( + DECLARE $p1 AS Int32?; + SELECT $p1 + 10 AS res; + )"; + + // Parameterized query + CHECK_ODBC_OK(SQLPrepare(stmt, query, SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER param = 5; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, ¶m, 0, nullptr), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLINTEGER res = 0; + SQLLEN res_ind = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &res, 0, &res_ind), SQL_SUCCESS); + ASSERT_EQ(res, 15); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcBasic, ColumnBinding) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + CHECK_ODBC_OK(SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR query_ddl[] = R"( + DROP TABLE IF EXISTS test_bind; + CREATE TABLE test_bind (id Int32, name Text, PRIMARY KEY (id)); + )"; + + SQLCHAR query[] = R"( + UPSERT INTO test_bind (id, name) VALUES (1, 'foo'), (2, 'bar'); + SELECT id, name FROM test_bind ORDER BY id; + )"; + + CHECK_ODBC_OK(SQLExecDirect(stmt, query_ddl, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, query, SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLINTEGER id = 0; + char name[16] = {0}; + SQLLEN id_ind = 0, name_ind = 0; + ASSERT_EQ(SQLBindCol(stmt, 1, SQL_C_LONG, &id, 0, &id_ind), SQL_SUCCESS); + ASSERT_EQ(SQLBindCol(stmt, 2, SQL_C_CHAR, name, sizeof(name), &name_ind), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(id, 1); + ASSERT_STREQ(name, "foo"); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(id, 2); + ASSERT_STREQ(name, "bar"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} From f4aecf96f7cc851cd647b64416a5be628161583c Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Fri, 11 Jul 2025 17:03:45 +0000 Subject: [PATCH 12/55] Error handlig --- CMakePresets.json | 1 + odbc/CMakeLists.txt | 1 + odbc/src/connection.cpp | 54 +---- odbc/src/connection.h | 9 +- odbc/src/environment.cpp | 37 --- odbc/src/environment.h | 21 +- odbc/src/odbc_driver.cpp | 397 +++++++++++++------------------ odbc/src/statement.cpp | 92 ++----- odbc/src/statement.h | 29 +-- odbc/src/utils/error_manager.cpp | 120 ++++++++++ odbc/src/utils/error_manager.h | 97 ++++++++ 11 files changed, 431 insertions(+), 427 deletions(-) create mode 100644 odbc/src/utils/error_manager.cpp create mode 100644 odbc/src/utils/error_manager.h diff --git a/CMakePresets.json b/CMakePresets.json index 2061ba6b78f..b13fdc05598 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -56,6 +56,7 @@ "cacheVariables": { "YDB_SDK_TESTS": "TRUE", "YDB_SDK_EXAMPLES": "TRUE", + "YDB_SDK_ODBC": "TRUE", "ARCADIA_ROOT": "..", "ARCADIA_BUILD_ROOT": "." } diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index f814f003138..799c9b89b19 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(ydb-odbc SHARED src/utils/types.cpp src/utils/util.cpp src/utils/convert.cpp + src/utils/error_manager.cpp src/odbc_driver.cpp src/connection.cpp src/statement.cpp diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index eba32c74ef6..7ed7679e015 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -1,5 +1,6 @@ #include "connection.h" #include "statement.h" +#include "utils/error_manager.h" #include #include @@ -35,8 +36,7 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { Database_ = params["Database"]; if (Endpoint_.empty() || Database_.empty()) { - AddError("08001", 0, "Missing Endpoint or Database in connection string"); - return SQL_ERROR; + throw TOdbcException("08001", 0, "Missing Endpoint or Database in connection string"); } YdbDriver_ = std::make_unique(NYdb::TDriverConfig() @@ -64,8 +64,7 @@ SQLRETURN TConnection::Connect(const std::string& serverName, Database_ = database; if (Endpoint_.empty() || Database_.empty()) { - AddError("08001", 0, "Missing Endpoint or Database in DSN"); - return SQL_ERROR; + throw TOdbcException("08001", 0, "Missing Endpoint or Database in DSN"); } YdbDriver_ = std::make_unique(NYdb::TDriverConfig() @@ -85,30 +84,6 @@ SQLRETURN TConnection::Disconnect() { return SQL_SUCCESS; } -SQLRETURN TConnection::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, - SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { - return SQL_NO_DATA; - } - - const auto& err = Errors_[recNumber-1]; - if (sqlState) { - strncpy((char*)sqlState, err.SqlState.c_str(), 6); - } - - if (nativeError) { - *nativeError = err.NativeError; - } - - if (messageText && bufferLength > 0) { - strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) { - *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); - } - } - return SQL_SUCCESS; -} - std::unique_ptr TConnection::CreateStatement() { return std::make_unique(this); } @@ -118,22 +93,11 @@ void TConnection::RemoveStatement(TStatement* stmt) { [stmt](const std::unique_ptr& s) { return s.get() == stmt; }), Statements_.end()); } -void TConnection::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { - Errors_.push_back({sqlState, nativeError, message}); -} - -void TConnection::ClearErrors() { - Errors_.clear(); -} - SQLRETURN TConnection::SetAutocommit(bool value) { Autocommit_ = value; if (Autocommit_ && Tx_) { auto status = Tx_->Commit().ExtractValueSync(); - if (!status.IsSuccess()) { - AddError("08001", 0, "Failed to commit transaction"); - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(status); Tx_.reset(); } return SQL_SUCCESS; @@ -153,20 +117,14 @@ void TConnection::SetTx(const NQuery::TTransaction& tx) { SQLRETURN TConnection::CommitTx() { auto status = Tx_->Commit().ExtractValueSync(); - if (!status.IsSuccess()) { - AddError("08001", 0, "Failed to commit transaction"); - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(status); Tx_.reset(); return SQL_SUCCESS; } SQLRETURN TConnection::RollbackTx() { auto status = Tx_->Rollback().ExtractValueSync(); - if (!status.IsSuccess()) { - AddError("08001", 0, "Failed to rollback transaction"); - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(status); Tx_.reset(); return SQL_SUCCESS; } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index fad81527772..ad69b0f171c 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -1,6 +1,7 @@ #pragma once #include "environment.h" +#include "utils/error_manager.h" #include #include @@ -19,7 +20,7 @@ namespace NOdbc { class TStatement; -class TConnection { +class TConnection : public TErrorManager { private: std::unique_ptr YdbDriver_; std::unique_ptr YdbClient_; @@ -27,7 +28,6 @@ class TConnection { std::unique_ptr YdbSchemeClient_; std::optional Tx_; - TErrorList Errors_; std::vector> Statements_; std::string Endpoint_; std::string Database_; @@ -42,8 +42,6 @@ class TConnection { SQLRETURN DriverConnect(const std::string& connectionString); SQLRETURN Disconnect(); - SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, - SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); std::unique_ptr CreateStatement(); void RemoveStatement(TStatement* stmt); @@ -52,9 +50,6 @@ class TConnection { NYdb::NTable::TTableClient* GetTableClient() { return YdbTableClient_.get(); } NScheme::TSchemeClient* GetSchemeClient() { return YdbSchemeClient_.get(); } - void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); - void ClearErrors(); - SQLRETURN SetAutocommit(bool value); bool GetAutocommit() const; diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index a09a634879b..541ca9e2160 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -13,42 +13,5 @@ SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQL return SQL_SUCCESS; } -SQLRETURN TEnvironment::GetDiagRec(SQLSMALLINT recNumber, - SQLCHAR* sqlState, - SQLINTEGER* nativeError, - SQLCHAR* messageText, - SQLSMALLINT bufferLength, - SQLSMALLINT* textLength) { - - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { - return SQL_NO_DATA; - } - - const auto& err = Errors_[recNumber-1]; - if (sqlState) { - strncpy((char*)sqlState, err.SqlState.c_str(), 6); - } - - if (nativeError) { - *nativeError = err.NativeError; - } - - if (messageText && bufferLength > 0) { - strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) { - *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); - } - } - return SQL_SUCCESS; -} - -void TEnvironment::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { - Errors_.push_back({sqlState, nativeError, message}); -} - -void TEnvironment::ClearErrors() { - Errors_.clear(); -} - } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/environment.h b/odbc/src/environment.h index 0190b913831..5258b722492 100644 --- a/odbc/src/environment.h +++ b/odbc/src/environment.h @@ -1,39 +1,24 @@ #pragma once +#include "utils/error_manager.h" + #include #include -#include -#include - namespace NYdb { namespace NOdbc { class TConnection; -struct TErrorInfo { - std::string SqlState; - SQLINTEGER NativeError; - std::string Message; -}; - -using TErrorList = std::vector; - -class TEnvironment { +class TEnvironment : public TErrorManager { private: SQLINTEGER OdbcVersion_; - TErrorList Errors_; public: TEnvironment(); ~TEnvironment(); SQLRETURN SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength); - SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, - SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); - - void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); - void ClearErrors(); }; } // namespace NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 3dec17d6aff..c047f770837 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -3,10 +3,21 @@ #include "statement.h" #include "utils/util.h" +#include "utils/error_manager.h" #include #include +namespace { + template + Handle* GetHandle(SQLHANDLE handle) { + if (!handle) { + throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid handle", SQL_INVALID_HANDLE); + } + return static_cast(handle); + } +} + extern "C" { SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, @@ -15,78 +26,58 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, if (!outputHandle) { return SQL_INVALID_HANDLE; } - - try { - switch (handleType) { - case SQL_HANDLE_ENV: { - if (inputHandle != SQL_NULL_HANDLE) { - return SQL_INVALID_HANDLE; - } + switch (handleType) { + case SQL_HANDLE_ENV: { + return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&]() { *outputHandle = new NYdb::NOdbc::TEnvironment(); return SQL_SUCCESS; - } - - case SQL_HANDLE_DBC: { - if (!inputHandle) { - return SQL_INVALID_HANDLE; - } + }); + } + case SQL_HANDLE_DBC: { + return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&]() { *outputHandle = new NYdb::NOdbc::TConnection(); return SQL_SUCCESS; - } - - case SQL_HANDLE_STMT: { - auto conn = static_cast(inputHandle); - if (!conn) { - return SQL_INVALID_HANDLE; - } + }); + } + case SQL_HANDLE_STMT: { + return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&](auto* conn) { auto stmt = conn->CreateStatement(); *outputHandle = stmt.release(); return SQL_SUCCESS; - } - - default: - return SQL_ERROR; + }); } - } catch (...) { - return SQL_ERROR; + default: + return SQL_ERROR; } } SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { - if (!handle) { - return SQL_INVALID_HANDLE; - } - - try { - switch (handleType) { - case SQL_HANDLE_ENV: { - auto env = static_cast(handle); + switch (handleType) { + case SQL_HANDLE_ENV: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* env) { delete env; return SQL_SUCCESS; - } - - case SQL_HANDLE_DBC: { - auto conn = static_cast(handle); + }); + } + case SQL_HANDLE_DBC: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* conn) { delete conn; return SQL_SUCCESS; - } - - case SQL_HANDLE_STMT: { - auto stmt = static_cast(handle); + }); + } + case SQL_HANDLE_STMT: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* stmt) { if (stmt->GetConnection()) { stmt->GetConnection()->RemoveStatement(stmt); } delete stmt; return SQL_SUCCESS; - } - - default: - return SQL_ERROR; + }); } - } catch (...) { - return SQL_ERROR; + default: + return SQL_ERROR; } } @@ -99,7 +90,9 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV environmentHandle, return SQL_INVALID_HANDLE; } - return env->SetAttribute(attribute, value, stringLength); + return NYdb::NOdbc::HandleOdbcExceptions(env, [&]() { + return env->SetAttribute(attribute, value, stringLength); + }); } SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, @@ -110,77 +103,58 @@ SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, SQLSMALLINT /*bufferLength*/, SQLSMALLINT* /*stringLength2Ptr*/, SQLUSMALLINT /*driverCompletion*/) { - auto conn = static_cast(connectionHandle); - if (!conn) { - return SQL_INVALID_HANDLE; - } - - return conn->DriverConnect(NYdb::NOdbc::GetString(inConnectionString, stringLength1)); + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + return conn->DriverConnect(NYdb::NOdbc::GetString(inConnectionString, stringLength1)); + }); } SQLRETURN SQL_API SQLConnect(SQLHDBC connectionHandle, SQLCHAR* serverName, SQLSMALLINT nameLength1, SQLCHAR* userName, SQLSMALLINT nameLength2, SQLCHAR* authentication, SQLSMALLINT nameLength3) { - auto conn = static_cast(connectionHandle); - if (!conn) { - return SQL_INVALID_HANDLE; - } - - return conn->Connect(NYdb::NOdbc::GetString(serverName, nameLength1), - NYdb::NOdbc::GetString(userName, nameLength2), - NYdb::NOdbc::GetString(authentication, nameLength3)); + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + return conn->Connect(NYdb::NOdbc::GetString(serverName, nameLength1), + NYdb::NOdbc::GetString(userName, nameLength2), + NYdb::NOdbc::GetString(authentication, nameLength3)); + }); } SQLRETURN SQL_API SQLDisconnect(SQLHDBC connectionHandle) { - auto conn = static_cast(connectionHandle); - if (!conn) { - return SQL_INVALID_HANDLE; - } - - return conn->Disconnect(); + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + return conn->Disconnect(); + }); } SQLRETURN SQL_API SQLExecDirect(SQLHSTMT statementHandle, SQLCHAR* statementText, SQLINTEGER textLength) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - - auto ret = stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); - if (ret != SQL_SUCCESS) { - return ret; - } - return stmt->Execute(); + 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) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Prepare(NYdb::NOdbc::GetString(statementText, textLength)); + }); } SQLRETURN SQL_API SQLExecute(SQLHSTMT statementHandle) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->Execute(); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Execute(); + }); } SQLRETURN SQL_API SQLFetch(SQLHSTMT statementHandle) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - - return stmt->Fetch(); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Fetch(); + }); } SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, @@ -189,12 +163,9 @@ SQLRETURN SQL_API SQLGetData(SQLHSTMT statementHandle, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - - return stmt->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + }); } SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, @@ -203,11 +174,9 @@ SQLRETURN SQL_API SQLBindCol(SQLHSTMT statementHandle, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->BindCol(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->BindCol(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + }); } SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, @@ -218,32 +187,24 @@ SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - if (!handle) { - return SQL_INVALID_HANDLE; - } - - try { - switch (handleType) { - case SQL_HANDLE_ENV: { - auto env = static_cast(handle); + switch (handleType) { + case SQL_HANDLE_ENV: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* env) { return env->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); - } - - case SQL_HANDLE_DBC: { - auto conn = static_cast(handle); + }); + } + case SQL_HANDLE_DBC: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* conn) { return conn->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); - } - - case SQL_HANDLE_STMT: { - auto stmt = static_cast(handle); + }); + } + case SQL_HANDLE_STMT: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) { return stmt->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); - } - - default: - return SQL_ERROR; + }); } - } catch (...) { - return SQL_ERROR; + default: + return SQL_ERROR; } } @@ -257,32 +218,26 @@ SQLRETURN SQL_API SQLBindParameter(SQLHSTMT statementHandle, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - - return stmt->BindParameter(paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->BindParameter(paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr); + }); } SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLINT completionType) { - if (!handle) { - return SQL_INVALID_HANDLE; - } - try { - switch (handleType) { - case SQL_HANDLE_DBC: { - auto conn = static_cast(handle); + switch (handleType) { + case SQL_HANDLE_DBC: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* conn) { if (completionType == SQL_COMMIT) { return conn->CommitTx(); } else if (completionType == SQL_ROLLBACK) { return conn->RollbackTx(); } else { - return SQL_ERROR; + throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid completion type"); } - } - case SQL_HANDLE_STMT: { - auto stmt = static_cast(handle); + }); + } + case SQL_HANDLE_STMT: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) -> SQLRETURN { auto conn = stmt->GetConnection(); if (!conn) return SQL_INVALID_HANDLE; if (completionType == SQL_COMMIT) { @@ -290,37 +245,33 @@ SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLI } else if (completionType == SQL_ROLLBACK) { return conn->RollbackTx(); } else { - return SQL_ERROR; + throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid completion type"); } - } - case SQL_HANDLE_ENV: { - // TODO: if's list of connections in ENV, go through them and commit/rollback transactions - return SQL_SUCCESS; - } - default: - return SQL_ERROR; + }); + } + case SQL_HANDLE_ENV: { + // TODO: if's list of connections in ENV, go through them and commit/rollback transactions + return SQL_SUCCESS; } - } catch (...) { - return SQL_ERROR; + default: + return SQL_ERROR; } } SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { - auto conn = static_cast(connectionHandle); - if (!conn) { - return SQL_INVALID_HANDLE; - } - if (attribute == SQL_ATTR_AUTOCOMMIT) { - if ((intptr_t)value == SQL_AUTOCOMMIT_ON) { - return conn->SetAutocommit(true); - } else if ((intptr_t)value == SQL_AUTOCOMMIT_OFF) { - return conn->SetAutocommit(false); - } else { - return SQL_ERROR; + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + if (attribute == SQL_ATTR_AUTOCOMMIT) { + if ((intptr_t)value == SQL_AUTOCOMMIT_ON) { + return conn->SetAutocommit(true); + } else if ((intptr_t)value == SQL_AUTOCOMMIT_OFF) { + return conn->SetAutocommit(false); + } else { + throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid autocommit value"); + } } - } - // TODO: other attributes - return SQL_ERROR; + // TODO: other attributes + throw NYdb::NOdbc::TOdbcException("HYC00", 0, "Optional feature not implemented"); + }); } SQLRETURN SQL_API SQLColumns(SQLHSTMT statementHandle, @@ -328,15 +279,13 @@ SQLRETURN SQL_API SQLColumns(SQLHSTMT statementHandle, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* columnName, SQLSMALLINT nameLength4) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->Columns( - NYdb::NOdbc::GetString(catalogName, nameLength1), - NYdb::NOdbc::GetString(schemaName, nameLength2), - NYdb::NOdbc::GetString(tableName, nameLength3), - NYdb::NOdbc::GetString(columnName, nameLength4)); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Columns( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + NYdb::NOdbc::GetString(columnName, nameLength4)); + }); } SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, @@ -344,74 +293,60 @@ SQLRETURN SQL_API SQLTables(SQLHSTMT statementHandle, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* tableType, SQLSMALLINT nameLength4) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->Tables( - NYdb::NOdbc::GetString(catalogName, nameLength1), - NYdb::NOdbc::GetString(schemaName, nameLength2), - NYdb::NOdbc::GetString(tableName, nameLength3), - NYdb::NOdbc::GetString(tableType, nameLength4)); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Tables( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + NYdb::NOdbc::GetString(tableType, nameLength4)); + }); } SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - - return stmt->Close(false); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Close(false); + }); } SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT statementHandle, SQLUSMALLINT option) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - switch (option) { - case SQL_CLOSE: - return stmt->Close(true); - case SQL_DROP: - return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); - case SQL_UNBIND: - stmt->UnbindColumns(); - return SQL_SUCCESS; - case SQL_RESET_PARAMS: - stmt->ResetParams(); - return SQL_SUCCESS; - default: - return SQL_ERROR; - } + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) -> SQLRETURN { + switch (option) { + case SQL_CLOSE: + return stmt->Close(true); + case SQL_DROP: + return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); + case SQL_UNBIND: + stmt->UnbindColumns(); + return SQL_SUCCESS; + case SQL_RESET_PARAMS: + stmt->ResetParams(); + return SQL_SUCCESS; + default: + throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid option"); + } + }); } SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT statementHandle, SQLSMALLINT fetchOrientation, SQLLEN fetchOffset) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - if (fetchOrientation == SQL_FETCH_NEXT) { - return stmt->Fetch(); - } else { - stmt->AddError("HYC00", 0, "Only SQL_FETCH_NEXT is supported"); - return SQL_ERROR; - } + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + if (fetchOrientation == SQL_FETCH_NEXT) { + return stmt->Fetch(); + } else { + throw NYdb::NOdbc::TOdbcException("HYC00", 0, "Only SQL_FETCH_NEXT is supported"); + } + }); } SQLRETURN SQL_API SQLRowCount(SQLHSTMT statementHandle, SQLLEN* rowCount) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->RowCount(rowCount); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->RowCount(rowCount); + }); } SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, SQLSMALLINT* colCount) { - auto stmt = static_cast(statementHandle); - if (!stmt) { - return SQL_INVALID_HANDLE; - } - return stmt->NumResultCols(colCount); + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->NumResultCols(colCount); + }); } } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 2bf4c78fd13..b61b8f07eb2 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -2,6 +2,7 @@ #include "utils/convert.h" #include "utils/types.h" +#include "utils/error_manager.h" #include #include @@ -21,36 +22,30 @@ SQLRETURN TStatement::Prepare(const std::string& statementText) { SQLRETURN TStatement::Execute() { if (!IsPrepared_ || PreparedQuery_.empty()) { - AddError("HY007", 0, "No prepared statement"); - return SQL_ERROR; + throw TOdbcException("HY007", 0, "No prepared statement"); } Cursor_.reset(); auto* client = Conn_->GetClient(); if (!client) { - return SQL_ERROR; + throw TOdbcException("HY000", 0, "No client connection"); } NYdb::TParams params = BuildParams(); - if (!Errors_.empty()) { - return SQL_ERROR; - } + if (!Conn_->GetTx()) { auto sessionResult = client->GetSession().ExtractValueSync(); - if (!sessionResult.IsSuccess()) { - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(sessionResult); + auto session = sessionResult.GetSession(); auto beginTxResult = session.BeginTransaction(NQuery::TTxSettings::SerializableRW()).ExtractValueSync(); - if (!beginTxResult.IsSuccess()) { - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(beginTxResult); + Conn_->SetTx(beginTxResult.GetTransaction()); } auto session = Conn_->GetTx()->GetSession(); auto iterator = session.StreamExecuteQuery(PreparedQuery_, NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(Conn_->GetAutocommit()), params).ExtractValueSync(); - if (!iterator.IsSuccess()) { - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(iterator); + Cursor_ = CreateExecCursor(this, std::move(iterator)); IsPrepared_ = false; PreparedQuery_.clear(); @@ -82,31 +77,6 @@ void TStatement::FillBoundColumns() { } } -SQLRETURN TStatement::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, - SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { - - if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { - return SQL_NO_DATA; - } - - const auto& err = Errors_[recNumber-1]; - if (sqlState) { - strncpy((char*)sqlState, err.SqlState.c_str(), 6); - } - - if (nativeError) { - *nativeError = err.NativeError; - } - - if (messageText && bufferLength > 0) { - strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) { - *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); - } - } - return SQL_SUCCESS; -} - SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { if (!Cursor_) { return SQL_NO_DATA; @@ -133,8 +103,7 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, SQLLEN* strLenOrIndPtr) { if (inputOutputType != SQL_PARAM_INPUT) { - AddError("HYC00", 0, "Only input parameters are supported"); - return SQL_ERROR; + throw TOdbcException("HYC00", 0, "Only input parameters are supported"); } BoundParams_.erase(std::remove_if(BoundParams_.begin(), BoundParams_.end(), @@ -147,12 +116,8 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, return SQL_SUCCESS; } -void TStatement::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message) { - Errors_.push_back({sqlState, nativeError, message}); -} - NYdb::TParams TStatement::BuildParams() { - Errors_.clear(); + ClearErrors(); NYdb::TParamsBuilder paramsBuilder; for (const auto& param : BoundParams_) { std::string paramName = "$p" + std::to_string(param.ParamNumber); @@ -166,7 +131,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& schemaName, const std::string& tableName, const std::string& columnName) { - Errors_.clear(); + ClearErrors(); Cursor_.reset(); std::vector columns = { @@ -192,8 +157,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, auto entries = GetPatternEntries(tableName); if (entries.empty()) { - AddError("HYC00", 0, "No tables found"); - return SQL_ERROR; + throw TOdbcException("HYC00", 0, "No tables found"); } TTable table; @@ -207,9 +171,8 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, auto status = Conn_->GetTableClient()->RetryOperationSync([path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { auto result = session.DescribeTable(path).ExtractValueSync(); - if (!result.IsSuccess()) { - return result; - } + NStatusHelpers::ThrowOnError(result); + auto columns = result.GetTableDescription().GetTableColumns(); auto columnIt = std::find_if(columns.begin(), columns.end(), [&columnName](const NTable::TTableColumn& column) { @@ -247,9 +210,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, return TStatus(EStatus::SUCCESS, {}); }); - if (!status.IsSuccess()) { - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(status); } Cursor_ = CreateVirtualCursor(this, columns, table); @@ -260,7 +221,7 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, const std::string& schemaName, const std::string& tableName, const std::string& tableType) { - Errors_.clear(); + ClearErrors(); Cursor_.reset(); std::vector columns = { @@ -273,8 +234,7 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, auto entries = GetPatternEntries(tableName); if (entries.empty()) { - AddError("HYC00", 0, "No tables found"); - return SQL_ERROR; + throw TOdbcException("HYC00", 0, "No tables found"); } TTable table; @@ -308,9 +268,8 @@ std::vector TStatement::GetPatternEntries(const std::stri SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries) { auto schemeClient = Conn_->GetSchemeClient(); auto listDirectoryResult = schemeClient->ListDirectory(path + "/").ExtractValueSync(); - if (!listDirectoryResult.IsSuccess()) { - return SQL_ERROR; - } + NStatusHelpers::ThrowOnError(listDirectoryResult); + for (const auto& entry : listDirectoryResult.GetChildren()) { std::string fullPath = path + "/" + entry.Name; if (entry.Type == NScheme::ESchemeEntryType::Directory || @@ -369,14 +328,13 @@ std::optional TStatement::GetTableType(NScheme::ESchemeEntryType ty SQLRETURN TStatement::Close(bool force) { if (!force && !Cursor_) { - AddError("24000", 0, "Invalid handle"); - return SQL_ERROR; + throw TOdbcException("24000", 0, "Invalid handle"); } Cursor_.reset(); PreparedQuery_.clear(); IsPrepared_ = false; - Errors_.clear(); + ClearErrors(); return SQL_SUCCESS; } @@ -390,7 +348,7 @@ void TStatement::ResetParams() { SQLRETURN TStatement::RowCount(SQLLEN* rowCount) { if (!rowCount) { - return SQL_ERROR; + throw TOdbcException("HY000", 0, "Invalid parameter"); } *rowCount = -1; @@ -399,7 +357,7 @@ SQLRETURN TStatement::RowCount(SQLLEN* rowCount) { SQLRETURN TStatement::NumResultCols(SQLSMALLINT* colCount) { if (!colCount) { - return SQL_ERROR; + throw TOdbcException("HY000", 0, "Invalid parameter"); } if (!Cursor_) { *colCount = 0; diff --git a/odbc/src/statement.h b/odbc/src/statement.h index d6b47a11462..8bed3534986 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -1,7 +1,7 @@ #pragma once #include "connection.h" - +#include "utils/error_manager.h" #include "utils/bindings.h" #include "utils/cursor.h" @@ -18,7 +18,7 @@ namespace NYdb { namespace NOdbc { -class TStatement : public IBindingFiller { +class TStatement : public TErrorManager, public IBindingFiller { public: TStatement(TConnection* conn); @@ -35,9 +35,6 @@ class TStatement : public IBindingFiller { void UnbindColumns(); void ResetParams(); - SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, - SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); - SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); @@ -58,27 +55,21 @@ class TStatement : public IBindingFiller { return Conn_; } - void AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message); - - NYdb::TParams BuildParams(); - private: - std::vector GetPatternEntries(const std::string& pattern); - SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); - bool IsPatternMatch(const std::string& path, const std::string& pattern); - - std::optional GetTableType(NScheme::ESchemeEntryType type); - TConnection* Conn_; - TErrorList Errors_; - std::unique_ptr Cursor_; + std::string PreparedQuery_; + bool IsPrepared_ = false; std::vector BoundColumns_; std::vector BoundParams_; - std::string PreparedQuery_; - bool IsPrepared_ = false; + NYdb::TParams BuildParams(); + + std::vector GetPatternEntries(const std::string& pattern); + SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); + bool IsPatternMatch(const std::string& path, const std::string& pattern); + std::optional GetTableType(NScheme::ESchemeEntryType type); }; } // namespace NOdbc diff --git a/odbc/src/utils/error_manager.cpp b/odbc/src/utils/error_manager.cpp new file mode 100644 index 00000000000..da9f339af89 --- /dev/null +++ b/odbc/src/utils/error_manager.cpp @@ -0,0 +1,120 @@ +#include "error_manager.h" + +#include +#include + +namespace NYdb { +namespace NOdbc { +namespace { + struct OdbcErrorMapping { + const char* sqlState; + const char* description; + SQLRETURN returnCode; + }; + + const std::unordered_map ERROR_MAPPINGS = { + {EStatus::SUCCESS, {"00000", "Success", SQL_SUCCESS}}, + {EStatus::BAD_REQUEST, {"42000", "Syntax error or access rule violation", SQL_ERROR}}, + {EStatus::UNAUTHORIZED, {"28000", "Invalid authorization specification", SQL_ERROR}}, + {EStatus::INTERNAL_ERROR, {"HY000", "General error", SQL_ERROR}}, + {EStatus::ABORTED, {"25000", "Invalid transaction state", SQL_ERROR}}, + {EStatus::UNAVAILABLE, {"08001", "Client unable to establish connection", SQL_ERROR}}, + {EStatus::OVERLOADED, {"HY000", "General error - server overloaded", SQL_ERROR}}, + {EStatus::SCHEME_ERROR, {"42S02", "Base table or view not found", SQL_ERROR}}, + {EStatus::GENERIC_ERROR, {"HY000", "General error", SQL_ERROR}}, + {EStatus::TIMEOUT, {"HYT00", "Timeout expired", SQL_ERROR}}, + {EStatus::BAD_SESSION, {"08003", "Connection does not exist", SQL_ERROR}}, + {EStatus::PRECONDITION_FAILED, {"23000", "Integrity constraint violation", SQL_ERROR}}, + {EStatus::ALREADY_EXISTS, {"23000", "Integrity constraint violation", SQL_ERROR}}, + {EStatus::NOT_FOUND, {"02000", "No data found", SQL_NO_DATA}}, + {EStatus::SESSION_EXPIRED, {"08003", "Connection does not exist", SQL_ERROR}}, + {EStatus::CANCELLED, {"HY008", "Operation canceled", SQL_ERROR}}, + {EStatus::UNDETERMINED, {"HY000", "General error", SQL_ERROR}}, + {EStatus::UNSUPPORTED, {"HYC00", "Optional feature not implemented", SQL_ERROR}}, + {EStatus::SESSION_BUSY, {"HY000", "General error - session busy", SQL_ERROR}}, + // Transport errors + {EStatus::TRANSPORT_UNAVAILABLE, {"08001", "Client unable to establish connection", SQL_ERROR}}, + {EStatus::CLIENT_RESOURCE_EXHAUSTED, {"HY000", "General error - resource exhausted", SQL_ERROR}}, + {EStatus::CLIENT_DEADLINE_EXCEEDED, {"HYT00", "Timeout expired", SQL_ERROR}}, + {EStatus::CLIENT_INTERNAL_ERROR, {"HY000", "General error", SQL_ERROR}}, + {EStatus::CLIENT_CANCELLED, {"HY008", "Operation canceled", SQL_ERROR}}, + {EStatus::CLIENT_UNAUTHENTICATED, {"28000", "Invalid authorization specification", SQL_ERROR}}, + {EStatus::CLIENT_LIMITS_REACHED, {"HY000", "General error - limits reached", SQL_ERROR}}, + {EStatus::CLIENT_DISCOVERY_FAILED, {"08001", "Client unable to establish connection", SQL_ERROR}}, + {EStatus::CLIENT_CALL_UNIMPLEMENTED, {"HYC00", "Optional feature not implemented", SQL_ERROR}}, + {EStatus::CLIENT_OUT_OF_RANGE, {"22003", "Numeric value out of range", SQL_ERROR}}, + }; + + const OdbcErrorMapping DEFAULT_ERROR_MAPPING = {"HY000", "Unknown YDB error", SQL_ERROR}; + + OdbcErrorMapping GetErrorMappingForStatus(EStatus status) { + auto it = ERROR_MAPPINGS.find(status); + if (it != ERROR_MAPPINGS.end()) { + return it->second; + } + return DEFAULT_ERROR_MAPPING; + } +} // namespace + +SQLRETURN TErrorManager::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message, SQLRETURN returnCode) { + Errors_.push_back({sqlState, nativeError, message, returnCode}); + return returnCode; +} + +SQLRETURN TErrorManager::AddError(const TOdbcException& ex) { + Errors_.push_back({ex.GetSqlState(), ex.GetNativeError(), ex.GetMessage(), ex.GetReturnCode()}); + return ex.GetReturnCode(); +} + +SQLRETURN TErrorManager::AddError(const TStatus& status) { + auto mapping = GetErrorMappingForStatus(status.GetStatus()); + std::string message = mapping.description; + if (!status.GetIssues().Empty()) { + message += ": " + status.GetIssues().ToString(); + } + Errors_.push_back({mapping.sqlState, static_cast(status.GetStatus()), message, mapping.returnCode}); + return mapping.returnCode; +} + +void TErrorManager::ClearErrors() { + Errors_.clear(); +} + +SQLRETURN TErrorManager::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength) { + if (recNumber < 1 || recNumber > (SQLSMALLINT)Errors_.size()) { + return SQL_NO_DATA; + } + + const auto& err = Errors_[recNumber-1]; + if (sqlState) { + strncpy((char*)sqlState, err.SqlState.c_str(), 6); + } + + if (nativeError) { + *nativeError = err.NativeError; + } + + if (messageText && bufferLength > 0) { + strncpy((char*)messageText, err.Message.c_str(), bufferLength); + if (textLength) { + *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); + } + } + return SQL_SUCCESS; +} + +SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { + if (!handlePtr) { + return SQL_INVALID_HANDLE; + } + + try { + return func(); + } catch (...) { + return SQL_ERROR; + } +} + +} // namespace NOdbc +} // namespace NYdb \ No newline at end of file diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h new file mode 100644 index 00000000000..1e31349964d --- /dev/null +++ b/odbc/src/utils/error_manager.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace NYdb { +namespace NOdbc { + +struct TErrorInfo { + std::string SqlState; + SQLINTEGER NativeError; + std::string Message; + SQLRETURN ReturnCode; +}; + +using TErrorList = std::vector; + +class TOdbcException : public std::exception { +public: + TOdbcException(const std::string& sqlState, SQLINTEGER nativeError, + const std::string& message, SQLRETURN returnCode = SQL_ERROR) + : SqlState_(sqlState) + , NativeError_(nativeError) + , Message_(message) + , ReturnCode_(returnCode) + {} + + const std::string& GetSqlState() const { + return SqlState_; + } + + SQLINTEGER GetNativeError() const { + return NativeError_; + } + + const std::string& GetMessage() const { + return Message_; + } + + SQLRETURN GetReturnCode() const { + return ReturnCode_; + } + + const char* what() const noexcept override { + return Message_.c_str(); + } + +private: + std::string SqlState_; + SQLINTEGER NativeError_; + std::string Message_; + SQLRETURN ReturnCode_; +}; + +class TErrorManager { +public: + SQLRETURN AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message, SQLRETURN returnCode = SQL_ERROR); + SQLRETURN AddError(const TOdbcException& ex); + SQLRETURN AddError(const TStatus& status); + + void ClearErrors(); + + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, + SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + +private: + TErrorList Errors_; +}; + +template +SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { + if (!handlePtr) { + return SQL_INVALID_HANDLE; + } + auto handle = static_cast(handlePtr); + + try { + return func(handle); + } catch (const NStatusHelpers::TYdbErrorException& ex) { + return handle->AddError(ex.GetStatus()); + } catch (const TOdbcException& ex) { + return handle->AddError(ex); + } catch (const std::exception& ex) { + return handle->AddError("HY000", 0, ex.what()); + } catch (...) { + return handle->AddError("HY000", 0, "Unknown error"); + } +} + +SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func); + +} // namespace NOdbc +} // namespace NYdb From 46833929bbc879330c7100dd5abb779d90030a4a Mon Sep 17 00:00:00 2001 From: Ylonies Date: Tue, 7 Apr 2026 14:12:28 +0000 Subject: [PATCH 13/55] some fixes --- odbc/CMakeLists.txt | 3 +- odbc/src/connection.cpp | 17 ++++++++ odbc/src/connection.h | 4 ++ odbc/src/odbc_driver.cpp | 39 +++++++++++++++-- odbc/src/statement.cpp | 75 ++++++++++++++++++++++++++------ odbc/src/statement.h | 5 +++ odbc/src/utils/bindings.h | 5 +++ odbc/src/utils/cursor.cpp | 22 +++++++--- odbc/src/utils/cursor.h | 5 ++- odbc/src/utils/error_manager.cpp | 54 ++++++++++++++++++++++- odbc/src/utils/error_manager.h | 12 ++++- 11 files changed, 212 insertions(+), 29 deletions(-) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 799c9b89b19..9919870702d 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -23,7 +23,6 @@ target_link_libraries(ydb-odbc YDB-CPP-SDK::Table YDB-CPP-SDK::Scheme YDB-CPP-SDK::Driver - ODBC::ODBC ) set_target_properties(ydb-odbc PROPERTIES @@ -43,7 +42,7 @@ add_subdirectory(tests) include(GNUInstallDirs) -install(FILES +install(FILES odbcinst.ini DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d RENAME ydb-odbc.ini diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 7ed7679e015..eb142108334 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -79,11 +79,24 @@ SQLRETURN TConnection::Connect(const std::string& serverName, } SQLRETURN TConnection::Disconnect() { + QuerySession_.reset(); + Tx_.reset(); + YdbSchemeClient_.reset(); + YdbTableClient_.reset(); YdbClient_.reset(); YdbDriver_.reset(); return SQL_SUCCESS; } +NQuery::TSession& TConnection::GetOrCreateQuerySession() { + if (!QuerySession_) { + auto sessionResult = YdbClient_->GetSession().ExtractValueSync(); + NStatusHelpers::ThrowOnError(sessionResult); + QuerySession_.emplace(std::move(sessionResult.GetSession())); + } + return *QuerySession_; +} + std::unique_ptr TConnection::CreateStatement() { return std::make_unique(this); } @@ -115,6 +128,10 @@ void TConnection::SetTx(const NQuery::TTransaction& tx) { Tx_ = tx; } +void TConnection::Reset() { + Tx_.reset(); +} + SQLRETURN TConnection::CommitTx() { auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); diff --git a/odbc/src/connection.h b/odbc/src/connection.h index ad69b0f171c..a0ce4acb991 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -27,6 +27,8 @@ class TConnection : public TErrorManager { std::unique_ptr YdbTableClient_; std::unique_ptr YdbSchemeClient_; std::optional Tx_; + /** Одна сессия KQP на ODBC-соединение: DDL/DML/SELECT видят одну и ту же схему без «новой» сессии на каждый Execute. */ + std::optional QuerySession_; std::vector> Statements_; std::string Endpoint_; @@ -47,6 +49,7 @@ class TConnection : public TErrorManager { void RemoveStatement(TStatement* stmt); NYdb::NQuery::TQueryClient* GetClient() { return YdbClient_.get(); } + NQuery::TSession& GetOrCreateQuerySession(); NYdb::NTable::TTableClient* GetTableClient() { return YdbTableClient_.get(); } NScheme::TSchemeClient* GetSchemeClient() { return YdbSchemeClient_.get(); } @@ -55,6 +58,7 @@ class TConnection : public TErrorManager { const std::optional& GetTx(); void SetTx(const NQuery::TTransaction& tx); + void Reset(); SQLRETURN CommitTx(); SQLRETURN RollbackTx(); diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index c047f770837..f26bd55c828 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -29,10 +29,13 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, switch (handleType) { case SQL_HANDLE_ENV: { - return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&]() { - *outputHandle = new NYdb::NOdbc::TEnvironment(); - return SQL_SUCCESS; - }); + return NYdb::NOdbc::HandleOdbcExceptions( + inputHandle, + [&]() { + *outputHandle = new NYdb::NOdbc::TEnvironment(); + return SQL_SUCCESS; + }, + NYdb::NOdbc::ENullInputHandlePolicy::Allow); } case SQL_HANDLE_DBC: { @@ -208,6 +211,34 @@ SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, } } +SQLRETURN SQL_API SQLGetDiagField(SQLSMALLINT handleType, + SQLHANDLE handle, + SQLSMALLINT recNumber, + SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + switch (handleType) { + case SQL_HANDLE_ENV: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* env) { + return env->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); + }); + } + case SQL_HANDLE_DBC: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* conn) { + return conn->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); + }); + } + case SQL_HANDLE_STMT: { + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) { + return stmt->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); + }); + } + default: + return SQL_ERROR; + } +} + SQLRETURN SQL_API SQLBindParameter(SQLHSTMT statementHandle, SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index b61b8f07eb2..b7fddc624bf 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -14,6 +14,7 @@ TStatement::TStatement(TConnection* conn) : Conn_(conn) {} SQLRETURN TStatement::Prepare(const std::string& statementText) { + StreamFetchError_ = false; Cursor_.reset(); PreparedQuery_ = statementText; IsPrepared_ = true; @@ -24,40 +25,86 @@ SQLRETURN TStatement::Execute() { if (!IsPrepared_ || PreparedQuery_.empty()) { throw TOdbcException("HY007", 0, "No prepared statement"); } + StreamFetchError_ = false; Cursor_.reset(); auto* client = Conn_->GetClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } NYdb::TParams params = BuildParams(); - - if (!Conn_->GetTx()) { - auto sessionResult = client->GetSession().ExtractValueSync(); - NStatusHelpers::ThrowOnError(sessionResult); - - auto session = sessionResult.GetSession(); - auto beginTxResult = session.BeginTransaction(NQuery::TTxSettings::SerializableRW()).ExtractValueSync(); - NStatusHelpers::ThrowOnError(beginTxResult); - Conn_->SetTx(beginTxResult.GetTransaction()); + if (Conn_->GetAutocommit()){ + Conn_->Reset(); } - auto session = Conn_->GetTx()->GetSession(); - auto iterator = session.StreamExecuteQuery(PreparedQuery_, - NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(Conn_->GetAutocommit()), params).ExtractValueSync(); + + auto& session = Conn_->GetOrCreateQuerySession(); + + auto iterator = CreateExecuteIterator(session, params); NStatusHelpers::ThrowOnError(iterator); - Cursor_ = CreateExecCursor(this, std::move(iterator)); + std::optional prefetchedResultPart = PrefetchFirstResultPart(iterator); + if (prefetchedResultPart) { + Cursor_ = CreateExecCursor(this, std::move(iterator), std::move(prefetchedResultPart)); + } else { + Cursor_.reset(); + } IsPrepared_ = false; PreparedQuery_.clear(); return SQL_SUCCESS; } +NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ + if (Conn_->GetAutocommit()) { + return session.StreamExecuteQuery( + PreparedQuery_, + NQuery::TTxControl::NoTx(), + params).ExtractValueSync(); + } + if (!Conn_->GetTx()) { + auto beginTxResult = session.BeginTransaction(NQuery::TTxSettings::SerializableRW()).ExtractValueSync(); + NStatusHelpers::ThrowOnError(beginTxResult); + Conn_->SetTx(beginTxResult.GetTransaction()); + } + return session.StreamExecuteQuery( + PreparedQuery_, + NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(false), + params).ExtractValueSync(); +} + +std::optional TStatement::PrefetchFirstResultPart(NQuery::TExecuteQueryIterator& iterator){ + std::optional prefetchedResultPart; + while (true) { + auto part = iterator.ReadNext().ExtractValueSync(); + if (part.EOS()) { + break; + } + if (!part.IsSuccess()) { + NStatusHelpers::ThrowOnError(part); + } + if (part.HasResultSet()) { + prefetchedResultPart.emplace(std::move(part)); + break; + } + } + return prefetchedResultPart; +} + SQLRETURN TStatement::Fetch() { if (!Cursor_) { Cursor_.reset(); return SQL_NO_DATA; } - return Cursor_->Fetch() ? SQL_SUCCESS : SQL_NO_DATA; + StreamFetchError_ = false; + if (!Cursor_->Fetch()) { + return StreamFetchError_ ? SQL_ERROR : SQL_NO_DATA; + } + return SQL_SUCCESS; +} + +void TStatement::OnStreamPartError(const TStatus& status) { + ClearErrors(); + AddError(status); + StreamFetchError_ = true; } SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 8bed3534986..f17780957bb 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -30,6 +30,7 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); void FillBoundColumns() override; + void OnStreamPartError(const TStatus& status) override; SQLRETURN Close(bool force = false); void UnbindColumns(); @@ -63,9 +64,13 @@ class TStatement : public TErrorManager, public IBindingFiller { std::vector BoundColumns_; std::vector BoundParams_; + bool StreamFetchError_ = false; NYdb::TParams BuildParams(); + NQuery::TExecuteQueryIterator CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params); + std::optional PrefetchFirstResultPart(NQuery::TExecuteQueryIterator& iterator); + std::vector GetPatternEntries(const std::string& pattern); SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); bool IsPatternMatch(const std::string& path, const std::string& pattern); diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h index df76de4e951..443d9787d70 100644 --- a/odbc/src/utils/bindings.h +++ b/odbc/src/utils/bindings.h @@ -3,6 +3,8 @@ #include #include +#include + namespace NYdb { namespace NOdbc { @@ -29,6 +31,9 @@ struct TBoundColumn { class IBindingFiller { public: virtual void FillBoundColumns() = 0; + virtual void OnStreamPartError(const TStatus& status) { + (void)status; + } virtual ~IBindingFiller() = default; }; diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index fbd10588aba..efbcea9a419 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -8,9 +8,11 @@ namespace NOdbc { class TExecCursor : public ICursor { public: - TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator) + TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator, + std::optional prefetchedPart) : BindingFiller_(bindingFiller) , Iterator_(std::move(iterator)) + , PrefetchedPart_(std::move(prefetchedPart)) {} bool Fetch() override { @@ -22,11 +24,19 @@ class TExecCursor : public ICursor { } ResultSetParser_.reset(); } - auto part = Iterator_.ReadNext().ExtractValueSync(); + NQuery::TExecuteQueryPart part = [&]() { + if (PrefetchedPart_) { + auto p = std::move(*PrefetchedPart_); + PrefetchedPart_.reset(); + return p; + } + return Iterator_.ReadNext().ExtractValueSync(); + }(); if (part.EOS()) { return false; } if (!part.IsSuccess()) { + BindingFiller_->OnStreamPartError(part); return false; } if (part.HasResultSet()) { @@ -62,7 +72,7 @@ class TExecCursor : public ICursor { IBindingFiller* BindingFiller_; NQuery::TExecuteQueryIterator Iterator_; - // std::optional Part_; + std::optional PrefetchedPart_; std::unique_ptr ResultSetParser_; std::vector Columns_; }; @@ -107,8 +117,10 @@ class TVirtualCursor : public ICursor { int64_t Cursor_ = -1; }; -std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator) { - return std::make_unique(bindingFiller, std::move(iterator)); +std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, + NQuery::TExecuteQueryIterator iterator, + std::optional prefetchedPart) { + return std::make_unique(bindingFiller, std::move(iterator), std::move(prefetchedPart)); } std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) { diff --git a/odbc/src/utils/cursor.h b/odbc/src/utils/cursor.h index e4b13ed5215..22828f66144 100644 --- a/odbc/src/utils/cursor.h +++ b/odbc/src/utils/cursor.h @@ -6,6 +6,7 @@ #include +#include #include #include @@ -30,7 +31,9 @@ class ICursor { virtual const std::vector& GetColumnMeta() const = 0; }; -std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, NYdb::NQuery::TExecuteQueryIterator iterator); +std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, + NYdb::NQuery::TExecuteQueryIterator iterator, + std::optional prefetchedPart = std::nullopt); std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table); } // namespace NOdbc diff --git a/odbc/src/utils/error_manager.cpp b/odbc/src/utils/error_manager.cpp index da9f339af89..fbb577e3824 100644 --- a/odbc/src/utils/error_manager.cpp +++ b/odbc/src/utils/error_manager.cpp @@ -104,8 +104,58 @@ SQLRETURN TErrorManager::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQ return SQL_SUCCESS; } -SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { - if (!handlePtr) { +SQLRETURN TErrorManager::GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { + const SQLSMALLINT count = static_cast(Errors_.size()); + + if (recNumber == 0) { + if (diagIdentifier == SQL_DIAG_NUMBER) { + if (!diagInfoPtr) { + return SQL_ERROR; + } + *static_cast(diagInfoPtr) = count; + return SQL_SUCCESS; + } + return SQL_NO_DATA; + } + + if (recNumber < 1 || recNumber > count) { + return SQL_NO_DATA; + } + + const auto& err = Errors_[recNumber - 1]; + switch (diagIdentifier) { + case SQL_DIAG_SQLSTATE: + if (!diagInfoPtr) { + return SQL_ERROR; + } + strncpy((char*)diagInfoPtr, err.SqlState.c_str(), 6); + return SQL_SUCCESS; + case SQL_DIAG_NATIVE: + if (!diagInfoPtr) { + return SQL_ERROR; + } + *static_cast(diagInfoPtr) = err.NativeError; + return SQL_SUCCESS; + case SQL_DIAG_MESSAGE_TEXT: + if (!diagInfoPtr || bufferLength <= 0) { + return SQL_ERROR; + } + strncpy((char*)diagInfoPtr, err.Message.c_str(), bufferLength); + if (stringLengthPtr) { + *stringLengthPtr = static_cast(err.Message.size()); + } + return SQL_SUCCESS; + default: + return SQL_NO_DATA; + } +} + +SQLRETURN HandleOdbcExceptions( + SQLHANDLE handlePtr, + std::function&& func, + ENullInputHandlePolicy nullInputPolicy) { + if (!handlePtr && nullInputPolicy != ENullInputHandlePolicy::Allow) { return SQL_INVALID_HANDLE; } diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h index 1e31349964d..5f72a69f563 100644 --- a/odbc/src/utils/error_manager.h +++ b/odbc/src/utils/error_manager.h @@ -66,11 +66,18 @@ class TErrorManager { SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); + SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr); private: TErrorList Errors_; }; +enum class ENullInputHandlePolicy : unsigned char { + Reject, + Allow, +}; + template SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { if (!handlePtr) { @@ -91,7 +98,10 @@ SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func); +SQLRETURN HandleOdbcExceptions( + SQLHANDLE handlePtr, + std::function&& func, + ENullInputHandlePolicy nullInputPolicy = ENullInputHandlePolicy::Reject); } // namespace NOdbc } // namespace NYdb From 08cd57c2745c5e1a41e96db97abb895286fb2f95 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Tue, 7 Apr 2026 16:18:04 +0000 Subject: [PATCH 14/55] env features EndTran for env + tests --- odbc/src/connection.cpp | 11 +++ odbc/src/connection.h | 5 +- odbc/src/environment.cpp | 51 ++++++++++++++ odbc/src/environment.h | 9 +++ odbc/src/odbc_driver.cpp | 16 +++-- odbc/tests/integration/CMakeLists.txt | 5 ++ odbc/tests/integration/basic_it.cpp | 24 +------ odbc/tests/integration/env_it.cpp | 99 +++++++++++++++++++++++++++ odbc/tests/integration/test_utils.h | 25 +++++++ 9 files changed, 217 insertions(+), 28 deletions(-) create mode 100644 odbc/tests/integration/env_it.cpp create mode 100644 odbc/tests/integration/test_utils.h diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index eb142108334..29b6758b2a8 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -146,5 +146,16 @@ SQLRETURN TConnection::RollbackTx() { return SQL_SUCCESS; } +void TConnection::SetEnvironment(TEnvironment* env){ + if (ParentEnv_){ + throw std::logic_error("Connection already bound to environment"); + } + ParentEnv_ = env; +} + +TEnvironment* TConnection::GetEnvironment(){ + return ParentEnv_; +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/connection.h b/odbc/src/connection.h index a0ce4acb991..f048e1cef4f 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -27,13 +27,13 @@ class TConnection : public TErrorManager { std::unique_ptr YdbTableClient_; std::unique_ptr YdbSchemeClient_; std::optional Tx_; - /** Одна сессия KQP на ODBC-соединение: DDL/DML/SELECT видят одну и ту же схему без «новой» сессии на каждый Execute. */ std::optional QuerySession_; std::vector> Statements_; std::string Endpoint_; std::string Database_; std::string AuthToken_; + TEnvironment* ParentEnv_; bool Autocommit_ = true; @@ -62,6 +62,9 @@ class TConnection : public TErrorManager { SQLRETURN CommitTx(); SQLRETURN RollbackTx(); + + void SetEnvironment(TEnvironment* env); + TEnvironment* GetEnvironment(); }; } // namespace NOdbc diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index 541ca9e2160..e66af68f11f 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -13,5 +13,56 @@ SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQL return SQL_SUCCESS; } +void TEnvironment::RegisterConnection(TConnection* conn){ + if (conn == nullptr){ + throw std::invalid_argument("null connection"); + } + connections_.insert(conn); +} + +void TEnvironment::UnregisterConnection(TConnection* conn){ + if (conn == nullptr){ + throw std::invalid_argument("null connection"); + } + connections_.erase(conn); +} + +std::vector TEnvironment::GetConnectionsSnapshot() const { + return std::vector(connections_.begin(), connections_.end()); +} + +SQLRETURN TEnvironment::EndTran(SQLSMALLINT completionType){ + if (completionType != SQL_COMMIT && completionType != SQL_ROLLBACK){ + return AddError("HY012", 0, "Invalid transaction operation code"); + } + bool hasFailures = false; + int failedCount = 0; + + for (auto* conn : connections_) { + if (!conn || !conn->GetTx()) { + continue; + } + try { + if (completionType == SQL_COMMIT) { + conn->CommitTx(); + } else { + conn->RollbackTx(); + } + } catch (const std::exception& ex) { + hasFailures = true; + ++failedCount; + AddError("HY000", 0, ex.what(), SQL_SUCCESS_WITH_INFO); + } catch (...) { + hasFailures = true; + ++failedCount; + AddError("HY000", 0, "Unknown error during ENV-level transaction completion", SQL_SUCCESS_WITH_INFO); + } + } + if (hasFailures) { + AddError("01000", 0, "SQLEndTran(SQL_HANDLE_ENV): some connections failed", SQL_SUCCESS_WITH_INFO); + return SQL_SUCCESS_WITH_INFO; + } + return SQL_SUCCESS; +} } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/environment.h b/odbc/src/environment.h index 5258b722492..70a785f45d7 100644 --- a/odbc/src/environment.h +++ b/odbc/src/environment.h @@ -4,6 +4,8 @@ #include #include +#include +#include namespace NYdb { namespace NOdbc { @@ -13,12 +15,19 @@ class TConnection; class TEnvironment : public TErrorManager { private: SQLINTEGER OdbcVersion_; + std::unordered_set connections_; public: TEnvironment(); ~TEnvironment(); SQLRETURN SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength); + + void RegisterConnection(TConnection*); + void UnregisterConnection(TConnection*); + std::vector GetConnectionsSnapshot() const; + + SQLRETURN EndTran(SQLSMALLINT completionType); }; } // namespace NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index f26bd55c828..2993c76fcef 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -39,8 +39,11 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, } case SQL_HANDLE_DBC: { - return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&]() { - *outputHandle = new NYdb::NOdbc::TConnection(); + return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&](auto* env) { + auto conn = std::make_unique(); + conn->SetEnvironment(env); + env->RegisterConnection(conn.get()); + *outputHandle = conn.release(); return SQL_SUCCESS; }); } @@ -66,6 +69,10 @@ SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { } case SQL_HANDLE_DBC: { return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* conn) { + auto* env = conn->GetEnvironment(); + if (env != nullptr){ + env->UnregisterConnection(conn); + } delete conn; return SQL_SUCCESS; }); @@ -281,8 +288,9 @@ SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLI }); } case SQL_HANDLE_ENV: { - // TODO: if's list of connections in ENV, go through them and commit/rollback transactions - return SQL_SUCCESS; + return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* env) -> SQLRETURN { + return env->EndTran(completionType); + }); } default: return SQL_ERROR; diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index e1aad9d3913..0360679931c 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -2,3 +2,8 @@ add_odbc_test(NAME odbc-basic_it SOURCES basic_it.cpp ) + +add_odbc_test(NAME odbc-env_it + SOURCES + env_it.cpp +) diff --git a/odbc/tests/integration/basic_it.cpp b/odbc/tests/integration/basic_it.cpp index b4c7078ac4e..37973667147 100644 --- a/odbc/tests/integration/basic_it.cpp +++ b/odbc/tests/integration/basic_it.cpp @@ -1,26 +1,4 @@ -#include - -#include -#include - -#include - - -#define CHECK_ODBC_OK(rc, handle, type) \ - ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) - -std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { - SQLCHAR sqlState[6], message[256]; - SQLINTEGER nativeError; - SQLSMALLINT textLength; - SQLRETURN rc = SQLGetDiagRec(type, handle, 1, sqlState, &nativeError, message, sizeof(message), &textLength); - if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { - return std::string((char*)sqlState) + ": " + (char*)message; - } - return "Unknown ODBC error"; -} - -const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; +#include "test_utils.h" TEST(OdbcBasic, SimpleQuery) { SQLHENV env; diff --git a/odbc/tests/integration/env_it.cpp b/odbc/tests/integration/env_it.cpp new file mode 100644 index 00000000000..fd351d127af --- /dev/null +++ b/odbc/tests/integration/env_it.cpp @@ -0,0 +1,99 @@ +#include "test_utils.h" + +namespace { + +void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, *env, dbc), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); +} + +void StartManualTx(SQLHDBC dbc, SQLHSTMT* stmt) { + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(*stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), *stmt, SQL_HANDLE_STMT); +} + +} // namespace + +TEST(OdbcEnv, EndTranCommitOnEnv) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + StartManualTx(dbc2, &stmt2); + + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT), env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcEnv, EndTranRollbackOnEnv) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + StartManualTx(dbc2, &stmt2); + + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_ROLLBACK), env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcEnv, EndTranPartialFailureReturnsInfo) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc2, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc2, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc2, &stmt2), SQL_SUCCESS); + (void)SQLExecDirect(stmt2, (SQLCHAR*)"SELECT FROM", SQL_NTS); + + rc = SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO || rc == SQL_ERROR) + << GetOdbcError(env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h new file mode 100644 index 00000000000..c43272f0f54 --- /dev/null +++ b/odbc/tests/integration/test_utils.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include +#include + +#include + +#define CHECK_ODBC_OK(rc, handle, type) \ + ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) + +inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { + SQLCHAR sqlState[6] = {0}; + SQLCHAR message[256] = {0}; + SQLINTEGER nativeError = 0; + SQLSMALLINT textLength = 0; + SQLRETURN rc = SQLGetDiagRec(type, handle, 1, sqlState, &nativeError, message, sizeof(message), &textLength); + if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { + return std::string((char*)sqlState) + ": " + (char*)message; + } + return "Unknown ODBC error"; +} + +inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; From b5bf9fa39bc22339b9aac816c25a3b6cba76225e Mon Sep 17 00:00:00 2001 From: Ylonies Date: Wed, 8 Apr 2026 11:54:28 +0000 Subject: [PATCH 15/55] attributes --- odbc/src/connection.cpp | 24 ++++- odbc/src/connection.h | 8 +- odbc/src/connection_attributes.cpp | 141 +++++++++++++++++++++++++++++ odbc/src/connection_attributes.h | 48 ++++++++++ odbc/src/environment.cpp | 20 +++- odbc/src/odbc_driver.cpp | 20 ++-- odbc/src/statement.cpp | 13 ++- 7 files changed, 250 insertions(+), 24 deletions(-) create mode 100644 odbc/src/connection_attributes.cpp create mode 100644 odbc/src/connection_attributes.h diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 29b6758b2a8..b1049163a5d 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -2,9 +2,8 @@ #include "statement.h" #include "utils/error_manager.h" -#include -#include #include +#include #include #include @@ -107,8 +106,8 @@ void TConnection::RemoveStatement(TStatement* stmt) { } SQLRETURN TConnection::SetAutocommit(bool value) { - Autocommit_ = value; - if (Autocommit_ && Tx_) { + Attributes_.SetAutocommit(value); + if (Attributes_.GetAutocommit() && Tx_) { auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); @@ -117,7 +116,22 @@ SQLRETURN TConnection::SetAutocommit(bool value) { } bool TConnection::GetAutocommit() const { - return Autocommit_; + return Attributes_.GetAutocommit(); +} + +SQLRETURN TConnection::SetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength) { + return Attributes_.SetConnectAttr(attr, value, stringLength, [this](bool autocommit) { + return SetAutocommit(autocommit); + }, *this); +} + +SQLRETURN TConnection::GetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + return Attributes_.GetConnectAttr(attr, value, bufferLength, stringLengthPtr, *this); +} + +NQuery::TTxSettings TConnection::MakeTxSettings() const { + return Attributes_.MakeTxSettings(); } const std::optional& TConnection::GetTx() { diff --git a/odbc/src/connection.h b/odbc/src/connection.h index f048e1cef4f..e1c9028fa84 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -1,6 +1,7 @@ #pragma once #include "environment.h" +#include "connection_attributes.h" #include "utils/error_manager.h" #include @@ -35,8 +36,7 @@ class TConnection : public TErrorManager { std::string AuthToken_; TEnvironment* ParentEnv_; - bool Autocommit_ = true; - + TConnectionAttributes Attributes_; public: SQLRETURN Connect(const std::string& serverName, const std::string& userName, @@ -56,6 +56,10 @@ class TConnection : public TErrorManager { SQLRETURN SetAutocommit(bool value); bool GetAutocommit() const; + SQLRETURN SetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength); + SQLRETURN GetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); + NQuery::TTxSettings MakeTxSettings() const; + const std::optional& GetTx(); void SetTx(const NQuery::TTransaction& tx); void Reset(); diff --git a/odbc/src/connection_attributes.cpp b/odbc/src/connection_attributes.cpp new file mode 100644 index 00000000000..61bfa7f8caa --- /dev/null +++ b/odbc/src/connection_attributes.cpp @@ -0,0 +1,141 @@ +#include "connection_attributes.h" + +#include + +namespace NYdb { +namespace NOdbc { + +std::optional TConnectionAttributes::ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation) { + if (accessMode == SQL_MODE_READ_ONLY) { + switch (txnIsolation) { + case SQL_TXN_READ_UNCOMMITTED: + return NQuery::TTxSettings::TS_STALE_RO; + case SQL_TXN_READ_COMMITTED: + return NQuery::TTxSettings::TS_ONLINE_RO; + case SQL_TXN_REPEATABLE_READ: + case SQL_TXN_SERIALIZABLE: + return NQuery::TTxSettings::TS_SNAPSHOT_RO; + default: + return std::nullopt; + } + } + + switch (txnIsolation) { + case SQL_TXN_REPEATABLE_READ: + case SQL_TXN_SERIALIZABLE: + return NQuery::TTxSettings::TS_SERIALIZABLE_RW; + default: + return std::nullopt; + } +} + +SQLRETURN TConnectionAttributes::SetAutocommit(bool value) { + Autocommit_ = value; + return SQL_SUCCESS; +} + +bool TConnectionAttributes::GetAutocommit() const { + return Autocommit_; +} + +SQLRETURN TConnectionAttributes::SetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER /*stringLength*/, + const std::function& applyAutocommit, + TErrorManager& errors) { + switch (attr) { + case SQL_ATTR_AUTOCOMMIT: { + const intptr_t val = reinterpret_cast(value); + if (val == static_cast(SQL_AUTOCOMMIT_ON)) { + return applyAutocommit(true); + } + if (val == static_cast(SQL_AUTOCOMMIT_OFF)) { + return applyAutocommit(false); + } + return errors.AddError("HY024", 0, "Invalid SQL_ATTR_AUTOCOMMIT value"); + } + case SQL_ATTR_ACCESS_MODE: { + const intptr_t val = reinterpret_cast(value); + if (val == static_cast(SQL_MODE_READ_WRITE)) { + AccessMode_ = SQL_MODE_READ_WRITE; + auto txMode = ResolveTxMode(AccessMode_, TxnIsolation_); + if (!txMode) { + return errors.AddError("HYC00", 0, "Transaction isolation is not supported for read-write mode"); + } + TxMode_ = *txMode; + return SQL_SUCCESS; + } + if (val == static_cast(SQL_MODE_READ_ONLY)) { + AccessMode_ = SQL_MODE_READ_ONLY; + auto txMode = ResolveTxMode(AccessMode_, TxnIsolation_); + if (!txMode) { + return errors.AddError("HYC00", 0, "Transaction isolation is not supported for read-only mode"); + } + TxMode_ = *txMode; + return SQL_SUCCESS; + } + return errors.AddError("HY024", 0, "Invalid SQL_ATTR_ACCESS_MODE value"); + } + case SQL_ATTR_TXN_ISOLATION: { + const intptr_t val = reinterpret_cast(value); + const SQLUINTEGER isolation = static_cast(val); + auto txMode = ResolveTxMode(AccessMode_, isolation); + if (!txMode) { + return errors.AddError("HYC00", 0, "SQL_ATTR_TXN_ISOLATION value is not supported"); + } + TxnIsolation_ = isolation; + TxMode_ = *txMode; + return SQL_SUCCESS; + } + default: + return errors.AddError("HYC00", 0, "Optional feature not implemented"); + } +} + +SQLRETURN TConnectionAttributes::GetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER /*bufferLength*/, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const { + if (!value) { + return errors.AddError("HY009", 0, "Invalid use of null pointer"); + } + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + auto* out = reinterpret_cast(value); + switch (attr) { + case SQL_ATTR_AUTOCOMMIT: + *out = GetAutocommit() ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF; + return SQL_SUCCESS; + case SQL_ATTR_ACCESS_MODE: + *out = AccessMode_; + return SQL_SUCCESS; + case SQL_ATTR_TXN_ISOLATION: + *out = TxnIsolation_; + return SQL_SUCCESS; + default: + return errors.AddError("HYC00", 0, "Optional feature not implemented"); + } +} + +NQuery::TTxSettings TConnectionAttributes::MakeTxSettings() const { + switch (TxMode_) { + case NQuery::TTxSettings::TS_ONLINE_RO: + return NQuery::TTxSettings::OnlineRO(); + case NQuery::TTxSettings::TS_STALE_RO: + return NQuery::TTxSettings::StaleRO(); + case NQuery::TTxSettings::TS_SNAPSHOT_RO: + return NQuery::TTxSettings::SnapshotRO(); + case NQuery::TTxSettings::TS_SNAPSHOT_RW: + return NQuery::TTxSettings::SnapshotRW(); + case NQuery::TTxSettings::TS_SERIALIZABLE_RW: + default: + return NQuery::TTxSettings::SerializableRW(); + } +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/connection_attributes.h b/odbc/src/connection_attributes.h new file mode 100644 index 00000000000..7b2f0fc7221 --- /dev/null +++ b/odbc/src/connection_attributes.h @@ -0,0 +1,48 @@ +#pragma once + +#include "utils/error_manager.h" + +#include + +#include +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +class TConnectionAttributes { +public: + SQLRETURN SetAutocommit(bool value); + bool GetAutocommit() const; + + SQLRETURN SetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER stringLength, + const std::function& applyAutocommit, + TErrorManager& errors); + + SQLRETURN GetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const; + + NQuery::TTxSettings MakeTxSettings() const; + +private: + static std::optional ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation); + +private: + bool Autocommit_ = true; + SQLUINTEGER AccessMode_ = SQL_MODE_READ_WRITE; + SQLUINTEGER TxnIsolation_ = SQL_TXN_SERIALIZABLE; + NQuery::TTxSettings::ETransactionMode TxMode_ = NQuery::TTxSettings::TS_SERIALIZABLE_RW; +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index e66af68f11f..44e3473d023 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -8,9 +8,23 @@ TEnvironment::TEnvironment() : OdbcVersion_(SQL_OV_ODBC3) {} TEnvironment::~TEnvironment() {} SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { - // TODO: реализовать обработку атрибутов - OdbcVersion_ = attribute == SQL_ATTR_ODBC_VERSION ? reinterpret_cast(value) : 0; - return SQL_SUCCESS; + switch (attribute) { + case SQL_ATTR_ODBC_VERSION: { + if (!value) { + return AddError("HY009", 0, "Invalid use of null pointer"); + } + OdbcVersion_ = static_cast(reinterpret_cast(value)); + return SQL_SUCCESS; + } + case SQL_ATTR_OUTPUT_NTS: { + if (value && static_cast(reinterpret_cast(value)) != SQL_TRUE) { + return AddError("HY024", 0, "SQL_ATTR_OUTPUT_NTS must be SQL_TRUE"); + } + return SQL_SUCCESS; + } + default: + return AddError("HYC00", 0, "Optional feature not implemented"); + } } void TEnvironment::RegisterConnection(TConnection* conn){ diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 2993c76fcef..6b516c63bb8 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -299,17 +299,14 @@ SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLI SQLRETURN SQL_API SQLSetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { - if (attribute == SQL_ATTR_AUTOCOMMIT) { - if ((intptr_t)value == SQL_AUTOCOMMIT_ON) { - return conn->SetAutocommit(true); - } else if ((intptr_t)value == SQL_AUTOCOMMIT_OFF) { - return conn->SetAutocommit(false); - } else { - throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid autocommit value"); - } - } - // TODO: other attributes - throw NYdb::NOdbc::TOdbcException("HYC00", 0, "Optional feature not implemented"); + return conn->SetConnectAttr(attribute, value, stringLength); + }); +} + +SQLRETURN SQL_API SQLGetConnectAttr(SQLHDBC connectionHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + return conn->GetConnectAttr(attribute, value, bufferLength, stringLengthPtr); }); } @@ -373,6 +370,7 @@ SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT statementHandle, SQLSMALLINT fetchOrie } else { throw NYdb::NOdbc::TOdbcException("HYC00", 0, "Only SQL_FETCH_NEXT is supported"); } + //TODO other fetch-orientation }); } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index b7fddc624bf..6f714d0b0bf 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -36,7 +36,7 @@ SQLRETURN TStatement::Execute() { if (Conn_->GetAutocommit()){ Conn_->Reset(); } - + auto& session = Conn_->GetOrCreateQuerySession(); auto iterator = CreateExecuteIterator(session, params); @@ -55,13 +55,20 @@ SQLRETURN TStatement::Execute() { NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ if (Conn_->GetAutocommit()) { + const auto txSettings = Conn_->MakeTxSettings(); + if (txSettings.GetMode() == NQuery::TTxSettings::TS_SERIALIZABLE_RW) { + return session.StreamExecuteQuery( + PreparedQuery_, + NQuery::TTxControl::NoTx(), + params).ExtractValueSync(); + } return session.StreamExecuteQuery( PreparedQuery_, - NQuery::TTxControl::NoTx(), + NQuery::TTxControl::BeginTx(txSettings).CommitTx(), params).ExtractValueSync(); } if (!Conn_->GetTx()) { - auto beginTxResult = session.BeginTransaction(NQuery::TTxSettings::SerializableRW()).ExtractValueSync(); + auto beginTxResult = session.BeginTransaction(Conn_->MakeTxSettings()).ExtractValueSync(); NStatusHelpers::ThrowOnError(beginTxResult); Conn_->SetTx(beginTxResult.GetTransaction()); } From 35b5492d6ccee0c3cd02ca0f349e0d9d97fc51fa Mon Sep 17 00:00:00 2001 From: Ylonies Date: Thu, 9 Apr 2026 10:30:42 +0000 Subject: [PATCH 16/55] fix --- odbc/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 9919870702d..0390b20698c 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(ydb-odbc SHARED src/utils/convert.cpp src/utils/error_manager.cpp src/odbc_driver.cpp + src/connection_attributes.cpp src/connection.cpp src/statement.cpp src/environment.cpp From 94aa4abe215fb09df0a0aa7723292ac5897a8549 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Fri, 17 Apr 2026 13:37:51 +0300 Subject: [PATCH 17/55] driver pool --- odbc/src/connection.cpp | 91 +++++++++++++++++++++++++++++++++++++++++ odbc/src/connection.h | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index b1049163a5d..908edd962f0 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -13,6 +14,60 @@ namespace NYdb { namespace NOdbc { +namespace { + +struct TDriverKey { + std::string Endpoint; + std::string Database; + + bool operator==(const TDriverKey& other) const noexcept { + return Endpoint == other.Endpoint && Database == other.Database; + } +}; + +struct TDriverKeyHash { + size_t operator()(const TDriverKey& key) const noexcept { + return std::hash{}(key.Endpoint) ^ (std::hash{}(key.Database) << 1U); + } +}; + +struct TDriverPool { + std::unordered_map, TDriverKeyHash> DriversByKey; + size_t InsertionsSinceCleanup = 0; +}; + +void CleanupExpiredDrivers(TDriverPool& pool) { + for (auto mapIt = pool.DriversByKey.begin(); mapIt != pool.DriversByKey.end();) { + if (mapIt->second.expired()) { + mapIt = pool.DriversByKey.erase(mapIt); + } else { + ++mapIt; + } + } +} + +std::shared_ptr AcquireSharedDriver(const std::string& endpoint, const std::string& database) { + static TDriverPool pool; + TDriverKey key{endpoint, database}; + auto it = pool.DriversByKey.find(key); + if (it != pool.DriversByKey.end()) { + if (std::shared_ptr existing = it->second.lock()) { + return existing; + } + } + auto driver = std::make_shared( + NYdb::TDriverConfig().SetEndpoint(endpoint).SetDatabase(database)); + pool.DriversByKey[std::move(key)] = driver; + ++pool.InsertionsSinceCleanup; + if (pool.InsertionsSinceCleanup >= 32) { + CleanupExpiredDrivers(pool); + pool.InsertionsSinceCleanup = 0; + } + return driver; +} + +} // namespace + SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { std::map params; size_t pos = 0; @@ -171,5 +226,41 @@ TEnvironment* TConnection::GetEnvironment(){ return ParentEnv_; } +void TConnection::RecreateYdbClients() { + QuerySession_.reset(); + Tx_.reset(); + YdbSchemeClient_.reset(); + YdbTableClient_.reset(); + YdbClient_.reset(); + YdbDriver_ = AcquireSharedDriver(Endpoint_, Database_); + YdbClient_ = std::make_unique(*YdbDriver_); + YdbSchemeClient_ = std::make_unique(*YdbDriver_); + YdbTableClient_ = std::make_unique(*YdbDriver_); +} + +void TConnection::RebindToDatabase(const std::string& newDatabase) { + std::string db = newDatabase; + TConnectionAttributes::NormalizeCatalogPath(db); + Database_ = std::move(db); + Attributes_.SetCurrentCatalog(Database_); + RecreateYdbClients(); +} + + +std::string TConnection::WrapQueryForCurrentCatalog(const std::string& sql) const { + std::optional rel = Attributes_.ResolveCatalogRoute(Database_).TablePathPrefix; + if (!rel) { + return sql; + } + std::string escapedPrefix; + escapedPrefix.reserve(rel->size() + 8); + for (const char ch : *rel) { + if (ch == '\\' || ch == '"') { + escapedPrefix.push_back('\\'); + } + escapedPrefix.push_back(ch); + } + return "PRAGMA TablePathPrefix = \"" + escapedPrefix + "\";\n" + sql; +} } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/connection.h b/odbc/src/connection.h index e1c9028fa84..71bb6664454 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -23,7 +23,7 @@ class TStatement; class TConnection : public TErrorManager { private: - std::unique_ptr YdbDriver_; + std::shared_ptr YdbDriver_; std::unique_ptr YdbClient_; std::unique_ptr YdbTableClient_; std::unique_ptr YdbSchemeClient_; From 7ac660f2d2f9d35f661fdd26da5f4477946eb448 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Fri, 17 Apr 2026 13:55:29 +0300 Subject: [PATCH 18/55] conn attributes Combine connection attribute routing and error-localization updates into one focused commit while keeping driver pool changes separate. Made-with: Cursor --- odbc/CMakeLists.txt | 3 +- odbc/src/connection.cpp | 31 +-- odbc/src/connection.h | 10 +- odbc/src/connection_attr.cpp | 308 ++++++++++++++++++++++++++ odbc/src/connection_attr.h | 86 +++++++ odbc/src/connection_attributes.cpp | 141 ------------ odbc/src/connection_attributes.h | 48 ---- odbc/src/statement.cpp | 7 +- odbc/src/utils/attr.cpp | 51 +++++ odbc/src/utils/attr.h | 46 ++++ odbc/src/utils/diag.h | 33 +++ odbc/tests/integration/CMakeLists.txt | 5 + odbc/tests/integration/attr_it.cpp | 244 ++++++++++++++++++++ 13 files changed, 804 insertions(+), 209 deletions(-) create mode 100644 odbc/src/connection_attr.cpp create mode 100644 odbc/src/connection_attr.h delete mode 100644 odbc/src/connection_attributes.cpp delete mode 100644 odbc/src/connection_attributes.h create mode 100644 odbc/src/utils/attr.cpp create mode 100644 odbc/src/utils/attr.h create mode 100644 odbc/src/utils/diag.h create mode 100644 odbc/tests/integration/attr_it.cpp diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 0390b20698c..5071c42f85d 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(ydb-odbc SHARED + src/utils/attr.cpp src/utils/cursor.cpp src/utils/types.cpp src/utils/util.cpp src/utils/convert.cpp src/utils/error_manager.cpp src/odbc_driver.cpp - src/connection_attributes.cpp + src/connection_attr.cpp src/connection.cpp src/statement.cpp src/environment.cpp diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 908edd962f0..a52d5036f04 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -93,13 +93,9 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { throw TOdbcException("08001", 0, "Missing Endpoint or Database in connection string"); } - YdbDriver_ = std::make_unique(NYdb::TDriverConfig() - .SetEndpoint(Endpoint_) - .SetDatabase(Database_)); - - YdbClient_ = std::make_unique(*YdbDriver_); - YdbSchemeClient_ = std::make_unique(*YdbDriver_); - YdbTableClient_ = std::make_unique(*YdbDriver_); + TConnectionAttributes::NormalizeCatalogPath(Database_); + RecreateYdbClients(); + Attributes_.SetCurrentCatalog(Database_); return SQL_SUCCESS; } @@ -121,13 +117,9 @@ SQLRETURN TConnection::Connect(const std::string& serverName, throw TOdbcException("08001", 0, "Missing Endpoint or Database in DSN"); } - YdbDriver_ = std::make_unique(NYdb::TDriverConfig() - .SetEndpoint(Endpoint_) - .SetDatabase(Database_)); - - YdbClient_ = std::make_unique(*YdbDriver_); - YdbSchemeClient_ = std::make_unique(*YdbDriver_); - YdbTableClient_ = std::make_unique(*YdbDriver_); + TConnectionAttributes::NormalizeCatalogPath(Database_); + RecreateYdbClients(); + Attributes_.SetCurrentCatalog(Database_); return SQL_SUCCESS; } @@ -175,6 +167,17 @@ bool TConnection::GetAutocommit() const { } SQLRETURN TConnection::SetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength) { + if (attr == SQL_ATTR_CURRENT_CATALOG) { + std::optional rebindDatabase; + SQLRETURN rc = Attributes_.ApplyCatalogChange(value, stringLength, Database_, rebindDatabase, *this); + if (rc != SQL_SUCCESS) { + return rc; + } + if (rebindDatabase) { + RebindToDatabase(*rebindDatabase); + } + return SQL_SUCCESS; + } return Attributes_.SetConnectAttr(attr, value, stringLength, [this](bool autocommit) { return SetAutocommit(autocommit); }, *this); diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 71bb6664454..284ac36cf65 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -1,7 +1,7 @@ #pragma once #include "environment.h" -#include "connection_attributes.h" +#include "connection_attr.h" #include "utils/error_manager.h" #include @@ -13,8 +13,9 @@ #include #include -#include +#include #include +#include namespace NYdb { namespace NOdbc { @@ -37,6 +38,9 @@ class TConnection : public TErrorManager { TEnvironment* ParentEnv_; TConnectionAttributes Attributes_; + + void RecreateYdbClients(); + void RebindToDatabase(const std::string& newDatabase); public: SQLRETURN Connect(const std::string& serverName, const std::string& userName, @@ -60,6 +64,8 @@ class TConnection : public TErrorManager { SQLRETURN GetConnectAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); NQuery::TTxSettings MakeTxSettings() const; + std::string WrapQueryForCurrentCatalog(const std::string& sql) const; + const std::optional& GetTx(); void SetTx(const NQuery::TTransaction& tx); void Reset(); diff --git a/odbc/src/connection_attr.cpp b/odbc/src/connection_attr.cpp new file mode 100644 index 00000000000..6197a4ad4a9 --- /dev/null +++ b/odbc/src/connection_attr.cpp @@ -0,0 +1,308 @@ + +#include "connection_attr.h" +#include "utils/attr.h" +#include "utils/diag.h" + +#include + +namespace NYdb { +namespace NOdbc { + +namespace { + +namespace Catalog { + +void NormalizePath(std::string& path) { + if (path.empty() || path == "/") { + return; + } + const size_t trailingSlashStart = path.find_last_not_of('/'); + if (trailingSlashStart == std::string::npos) { + path.assign("/"); + return; + } + path.erase(trailingSlashStart + 1); +} + +TConnectionAttributes::TCatalogBinding BuildBinding(const std::string& currentCatalog, const std::string& database) { + TConnectionAttributes::TCatalogBinding binding; + binding.Catalog = currentCatalog; + binding.Database = database; + NormalizePath(binding.Catalog); + NormalizePath(binding.Database); + if (binding.Catalog == binding.Database) { + return binding; + } + + const std::string databasePrefix = binding.Database + "/"; + if (binding.Catalog.size() <= databasePrefix.size() || + binding.Catalog.compare(0, databasePrefix.size(), databasePrefix) != 0) { + return binding; + } + + std::string relativeCatalog = binding.Catalog.substr(databasePrefix.size()); + if (!relativeCatalog.empty()) { + binding.RelativeCatalog = std::move(relativeCatalog); + } + return binding; +} + +} // namespace Catalog + +namespace Tx { + +bool IsKnownTxnIsolation(SQLUINTEGER txnIsolation) { + switch (txnIsolation) { + case SQL_TXN_READ_UNCOMMITTED: + case SQL_TXN_READ_COMMITTED: + case SQL_TXN_REPEATABLE_READ: + case SQL_TXN_SERIALIZABLE: + return true; + default: + return false; + } +} + +std::optional ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation) { + if (accessMode == SQL_MODE_READ_ONLY) { + switch (txnIsolation) { + case SQL_TXN_READ_UNCOMMITTED: + return NQuery::TTxSettings::TS_STALE_RO; + case SQL_TXN_READ_COMMITTED: + return NQuery::TTxSettings::TS_ONLINE_RO; + case SQL_TXN_REPEATABLE_READ: + case SQL_TXN_SERIALIZABLE: + return NQuery::TTxSettings::TS_SNAPSHOT_RO; + default: + return std::nullopt; + } + } + + switch (txnIsolation) { + case SQL_TXN_REPEATABLE_READ: + case SQL_TXN_SERIALIZABLE: + return NQuery::TTxSettings::TS_SERIALIZABLE_RW; + default: + return std::nullopt; + } +} + +} // namespace Tx + +namespace Autocommit { + +SQLRETURN Get(bool autocommitEnabled, SQLPOINTER value) { + auto* out = reinterpret_cast(value); + *out = autocommitEnabled ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF; + return SQL_SUCCESS; +} + +} // namespace Autocommit + +} + +void TConnectionAttributes::NormalizeCatalogPath(std::string& path) { + Catalog::NormalizePath(path); +} + +SQLRETURN TConnectionAttributes::SetAutocommit(bool value) { + Autocommit_ = value; + return SQL_SUCCESS; +} + +bool TConnectionAttributes::GetAutocommit() const { + return Autocommit_; +} + +SQLRETURN TConnectionAttributes::SetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER stringLength, + const std::function& applyAutocommit, + TErrorManager& errors) { + switch (attr) { + case SQL_ATTR_AUTOCOMMIT: + return SetAutocommit(value, applyAutocommit, errors); + case SQL_ATTR_ACCESS_MODE: + return SetAccessMode(value, errors); + case SQL_ATTR_TXN_ISOLATION: + return SetTxnIsolation(value, errors); + case SQL_ATTR_CURRENT_CATALOG: + return SetCurrentCatalog(value, stringLength, errors); + default: + return Diag::AddNotImplemented(errors); + } +} + +SQLRETURN TConnectionAttributes::GetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const { + if (!value) { + return Diag::AddNullPointer(errors); + } + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + switch (attr) { + case SQL_ATTR_AUTOCOMMIT: + return GetAutocommit(value); + case SQL_ATTR_ACCESS_MODE: + return GetAccessMode(value); + case SQL_ATTR_TXN_ISOLATION: + return GetTxnIsolation(value); + case SQL_ATTR_CURRENT_CATALOG: + return GetCurrentCatalog(value, bufferLength, stringLengthPtr, errors); + default: + return Diag::AddNotImplemented(errors); + } +} + +SQLRETURN TConnectionAttributes::SetAutocommit( + SQLPOINTER value, + const std::function& applyAutocommit, + TErrorManager& errors) { + const auto token = ReadIntegerAttrIfIn( + value, + {static_cast(SQL_AUTOCOMMIT_ON), static_cast(SQL_AUTOCOMMIT_OFF)}); + if (!token) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_AUTOCOMMIT"); + } + if (*token == static_cast(SQL_AUTOCOMMIT_ON)) { + return applyAutocommit(true); + } + return applyAutocommit(false); +} + +SQLRETURN TConnectionAttributes::SetAccessMode(SQLPOINTER value, TErrorManager& errors) { + const auto mode = ReadIntegerAttrIfIn(value, {SQL_MODE_READ_WRITE, SQL_MODE_READ_ONLY}); + if (!mode) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_ACCESS_MODE"); + } + AccessMode_ = *mode; + auto txMode = Tx::ResolveTxMode(AccessMode_, TxnIsolation_); + if (!txMode) { + return errors.AddError( + "HYC00", + 0, + AccessMode_ == SQL_MODE_READ_WRITE + ? "Transaction isolation is not supported for read-write mode" + : "Transaction isolation is not supported for read-only mode"); + } + TxMode_ = *txMode; + return SQL_SUCCESS; +} + +SQLRETURN TConnectionAttributes::SetTxnIsolation(SQLPOINTER value, TErrorManager& errors) { + const SQLUINTEGER isolation = ReadIntegerAttr(value); + if (!Tx::IsKnownTxnIsolation(isolation)) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_TXN_ISOLATION"); + } + auto txMode = Tx::ResolveTxMode(AccessMode_, isolation); + if (!txMode) { + return errors.AddError("HYC00", 0, "SQL_ATTR_TXN_ISOLATION value is not supported"); + } + TxnIsolation_ = isolation; + TxMode_ = *txMode; + return SQL_SUCCESS; +} + +SQLRETURN TConnectionAttributes::SetCurrentCatalog(SQLPOINTER value, SQLINTEGER stringLength, TErrorManager& errors) { + if (!value) { + return Diag::AddNullPointer(errors); + } + CurrentCatalog_ = ReadAttributeString(value, stringLength); + Catalog::NormalizePath(CurrentCatalog_); + if (CurrentCatalog_.empty()) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_CURRENT_CATALOG"); + } + return SQL_SUCCESS; +} + +SQLRETURN TConnectionAttributes::GetAutocommit(SQLPOINTER value) const { + return Autocommit::Get(Autocommit_, value); +} + +SQLRETURN TConnectionAttributes::GetAccessMode(SQLPOINTER value) const { + auto* out = reinterpret_cast(value); + *out = AccessMode_; + return SQL_SUCCESS; +} + +SQLRETURN TConnectionAttributes::GetTxnIsolation(SQLPOINTER value) const { + auto* out = reinterpret_cast(value); + *out = TxnIsolation_; + return SQL_SUCCESS; +} + +SQLRETURN TConnectionAttributes::GetCurrentCatalog( + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const { + return WriteAttributeString(CurrentCatalog_, value, bufferLength, stringLengthPtr, errors); +} + +NQuery::TTxSettings TConnectionAttributes::MakeTxSettings() const { + switch (TxMode_) { + case NQuery::TTxSettings::TS_ONLINE_RO: + return NQuery::TTxSettings::OnlineRO(); + case NQuery::TTxSettings::TS_STALE_RO: + return NQuery::TTxSettings::StaleRO(); + case NQuery::TTxSettings::TS_SNAPSHOT_RO: + return NQuery::TTxSettings::SnapshotRO(); + case NQuery::TTxSettings::TS_SNAPSHOT_RW: + return NQuery::TTxSettings::SnapshotRW(); + case NQuery::TTxSettings::TS_SERIALIZABLE_RW: + default: + return NQuery::TTxSettings::SerializableRW(); + } +} + +void TConnectionAttributes::SetCurrentCatalog(const std::string& value) { + CurrentCatalog_ = value; + Catalog::NormalizePath(CurrentCatalog_); +} + +const std::string& TConnectionAttributes::GetCurrentCatalog() const { + return CurrentCatalog_; +} + +TConnectionAttributes::TCatalogBinding TConnectionAttributes::BuildCatalogBinding(const std::string& database) const { + return Catalog::BuildBinding(CurrentCatalog_, database); +} + +TConnectionAttributes::TCatalogRoute TConnectionAttributes::ResolveCatalogRoute(const std::string& currentDatabase) const { + const TCatalogBinding binding = BuildCatalogBinding(currentDatabase); + if (binding.Catalog == binding.Database) { + return {binding.Database, std::nullopt}; + } + if (binding.RelativeCatalog) { + return {binding.Database, binding.Catalog}; + } + return {binding.Catalog, std::nullopt}; +} + +SQLRETURN TConnectionAttributes::ApplyCatalogChange( + SQLPOINTER value, + SQLINTEGER stringLength, + const std::string& currentDatabase, + std::optional& rebindDatabase, + TErrorManager& errors) { + SQLRETURN rc = SetCurrentCatalog(value, stringLength, errors); + if (rc != SQL_SUCCESS) { + return rc; + } + const TCatalogRoute route = ResolveCatalogRoute(currentDatabase); + if (route.EffectiveDatabase != currentDatabase) { + rebindDatabase = route.EffectiveDatabase; + } else { + rebindDatabase.reset(); + } + return SQL_SUCCESS; +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/connection_attr.h b/odbc/src/connection_attr.h new file mode 100644 index 00000000000..607f7bf929f --- /dev/null +++ b/odbc/src/connection_attr.h @@ -0,0 +1,86 @@ +#pragma once + +#include "utils/error_manager.h" + +#include + +#include +#include +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +class TConnectionAttributes { +public: + struct TCatalogBinding { + std::string Catalog; + std::string Database; + std::optional RelativeCatalog; + }; + + struct TCatalogRoute { + std::string EffectiveDatabase; + std::optional TablePathPrefix; + }; + + SQLRETURN SetAutocommit(bool value); + bool GetAutocommit() const; + + SQLRETURN SetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER stringLength, + const std::function& applyAutocommit, + TErrorManager& errors); + + SQLRETURN GetConnectAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const; + + NQuery::TTxSettings MakeTxSettings() const; + void SetCurrentCatalog(const std::string& value); + const std::string& GetCurrentCatalog() const; + TCatalogBinding BuildCatalogBinding(const std::string& database) const; + TCatalogRoute ResolveCatalogRoute(const std::string& currentDatabase) const; + SQLRETURN ApplyCatalogChange( + SQLPOINTER value, + SQLINTEGER stringLength, + const std::string& currentDatabase, + std::optional& rebindDatabase, + TErrorManager& errors); + static void NormalizeCatalogPath(std::string& path); + +private: + SQLRETURN SetAutocommit( + SQLPOINTER value, + const std::function& applyAutocommit, + TErrorManager& errors); + SQLRETURN SetAccessMode(SQLPOINTER value, TErrorManager& errors); + SQLRETURN SetTxnIsolation(SQLPOINTER value, TErrorManager& errors); + SQLRETURN SetCurrentCatalog(SQLPOINTER value, SQLINTEGER stringLength, TErrorManager& errors); + + SQLRETURN GetAutocommit(SQLPOINTER value) const; + SQLRETURN GetAccessMode(SQLPOINTER value) const; + SQLRETURN GetTxnIsolation(SQLPOINTER value) const; + SQLRETURN GetCurrentCatalog( + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const; + + bool Autocommit_ = true; + std::string CurrentCatalog_; + SQLUINTEGER AccessMode_ = SQL_MODE_READ_WRITE; + SQLUINTEGER TxnIsolation_ = SQL_TXN_SERIALIZABLE; + NQuery::TTxSettings::ETransactionMode TxMode_ = NQuery::TTxSettings::TS_SERIALIZABLE_RW; +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/connection_attributes.cpp b/odbc/src/connection_attributes.cpp deleted file mode 100644 index 61bfa7f8caa..00000000000 --- a/odbc/src/connection_attributes.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "connection_attributes.h" - -#include - -namespace NYdb { -namespace NOdbc { - -std::optional TConnectionAttributes::ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation) { - if (accessMode == SQL_MODE_READ_ONLY) { - switch (txnIsolation) { - case SQL_TXN_READ_UNCOMMITTED: - return NQuery::TTxSettings::TS_STALE_RO; - case SQL_TXN_READ_COMMITTED: - return NQuery::TTxSettings::TS_ONLINE_RO; - case SQL_TXN_REPEATABLE_READ: - case SQL_TXN_SERIALIZABLE: - return NQuery::TTxSettings::TS_SNAPSHOT_RO; - default: - return std::nullopt; - } - } - - switch (txnIsolation) { - case SQL_TXN_REPEATABLE_READ: - case SQL_TXN_SERIALIZABLE: - return NQuery::TTxSettings::TS_SERIALIZABLE_RW; - default: - return std::nullopt; - } -} - -SQLRETURN TConnectionAttributes::SetAutocommit(bool value) { - Autocommit_ = value; - return SQL_SUCCESS; -} - -bool TConnectionAttributes::GetAutocommit() const { - return Autocommit_; -} - -SQLRETURN TConnectionAttributes::SetConnectAttr( - SQLINTEGER attr, - SQLPOINTER value, - SQLINTEGER /*stringLength*/, - const std::function& applyAutocommit, - TErrorManager& errors) { - switch (attr) { - case SQL_ATTR_AUTOCOMMIT: { - const intptr_t val = reinterpret_cast(value); - if (val == static_cast(SQL_AUTOCOMMIT_ON)) { - return applyAutocommit(true); - } - if (val == static_cast(SQL_AUTOCOMMIT_OFF)) { - return applyAutocommit(false); - } - return errors.AddError("HY024", 0, "Invalid SQL_ATTR_AUTOCOMMIT value"); - } - case SQL_ATTR_ACCESS_MODE: { - const intptr_t val = reinterpret_cast(value); - if (val == static_cast(SQL_MODE_READ_WRITE)) { - AccessMode_ = SQL_MODE_READ_WRITE; - auto txMode = ResolveTxMode(AccessMode_, TxnIsolation_); - if (!txMode) { - return errors.AddError("HYC00", 0, "Transaction isolation is not supported for read-write mode"); - } - TxMode_ = *txMode; - return SQL_SUCCESS; - } - if (val == static_cast(SQL_MODE_READ_ONLY)) { - AccessMode_ = SQL_MODE_READ_ONLY; - auto txMode = ResolveTxMode(AccessMode_, TxnIsolation_); - if (!txMode) { - return errors.AddError("HYC00", 0, "Transaction isolation is not supported for read-only mode"); - } - TxMode_ = *txMode; - return SQL_SUCCESS; - } - return errors.AddError("HY024", 0, "Invalid SQL_ATTR_ACCESS_MODE value"); - } - case SQL_ATTR_TXN_ISOLATION: { - const intptr_t val = reinterpret_cast(value); - const SQLUINTEGER isolation = static_cast(val); - auto txMode = ResolveTxMode(AccessMode_, isolation); - if (!txMode) { - return errors.AddError("HYC00", 0, "SQL_ATTR_TXN_ISOLATION value is not supported"); - } - TxnIsolation_ = isolation; - TxMode_ = *txMode; - return SQL_SUCCESS; - } - default: - return errors.AddError("HYC00", 0, "Optional feature not implemented"); - } -} - -SQLRETURN TConnectionAttributes::GetConnectAttr( - SQLINTEGER attr, - SQLPOINTER value, - SQLINTEGER /*bufferLength*/, - SQLINTEGER* stringLengthPtr, - TErrorManager& errors) const { - if (!value) { - return errors.AddError("HY009", 0, "Invalid use of null pointer"); - } - if (stringLengthPtr) { - *stringLengthPtr = 0; - } - auto* out = reinterpret_cast(value); - switch (attr) { - case SQL_ATTR_AUTOCOMMIT: - *out = GetAutocommit() ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF; - return SQL_SUCCESS; - case SQL_ATTR_ACCESS_MODE: - *out = AccessMode_; - return SQL_SUCCESS; - case SQL_ATTR_TXN_ISOLATION: - *out = TxnIsolation_; - return SQL_SUCCESS; - default: - return errors.AddError("HYC00", 0, "Optional feature not implemented"); - } -} - -NQuery::TTxSettings TConnectionAttributes::MakeTxSettings() const { - switch (TxMode_) { - case NQuery::TTxSettings::TS_ONLINE_RO: - return NQuery::TTxSettings::OnlineRO(); - case NQuery::TTxSettings::TS_STALE_RO: - return NQuery::TTxSettings::StaleRO(); - case NQuery::TTxSettings::TS_SNAPSHOT_RO: - return NQuery::TTxSettings::SnapshotRO(); - case NQuery::TTxSettings::TS_SNAPSHOT_RW: - return NQuery::TTxSettings::SnapshotRW(); - case NQuery::TTxSettings::TS_SERIALIZABLE_RW: - default: - return NQuery::TTxSettings::SerializableRW(); - } -} - -} // namespace NOdbc -} // namespace NYdb diff --git a/odbc/src/connection_attributes.h b/odbc/src/connection_attributes.h deleted file mode 100644 index 7b2f0fc7221..00000000000 --- a/odbc/src/connection_attributes.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "utils/error_manager.h" - -#include - -#include -#include - -#include -#include - -namespace NYdb { -namespace NOdbc { - -class TConnectionAttributes { -public: - SQLRETURN SetAutocommit(bool value); - bool GetAutocommit() const; - - SQLRETURN SetConnectAttr( - SQLINTEGER attr, - SQLPOINTER value, - SQLINTEGER stringLength, - const std::function& applyAutocommit, - TErrorManager& errors); - - SQLRETURN GetConnectAttr( - SQLINTEGER attr, - SQLPOINTER value, - SQLINTEGER bufferLength, - SQLINTEGER* stringLengthPtr, - TErrorManager& errors) const; - - NQuery::TTxSettings MakeTxSettings() const; - -private: - static std::optional ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation); - -private: - bool Autocommit_ = true; - SQLUINTEGER AccessMode_ = SQL_MODE_READ_WRITE; - SQLUINTEGER TxnIsolation_ = SQL_TXN_SERIALIZABLE; - NQuery::TTxSettings::ETransactionMode TxMode_ = NQuery::TTxSettings::TS_SERIALIZABLE_RW; -}; - -} // namespace NOdbc -} // namespace NYdb diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 6f714d0b0bf..dd657f304ba 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -54,16 +54,17 @@ SQLRETURN TStatement::Execute() { } NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ + const std::string queryText = Conn_->WrapQueryForCurrentCatalog(PreparedQuery_); if (Conn_->GetAutocommit()) { const auto txSettings = Conn_->MakeTxSettings(); if (txSettings.GetMode() == NQuery::TTxSettings::TS_SERIALIZABLE_RW) { return session.StreamExecuteQuery( - PreparedQuery_, + queryText, NQuery::TTxControl::NoTx(), params).ExtractValueSync(); } return session.StreamExecuteQuery( - PreparedQuery_, + queryText, NQuery::TTxControl::BeginTx(txSettings).CommitTx(), params).ExtractValueSync(); } @@ -73,7 +74,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession Conn_->SetTx(beginTxResult.GetTransaction()); } return session.StreamExecuteQuery( - PreparedQuery_, + queryText, NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(false), params).ExtractValueSync(); } diff --git a/odbc/src/utils/attr.cpp b/odbc/src/utils/attr.cpp new file mode 100644 index 00000000000..1fb2a83324a --- /dev/null +++ b/odbc/src/utils/attr.cpp @@ -0,0 +1,51 @@ +#include "attr.h" +#include "diag.h" + +#include +#include + +namespace NYdb::NOdbc { + +std::string ReadAttributeString(SQLPOINTER value, SQLINTEGER stringLength) { + const char* const str = static_cast(value); + if (stringLength == SQL_NTS) { + return std::string(str); + } + if (stringLength < 0) { + return {}; + } + return std::string(str, static_cast(stringLength)); +} + +SQLRETURN WriteAttributeString( + const std::string& source, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) { + const SQLINTEGER length = static_cast(source.size()); + if (stringLengthPtr != nullptr) { + *stringLengthPtr = length; + } + if (value == nullptr) { + return SQL_SUCCESS; + } + if (bufferLength <= 0) { + return Diag::AddInvalidBufferLength(errors); + } + + auto* dest = static_cast(value); + const size_t maxData = static_cast(bufferLength - 1); + const size_t nCopy = std::min(source.size(), maxData); + if (nCopy > 0) { + std::memcpy(dest, source.data(), nCopy); + } + dest[nCopy] = 0; + + if (length >= bufferLength) { + return Diag::AddRightTruncated(errors); + } + return SQL_SUCCESS; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/attr.h b/odbc/src/utils/attr.h new file mode 100644 index 00000000000..34abeed42da --- /dev/null +++ b/odbc/src/utils/attr.h @@ -0,0 +1,46 @@ +#pragma once + +#include "error_manager.h" + +#include +#include +#include + +#include +#include + +namespace NYdb::NOdbc { + +std::string ReadAttributeString(SQLPOINTER value, SQLINTEGER stringLength); + +SQLRETURN WriteAttributeString( + const std::string& source, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors); + +template +T ReadIntegerAttr(SQLPOINTER value) noexcept; + +template +std::optional ReadIntegerAttrIfIn(SQLPOINTER value, std::initializer_list allowed) noexcept; + +template +T ReadIntegerAttr(SQLPOINTER value) noexcept { + return static_cast(reinterpret_cast(value)); +} + +template +std::optional ReadIntegerAttrIfIn(SQLPOINTER value, std::initializer_list allowed) noexcept { + const T token = ReadIntegerAttr(value); + for (const T allowedValue : allowed) { + if (token == allowedValue) { + return token; + } + } + return std::nullopt; +} + + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/diag.h b/odbc/src/utils/diag.h new file mode 100644 index 00000000000..5e2db740a07 --- /dev/null +++ b/odbc/src/utils/diag.h @@ -0,0 +1,33 @@ +#pragma once + +#include "error_manager.h" + +#include +#include + +namespace NYdb::NOdbc { +namespace Diag { + + inline SQLRETURN AddNullPointer(TErrorManager& errors) { + return errors.AddError("HY009", 0, "Invalid use of null pointer"); + } + + inline SQLRETURN AddNotImplemented(TErrorManager& errors) { + return errors.AddError("HYC00", 0, "Optional feature not implemented"); + } + + inline SQLRETURN AddInvalidAttrValue(TErrorManager& errors, std::string_view attrName) { + return errors.AddError("HY024", 0, "Invalid " + std::string(attrName) + " value"); + } + + inline SQLRETURN AddInvalidBufferLength(TErrorManager& errors) { + return errors.AddError("HY090", 0, "Invalid string or buffer length"); + } + + inline SQLRETURN AddRightTruncated(TErrorManager& errors) { + return errors.AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + +} + +} // namespace NYdb::NOdbc::Diag diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 0360679931c..39128437ced 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -7,3 +7,8 @@ add_odbc_test(NAME odbc-env_it SOURCES env_it.cpp ) + +add_odbc_test(NAME odbc-attr_it + SOURCES + attr_it.cpp +) diff --git a/odbc/tests/integration/attr_it.cpp b/odbc/tests/integration/attr_it.cpp new file mode 100644 index 00000000000..514278f8e67 --- /dev/null +++ b/odbc/tests/integration/attr_it.cpp @@ -0,0 +1,244 @@ +#include "test_utils.h" + +#include +#include + +namespace { + +bool SqlStatePrefix(const std::string& diag, const char* state5) { + return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; +} + +void AllocEnv(SQLHENV* env) { + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); +} + +void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { + AllocEnv(env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, *env, dbc), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); +} + +} // namespace + +TEST(OdbcAttrEnv, OdbcVersionAttr) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcAttrEnv, OutputNtsAttr) { + SQLHENV env; + AllocEnv(&env); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_TRUE, 0), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_FALSE, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcAttrConn, AutocommitAttr) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_autocommit"; + SQLCHAR createQuery[] = + "CREATE TABLE test_attr_autocommit (id Int32, value Int32, PRIMARY KEY (id))"; + SQLCHAR upsertRollbackQuery[] = "UPSERT INTO test_attr_autocommit (id, value) VALUES (1, 100)"; + SQLCHAR upsertCommitQuery[] = "UPSERT INTO test_attr_autocommit (id, value) VALUES (1, 200)"; + SQLCHAR selectQuery[] = "SELECT value FROM test_attr_autocommit WHERE id = 1"; + + CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, upsertRollbackQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_ROLLBACK), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, upsertCommitQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 200); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0), dbc, SQL_HANDLE_DBC); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcAttrConn, AccessModeAttr) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + + constexpr SQLUINTEGER readWriteMode = SQL_MODE_READ_WRITE; + constexpr SQLUINTEGER readOnlyMode = SQL_MODE_READ_ONLY; + SQLUINTEGER currentMode = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); + ASSERT_EQ(readWriteMode, currentMode); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readOnlyMode, 0), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); + ASSERT_EQ(readOnlyMode, currentMode); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readWriteMode, 0), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); + ASSERT_EQ(readWriteMode, currentMode); + + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)9999, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); + + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_read_only"; + SQLCHAR createQuery[] = "CREATE TABLE test_attr_read_only (id Int32, PRIMARY KEY (id))"; + SQLCHAR selectOneQuery[] = "SELECT 1 AS value"; + SQLCHAR upsertQuery[] = "UPSERT INTO test_attr_read_only (id) VALUES (1)"; + CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readOnlyMode, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLExecDirect(stmt, upsertQuery, SQL_NTS), SQL_ERROR); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcAttrConn, TxnIsolationAttr) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLCHAR selectOneQuery[] = "SELECT 1 AS value"; + + SQLUINTEGER currentIsolation = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, ¤tIsolation, sizeof(currentIsolation), nullptr), SQL_SUCCESS); + ASSERT_EQ(static_cast(SQL_TXN_SERIALIZABLE), currentIsolation); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_REPEATABLE_READ, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_COMMITTED, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HYC00")); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, ¤tIsolation, sizeof(currentIsolation), nullptr), SQL_SUCCESS); + ASSERT_EQ(static_cast(SQL_TXN_REPEATABLE_READ), currentIsolation); + + // In read-only mode all four standard levels are accepted and remain executable. + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_ONLY, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_UNCOMMITTED, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_COMMITTED, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_REPEATABLE_READ, 0), dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_SERIALIZABLE, 0), dbc, SQL_HANDLE_DBC); + + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)9999, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcAttrConn, CurrentCatalogAttr) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + constexpr const char* dbRoot = "/local"; + const std::string catA = std::string(dbRoot) + "/odbc_cat_a"; + const std::string catB = std::string(dbRoot) + "/odbc_cat_b"; + SQLCHAR dropAQuery[] = "DROP TABLE IF EXISTS `odbc_cat_a/probe`"; + SQLCHAR dropBQuery[] = "DROP TABLE IF EXISTS `odbc_cat_b/probe`"; + SQLCHAR createAQuery[] = + "CREATE TABLE `odbc_cat_a/probe` (id Int32, value Int32, PRIMARY KEY (id))"; + SQLCHAR createBQuery[] = + "CREATE TABLE `odbc_cat_b/probe` (id Int32, value Int32, PRIMARY KEY (id))"; + SQLCHAR upsertAQuery[] = "UPSERT INTO `odbc_cat_a/probe` (id, value) VALUES (1, 100)"; + SQLCHAR upsertBQuery[] = "UPSERT INTO `odbc_cat_b/probe` (id, value) VALUES (1, 200)"; + SQLCHAR selectAQuery[] = "SELECT value FROM `odbc_cat_a/probe` WHERE id = 1"; + SQLCHAR selectQuery[] = "SELECT value FROM probe WHERE id = 1"; + + char catalog[256] = {0}; + SQLINTEGER textLen = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); + ASSERT_STREQ(catalog, dbRoot); + + + CHECK_ODBC_OK(SQLExecDirect(stmt, dropAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, dropBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, createAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, createBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, upsertAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, upsertBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catA.c_str(), SQL_NTS), dbc, + SQL_HANDLE_DBC); + std::memset(catalog, 0, sizeof(catalog)); + textLen = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); + ASSERT_STREQ(catalog, catA.c_str()); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 100); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + valueInt = 0; + valueInd = 0; + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catB.c_str(), SQL_NTS), dbc, + SQL_HANDLE_DBC); + std::memset(catalog, 0, sizeof(catalog)); + textLen = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); + ASSERT_STREQ(catalog, catB.c_str()); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 200); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + const std::string catWithSlashes = catB + "///"; + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catWithSlashes.c_str(), SQL_NTS), dbc, + SQL_HANDLE_DBC); + std::memset(catalog, 0, sizeof(catalog)); + textLen = 0; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); + ASSERT_STREQ(catalog, catB.c_str()); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + From ca7fbdc9ee6b404fa05f01da9622ca3f9f555a76 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sat, 18 Apr 2026 17:53:17 +0000 Subject: [PATCH 19/55] stmt attr --- odbc/CMakeLists.txt | 2 + odbc/src/odbc_driver.cpp | 17 + odbc/src/statement.cpp | 67 ++- odbc/src/statement.h | 5 + odbc/src/statement_attr.cpp | 101 ++++ odbc/src/statement_attr.h | 39 ++ odbc/src/utils/convert.cpp | 74 +++ odbc/src/utils/cursor.cpp | 14 +- odbc/src/utils/escape.cpp | 444 ++++++++++++++++++ odbc/src/utils/escape.h | 9 + odbc/src/utils/sql_like.h | 49 ++ odbc/tests/integration/CMakeLists.txt | 5 + odbc/tests/integration/attr_it.cpp | 21 - odbc/tests/integration/env_it.cpp | 9 - odbc/tests/integration/stmt_attr_it.cpp | 334 +++++++++++++ odbc/tests/integration/test_utils.h | 24 +- odbc/tests/unit/CMakeLists.txt | 23 + odbc/tests/unit/escape_ut.cpp | 71 +++ odbc/tests/unit/sql_like_ut.cpp | 28 ++ .../unit/library/operation_id/CMakeLists.txt | 1 + 20 files changed, 1294 insertions(+), 43 deletions(-) create mode 100644 odbc/src/statement_attr.cpp create mode 100644 odbc/src/statement_attr.h create mode 100644 odbc/src/utils/escape.cpp create mode 100644 odbc/src/utils/escape.h create mode 100644 odbc/src/utils/sql_like.h create mode 100644 odbc/tests/integration/stmt_attr_it.cpp create mode 100644 odbc/tests/unit/escape_ut.cpp create mode 100644 odbc/tests/unit/sql_like_ut.cpp diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 5071c42f85d..06386fd31dd 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(ydb-odbc SHARED src/utils/attr.cpp + src/utils/escape.cpp src/utils/cursor.cpp src/utils/types.cpp src/utils/util.cpp @@ -8,6 +9,7 @@ add_library(ydb-odbc SHARED src/odbc_driver.cpp src/connection_attr.cpp src/connection.cpp + src/statement_attr.cpp src/statement.cpp src/environment.cpp ) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 6b516c63bb8..cba323453af 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -386,4 +386,21 @@ SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, SQLSMALLINT* colCou }); } +SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->SetStmtAttr(attribute, value, stringLength); + }); +} + +SQLRETURN SQL_API SQLGetStmtAttr( + SQLHSTMT statementHandle, + SQLINTEGER attribute, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->GetStmtAttr(attribute, value, bufferLength, stringLengthPtr); + }); +} + } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index dd657f304ba..f4b04ec0be2 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -3,10 +3,14 @@ #include "utils/convert.h" #include "utils/types.h" #include "utils/error_manager.h" +#include "utils/escape.h" +#include "utils/sql_like.h" #include #include +#include + namespace NYdb { namespace NOdbc { @@ -15,6 +19,7 @@ TStatement::TStatement(TConnection* conn) SQLRETURN TStatement::Prepare(const std::string& statementText) { StreamFetchError_ = false; + RowsFetched_ = 0; Cursor_.reset(); PreparedQuery_ = statementText; IsPrepared_ = true; @@ -26,6 +31,7 @@ SQLRETURN TStatement::Execute() { throw TOdbcException("HY007", 0, "No prepared statement"); } StreamFetchError_ = false; + RowsFetched_ = 0; Cursor_.reset(); auto* client = Conn_->GetClient(); if (!client) { @@ -54,19 +60,27 @@ SQLRETURN TStatement::Execute() { } NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ - const std::string queryText = Conn_->WrapQueryForCurrentCatalog(PreparedQuery_); + const std::string sqlText = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON + ? PreparedQuery_ + : RewriteOdbcEscapes(PreparedQuery_); + const std::string queryText = Conn_->WrapQueryForCurrentCatalog(sqlText); + NQuery::TExecuteQuerySettings execSettings; + const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); + execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); if (Conn_->GetAutocommit()) { const auto txSettings = Conn_->MakeTxSettings(); if (txSettings.GetMode() == NQuery::TTxSettings::TS_SERIALIZABLE_RW) { return session.StreamExecuteQuery( queryText, NQuery::TTxControl::NoTx(), - params).ExtractValueSync(); + params, + execSettings).ExtractValueSync(); } return session.StreamExecuteQuery( queryText, NQuery::TTxControl::BeginTx(txSettings).CommitTx(), - params).ExtractValueSync(); + params, + execSettings).ExtractValueSync(); } if (!Conn_->GetTx()) { auto beginTxResult = session.BeginTransaction(Conn_->MakeTxSettings()).ExtractValueSync(); @@ -76,7 +90,8 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession return session.StreamExecuteQuery( queryText, NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(false), - params).ExtractValueSync(); + params, + execSettings).ExtractValueSync(); } std::optional TStatement::PrefetchFirstResultPart(NQuery::TExecuteQueryIterator& iterator){ @@ -102,10 +117,15 @@ SQLRETURN TStatement::Fetch() { Cursor_.reset(); return SQL_NO_DATA; } + const SQLULEN maxRows = Attributes_.GetMaxRows(); + if (maxRows > 0 && RowsFetched_ >= maxRows) { + return SQL_NO_DATA; + } StreamFetchError_ = false; if (!Cursor_->Fetch()) { return StreamFetchError_ ? SQL_ERROR : SQL_NO_DATA; } + ++RowsFetched_; return SQL_SUCCESS; } @@ -187,6 +207,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& tableName, const std::string& columnName) { ClearErrors(); + RowsFetched_ = 0; Cursor_.reset(); std::vector columns = { @@ -224,18 +245,24 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, continue; } - auto status = Conn_->GetTableClient()->RetryOperationSync([path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { + auto status = Conn_->GetTableClient()->RetryOperationSync([this, path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { auto result = session.DescribeTable(path).ExtractValueSync(); NStatusHelpers::ThrowOnError(result); auto columns = result.GetTableDescription().GetTableColumns(); - auto columnIt = std::find_if(columns.begin(), columns.end(), [&columnName](const NTable::TTableColumn& column) { - return column.Name == columnName; + auto columnIt = std::find_if(columns.begin(), columns.end(), [&](const NTable::TTableColumn& column) { + if (Attributes_.GetMetadataId() == SQL_TRUE) { + return column.Name == columnName; + } + if (columnName.empty()) { + return column.Name.empty(); + } + return SqlLikeMatch(column.Name, columnName); }); if (columnIt == columns.end()) { - return TStatus(EStatus::NOT_FOUND, { NYdb::NIssue::TIssue("Column not found") }); + throw TOdbcException("42S22", 0, "Column not found", SQL_ERROR); } auto column = *columnIt; @@ -277,6 +304,7 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, const std::string& tableName, const std::string& tableType) { ClearErrors(); + RowsFetched_ = 0; Cursor_.reset(); std::vector columns = { @@ -340,7 +368,13 @@ SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pat } bool TStatement::IsPatternMatch(const std::string& path, const std::string& pattern) { - return path.starts_with(pattern); + if (pattern.empty()) { + return true; + } + if (Attributes_.GetMetadataId() == SQL_TRUE) { + return path == pattern; + } + return SqlLikeMatch(path, pattern); } std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { @@ -375,9 +409,15 @@ std::optional TStatement::GetTableType(NScheme::ESchemeEntryType ty return "COORDINATION_NODE"; case NScheme::ESchemeEntryType::Unknown: return "UNKNOWN"; + case NScheme::ESchemeEntryType::SysView: + return "SYSTEM VIEW"; + case NScheme::ESchemeEntryType::Transfer: + return "TRANSFER"; case NScheme::ESchemeEntryType::Directory: case NScheme::ESchemeEntryType::SubDomain: return std::nullopt; + default: + return std::nullopt; } } @@ -387,6 +427,7 @@ SQLRETURN TStatement::Close(bool force) { } Cursor_.reset(); + RowsFetched_ = 0; PreparedQuery_.clear(); IsPrepared_ = false; ClearErrors(); @@ -422,5 +463,13 @@ SQLRETURN TStatement::NumResultCols(SQLSMALLINT* colCount) { return SQL_SUCCESS; } +SQLRETURN TStatement::SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength) { + return Attributes_.SetStmtAttr(attr, value, stringLength, *this); +} + +SQLRETURN TStatement::GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + return Attributes_.GetStmtAttr(attr, value, bufferLength, stringLengthPtr, *this); +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/statement.h b/odbc/src/statement.h index f17780957bb..702fe56c71e 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -1,6 +1,7 @@ #pragma once #include "connection.h" +#include "statement_attr.h" #include "utils/error_manager.h" #include "utils/bindings.h" #include "utils/cursor.h" @@ -51,6 +52,8 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN RowCount(SQLLEN* rowCount); SQLRETURN NumResultCols(SQLSMALLINT* colCount); + SQLRETURN SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength); + SQLRETURN GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); TConnection* GetConnection() { return Conn_; @@ -65,6 +68,8 @@ class TStatement : public TErrorManager, public IBindingFiller { std::vector BoundColumns_; std::vector BoundParams_; bool StreamFetchError_ = false; + SQLULEN RowsFetched_ = 0; + TStatementAttributes Attributes_; NYdb::TParams BuildParams(); diff --git a/odbc/src/statement_attr.cpp b/odbc/src/statement_attr.cpp new file mode 100644 index 00000000000..f0baad0016a --- /dev/null +++ b/odbc/src/statement_attr.cpp @@ -0,0 +1,101 @@ +#include "statement_attr.h" + +#include "utils/attr.h" +#include "utils/diag.h" + +#include + +namespace NYdb { +namespace NOdbc { + +SQLRETURN TStatementAttributes::SetStmtAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER /*stringLength*/, + TErrorManager& errors) { + switch (attr) { + case SQL_ATTR_QUERY_TIMEOUT: { + const SQLINTEGER timeout = ReadIntegerAttr(value); + if (timeout < 0) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_QUERY_TIMEOUT"); + } + QueryTimeoutSec_ = static_cast(timeout); + return SQL_SUCCESS; + } + case SQL_ATTR_MAX_ROWS: { + const SQLLEN maxRows = ReadIntegerAttr(value); + if (maxRows < 0) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_MAX_ROWS"); + } + MaxRows_ = static_cast(maxRows); + return SQL_SUCCESS; + } + case SQL_ATTR_NOSCAN: { + const auto mode = ReadIntegerAttrIfIn(value, {SQL_NOSCAN_OFF, SQL_NOSCAN_ON}); + if (!mode) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_NOSCAN"); + } + NoScan_ = *mode; + return SQL_SUCCESS; + } + case SQL_ATTR_METADATA_ID: { + const auto mode = ReadIntegerAttrIfIn(value, {SQL_FALSE, SQL_TRUE}); + if (!mode) { + return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_METADATA_ID"); + } + MetadataId_ = *mode; + return SQL_SUCCESS; + } + default: + return Diag::AddNotImplemented(errors); + } +} + +SQLRETURN TStatementAttributes::GetStmtAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER /*bufferLength*/, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const { + if (!value) { + return Diag::AddNullPointer(errors); + } + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + switch (attr) { + case SQL_ATTR_QUERY_TIMEOUT: + *reinterpret_cast(value) = QueryTimeoutSec_; + return SQL_SUCCESS; + case SQL_ATTR_MAX_ROWS: + *reinterpret_cast(value) = MaxRows_; + return SQL_SUCCESS; + case SQL_ATTR_NOSCAN: + *reinterpret_cast(value) = NoScan_; + return SQL_SUCCESS; + case SQL_ATTR_METADATA_ID: + *reinterpret_cast(value) = MetadataId_; + return SQL_SUCCESS; + default: + return Diag::AddNotImplemented(errors); + } +} + +SQLUINTEGER TStatementAttributes::GetQueryTimeoutSec() const noexcept{ + return QueryTimeoutSec_; +} + +SQLULEN TStatementAttributes::GetMaxRows() const noexcept { + return MaxRows_; +} + +SQLULEN TStatementAttributes::GetNoScanMode() const noexcept { + return NoScan_; +} + +SQLULEN TStatementAttributes::GetMetadataId() const noexcept { + return MetadataId_; +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/statement_attr.h b/odbc/src/statement_attr.h new file mode 100644 index 00000000000..b0d6e9bd97f --- /dev/null +++ b/odbc/src/statement_attr.h @@ -0,0 +1,39 @@ +#pragma once + +#include "utils/error_manager.h" + +#include +#include + +namespace NYdb { +namespace NOdbc { + +class TStatementAttributes { +public: + SQLRETURN SetStmtAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER stringLength, + TErrorManager& errors); + + SQLRETURN GetStmtAttr( + SQLINTEGER attr, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr, + TErrorManager& errors) const; + + SQLUINTEGER GetQueryTimeoutSec() const noexcept; + SQLULEN GetMaxRows() const noexcept; + SQLULEN GetNoScanMode() const noexcept; + SQLULEN GetMetadataId() const noexcept; + +private: + SQLUINTEGER QueryTimeoutSec_ = 0; + SQLULEN MaxRows_ = 0; + SQLULEN NoScan_ = SQL_NOSCAN_OFF; + SQLULEN MetadataId_ = SQL_FALSE; +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 224f228e498..4e415c65521 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -1,7 +1,10 @@ #include "convert.h" +#include #include +#include + namespace NYdb { namespace NOdbc { @@ -311,6 +314,28 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER EPrimitiveType ydbType = parser.GetPrimitiveType(); switch (targetType) { + case SQL_C_SHORT: + case SQL_C_SSHORT: + { + SQLSMALLINT v = 0; + switch (ydbType) { + case EPrimitiveType::Int16: v = parser.GetInt16(); break; + case EPrimitiveType::Uint16: v = static_cast(parser.GetUint16()); break; + case EPrimitiveType::Int8: v = static_cast(parser.GetInt8()); break; + case EPrimitiveType::Uint8: v = static_cast(parser.GetUint8()); break; + case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; + case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; + case EPrimitiveType::Bool: v = parser.GetBool() ? 1 : 0; break; + default: return SQL_ERROR; + } + if (targetValue) { + *reinterpret_cast(targetValue) = v; + } + if (strLenOrInd) { + *strLenOrInd = sizeof(SQLSMALLINT); + } + return SQL_SUCCESS; + } case SQL_C_SLONG: case SQL_C_LONG: { @@ -377,6 +402,55 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case EPrimitiveType::String: str = parser.GetString(); break; case EPrimitiveType::Json: str = parser.GetJson(); break; case EPrimitiveType::JsonDocument: str = parser.GetJsonDocument(); break; + case EPrimitiveType::Date: { + const TString t = parser.GetDate().FormatGmTime("%Y-%m-%d"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::Date32: { + const i32 days = parser.GetDate32(); + if (days < 0) { + return SQL_ERROR; + } + const TString t = TInstant::Days(static_cast(days)).FormatGmTime("%Y-%m-%d"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::Datetime: { + const TString t = parser.GetDatetime().FormatGmTime("%Y-%m-%d %H:%M:%S"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::Datetime64: { + const std::int64_t secs = parser.GetDatetime64(); + if (secs < 0) { + return SQL_ERROR; + } + const TString t = + TInstant::Seconds(static_cast(static_cast(secs))) + .FormatGmTime("%Y-%m-%d %H:%M:%S"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::Timestamp: { + const TString t = parser.GetTimestamp().FormatGmTime("%Y-%m-%d %H:%M:%S"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::Timestamp64: { + const std::int64_t micros = parser.GetTimestamp64(); + if (micros < 0) { + return SQL_ERROR; + } + const TString t = + TInstant::MicroSeconds(static_cast(static_cast(micros))) + .FormatGmTime("%Y-%m-%d %H:%M:%S"); + str.assign(t.data(), t.size()); + break; + } + case EPrimitiveType::TzDate: str = parser.GetTzDate(); break; + case EPrimitiveType::TzDatetime: str = parser.GetTzDatetime(); break; + case EPrimitiveType::TzTimestamp: str = parser.GetTzTimestamp(); break; default: return SQL_ERROR; } SQLLEN len = str.size(); diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index efbcea9a419..26ad393b03a 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -3,6 +3,8 @@ #include "convert.h" #include "types.h" +#include + namespace NYdb { namespace NOdbc { @@ -40,7 +42,17 @@ class TExecCursor : public ICursor { return false; } if (part.HasResultSet()) { - ResultSetParser_ = std::make_unique(part.ExtractResultSet()); + TResultSet rs = part.ExtractResultSet(); + Columns_.clear(); + Columns_.reserve(rs.ColumnsCount()); + for (const auto& col : rs.GetColumnsMeta()) { + Columns_.push_back(TColumnMeta{ + col.Name, + GetTypeId(col.Type), + 0, + IsNullable(col.Type)}); + } + ResultSetParser_ = std::make_unique(rs); } } return false; diff --git a/odbc/src/utils/escape.cpp b/odbc/src/utils/escape.cpp new file mode 100644 index 00000000000..5a9c643eb7a --- /dev/null +++ b/odbc/src/utils/escape.cpp @@ -0,0 +1,444 @@ +#include "escape.h" + +#include +#include +#include +#include +#include + +namespace NYdb::NOdbc { +namespace { + +bool EqualNoCase(std::string_view lhs, std::string_view rhs) { + return lhs.size() == rhs.size() && + std::equal(lhs.begin(), lhs.end(), rhs.begin(), [](char leftCh, char rightCh) { + return std::tolower(static_cast(leftCh)) == + std::tolower(static_cast(rightCh)); + }); +} + +void SkipLeadingWhitespace(std::string_view sql, size_t& cursor) { + const auto strEnd = sql.end(); + const auto firstNonSpace = std::find_if_not( + sql.begin() + static_cast(cursor), + strEnd, + [](unsigned char byte) { + return std::isspace(byte) != 0; + }); + cursor = static_cast(firstNonSpace - sql.begin()); +} + +bool ReadIdent(std::string_view sql, size_t& cursor, std::string_view* outIdent) { + SkipLeadingWhitespace(sql, cursor); + const size_t identStart = cursor; + const auto afterIdent = std::find_if_not( + sql.begin() + static_cast(cursor), + sql.end(), + [](unsigned char byte) { + return std::isalpha(byte) != 0 || byte == '_'; + }); + cursor = static_cast(afterIdent - sql.begin()); + if (cursor == identStart) { + return false; + } + *outIdent = std::string_view(sql.data() + identStart, cursor - identStart); + return true; +} + +bool ParseSingleQuoted(std::string_view sql, size_t& cursor, std::string* outValue) { + SkipLeadingWhitespace(sql, cursor); + if (cursor >= sql.size() || sql[cursor] != '\'') { + return false; + } + ++cursor; + outValue->clear(); + while (cursor < sql.size()) { + if (sql[cursor] == '\'') { + if (cursor + 1 < sql.size() && sql[cursor + 1] == '\'') { + outValue->push_back('\''); + cursor += 2; + continue; + } + ++cursor; + return true; + } + outValue->push_back(sql[cursor++]); + } + return false; +} + +size_t FindMatchingCloseBrace(std::string_view sql, size_t openBrace) { + if (openBrace >= sql.size() || sql[openBrace] != '{') { + return std::string_view::npos; + } + int braceDepth = 1; + for (size_t idx = openBrace + 1; idx < sql.size(); ++idx) { + if (sql[idx] == '{') { + ++braceDepth; + } else if (sql[idx] == '}') { + --braceDepth; + if (braceDepth == 0) { + return idx; + } + } + } + return std::string_view::npos; +} + +std::string NormalizeOdbcTimestampLiteral(const std::string& raw) { + std::string normalized = raw; + const auto firstSpace = std::find(normalized.begin(), normalized.end(), ' '); + if (firstSpace != normalized.end()) { + *firstSpace = 'T'; + } + if (std::find(normalized.begin(), normalized.end(), 'Z') == normalized.end()) { + normalized.push_back('Z'); + } + return normalized; +} + +std::string ToUpperAscii(std::string_view sv) { + std::string upper; + upper.resize(sv.size()); + std::transform(sv.begin(), sv.end(), upper.begin(), [](unsigned char byte) { + return static_cast(std::toupper(byte)); + }); + return upper; +} + +std::string MapSqlTypeToken(std::string_view sqlType) { + static const std::unordered_map kMap = { + {"CHAR", "Utf8"}, + {"VARCHAR", "Utf8"}, + {"LONGVARCHAR", "Utf8"}, + {"WCHAR", "Utf8"}, + {"WVARCHAR", "Utf8"}, + {"WLONGVARCHAR", "Utf8"}, + {"BIT", "Bool"}, + {"TINYINT", "Int8"}, + {"SMALLINT", "Int16"}, + {"INTEGER", "Int32"}, + {"BIGINT", "Int64"}, + {"REAL", "Float"}, + {"FLOAT", "Double"}, + {"DOUBLE", "Double"}, + {"DECIMAL", "Decimal(22, 9)"}, + {"NUMERIC", "Decimal(22, 9)"}, + {"BINARY", "String"}, + {"VARBINARY", "String"}, + {"LONGVARBINARY", "String"}, + {"DATE", "Date"}, + {"TIME", "Time"}, + {"TIMESTAMP", "Datetime"}, + {"TYPE_DATE", "Date"}, + {"TYPE_TIME", "Time"}, + {"TYPE_TIMESTAMP", "Datetime"}, + }; + std::string key = ToUpperAscii(sqlType); + const std::string kSql = "SQL_"; + if (key.size() > kSql.size() && key.compare(0, kSql.size(), kSql) == 0) { + key.erase(0, kSql.size()); + } + const auto mapped = kMap.find(key); + if (mapped != kMap.end()) { + return mapped->second; + } + return key; +} + +std::string RewriteOdbcEscapesImpl(std::string_view sql); + + +enum class OdbcBraceKind { + OutputProcedureCall, // {?= call ... } + FnBody, // {fn ...} + OjBody, // {oj ...} + DateLiteral, // {d '...'} + TimeLiteral, // {t '...'} + TimestampLiteral, // {ts '...'} + ProcedureCall, // {call ...} + LikeEscape, // {escape '...'} +}; + +struct OdbcBraceParsed { + OdbcBraceKind Kind; + std::string_view RecurseTail; + std::string QuotedValue; +}; + +std::optional TryParseOutputCallBrace(std::string_view sql, size_t parsePos, size_t closeBrace) { + if (parsePos + 1 >= sql.size() || sql[parsePos] != '?' || sql[parsePos + 1] != '=') { + return std::nullopt; + } + size_t inner = parsePos + 2; + SkipLeadingWhitespace(sql, inner); + std::string_view keyword; + if (!ReadIdent(sql, inner, &keyword) || !EqualNoCase(keyword, "call")) { + return std::nullopt; + } + SkipLeadingWhitespace(sql, inner); + if (inner > closeBrace) { + return std::nullopt; + } + OdbcBraceParsed parsed; + parsed.Kind = OdbcBraceKind::OutputProcedureCall; + parsed.RecurseTail = std::string_view(sql.data() + inner, closeBrace - inner); + return parsed; +} + +std::optional MakeRecurseTailBrace(OdbcBraceKind kind, std::string_view sql, size_t& parsePos, size_t closeBrace) { + SkipLeadingWhitespace(sql, parsePos); + if (parsePos > closeBrace) { + return std::nullopt; + } + OdbcBraceParsed parsed; + parsed.Kind = kind; + parsed.RecurseTail = std::string_view(sql.data() + parsePos, closeBrace - parsePos); + return parsed; +} + +std::optional MakeQuotedBrace(OdbcBraceKind kind, std::string_view sql, size_t& parsePos, size_t closeBrace) { + std::string quotedLit; + if (!ParseSingleQuoted(sql, parsePos, "edLit) || parsePos > closeBrace) { + return std::nullopt; + } + SkipLeadingWhitespace(sql, parsePos); + if (parsePos != closeBrace) { + return std::nullopt; + } + OdbcBraceParsed parsed; + parsed.Kind = kind; + parsed.QuotedValue = std::move(quotedLit); + return parsed; +} + +struct BraceKeywordSpec { + std::string_view Keyword; + OdbcBraceKind Kind; + bool IsQuotedLiteral; +}; + +static constexpr BraceKeywordSpec kBraceKeywordSpecs[] = { + {"fn", OdbcBraceKind::FnBody, false}, + {"oj", OdbcBraceKind::OjBody, false}, + {"d", OdbcBraceKind::DateLiteral, true}, + {"t", OdbcBraceKind::TimeLiteral, true}, + {"ts", OdbcBraceKind::TimestampLiteral, true}, + {"call", OdbcBraceKind::ProcedureCall, false}, + {"escape", OdbcBraceKind::LikeEscape, true}, +}; + +std::optional TryParseOdbcBrace(std::string_view sql, size_t openBrace, size_t closeBrace) { + size_t parsePos = openBrace + 1; + SkipLeadingWhitespace(sql, parsePos); + + if (std::optional outputCall = TryParseOutputCallBrace(sql, parsePos, closeBrace)) { + return outputCall; + } + if (parsePos + 1 < sql.size() && sql[parsePos] == '?' && sql[parsePos + 1] == '=') { + return std::nullopt; + } + + std::string_view token; + if (!ReadIdent(sql, parsePos, &token)) { + return std::nullopt; + } + + for (const BraceKeywordSpec& spec : kBraceKeywordSpecs) { + if (!EqualNoCase(token, spec.Keyword)) { + continue; + } + if (spec.IsQuotedLiteral) { + return MakeQuotedBrace(spec.Kind, sql, parsePos, closeBrace); + } + return MakeRecurseTailBrace(spec.Kind, sql, parsePos, closeBrace); + } + + return std::nullopt; +} + +void AppendRewrittenBrace(std::string& rewritten, const OdbcBraceParsed& parsed) { + switch (parsed.Kind) { + case OdbcBraceKind::OutputProcedureCall: + case OdbcBraceKind::ProcedureCall: + rewritten += "CALL "; + rewritten.append(RewriteOdbcEscapesImpl(parsed.RecurseTail)); + return; + case OdbcBraceKind::FnBody: + case OdbcBraceKind::OjBody: + rewritten.append(RewriteOdbcEscapesImpl(parsed.RecurseTail)); + return; + case OdbcBraceKind::DateLiteral: + rewritten += "CAST('"; + rewritten += parsed.QuotedValue; + rewritten += "' AS Date)"; + return; + case OdbcBraceKind::TimeLiteral: + rewritten += "CAST('"; + rewritten += parsed.QuotedValue; + rewritten += "' AS Time)"; + return; + case OdbcBraceKind::TimestampLiteral: { + const std::string normalizedTs = NormalizeOdbcTimestampLiteral(parsed.QuotedValue); + rewritten += "CAST('"; + rewritten += normalizedTs; + rewritten += "' AS Datetime)"; + return; + } + case OdbcBraceKind::LikeEscape: + rewritten += " ESCAPE '"; + rewritten += parsed.QuotedValue; + rewritten += '\''; + return; + } +} + +std::string RewriteOdbcEscapesImpl(std::string_view sql) { + std::string rewritten; + rewritten.reserve(sql.size()); + + for (size_t readPos = 0; readPos < sql.size();) { + if (sql[readPos] != '{') { + rewritten.push_back(sql[readPos++]); + continue; + } + + const size_t closeBrace = FindMatchingCloseBrace(sql, readPos); + if (closeBrace == std::string_view::npos) { + rewritten.push_back(sql[readPos++]); + continue; + } + + if (std::optional parsedBrace = TryParseOdbcBrace(sql, readPos, closeBrace)) { + AppendRewrittenBrace(rewritten, *parsedBrace); + readPos = closeBrace + 1; + continue; + } + + rewritten.push_back(sql[readPos++]); + } + + return rewritten; +} + +std::string RewriteOdbcConvertCalls(std::string_view sql); + +class TOdbcConvertCallRewriter { +public: + explicit TOdbcConvertCallRewriter(std::string_view sql) + : Sql_(sql) { + Rewritten_.reserve(sql.size()); + } + + std::string TakeResult() && { + return std::move(Rewritten_); + } + + void Run() { + while (SegmentStart_ < Sql_.size()) { + const std::optional convertKeywordPos = FindNextConvertKeyword(SegmentStart_); + if (!convertKeywordPos) { + Rewritten_.append(Sql_.substr(SegmentStart_)); + break; + } + Rewritten_.append(Sql_.substr(SegmentStart_, *convertKeywordPos - SegmentStart_)); + if (!TryRewriteConvertAt(*convertKeywordPos)) { + break; + } + } + } + +private: + static constexpr size_t kConvertTokenLen = 7; + + std::optional FindNextConvertKeyword(size_t from) const { + for (size_t probePos = from; probePos + kConvertTokenLen <= Sql_.size(); ++probePos) { + if (!EqualNoCase(Sql_.substr(probePos, kConvertTokenLen), "CONVERT")) { + continue; + } + size_t afterKeyword = probePos + kConvertTokenLen; + SkipLeadingWhitespace(Sql_, afterKeyword); + if (afterKeyword < Sql_.size() && Sql_[afterKeyword] == '(') { + return probePos; + } + } + return std::nullopt; + } + + bool TryRewriteConvertAt(size_t convertKeywordPos) { + size_t parsePos = convertKeywordPos + kConvertTokenLen; + SkipLeadingWhitespace(Sql_, parsePos); + if (parsePos >= Sql_.size() || Sql_[parsePos] != '(') { + Rewritten_.append(Sql_.substr(convertKeywordPos, kConvertTokenLen)); + SegmentStart_ = convertKeywordPos + kConvertTokenLen; + return true; + } + ++parsePos; + + int parenDepth = 1; + const size_t firstArgStart = parsePos; + std::optional typeCommaPos; + for (; parsePos < Sql_.size(); ++parsePos) { + if (Sql_[parsePos] == '(') { + ++parenDepth; + } else if (Sql_[parsePos] == ')') { + --parenDepth; + } else if (Sql_[parsePos] == ',' && parenDepth == 1) { + typeCommaPos = parsePos; + break; + } + } + if (!typeCommaPos) { + Rewritten_.append(Sql_.substr(convertKeywordPos)); + return false; + } + + const std::string_view firstArg(Sql_.data() + firstArgStart, *typeCommaPos - firstArgStart); + parsePos = *typeCommaPos + 1; + SkipLeadingWhitespace(Sql_, parsePos); + const size_t sqlTypeStart = parsePos; + const auto sqlTypeEnd = std::find_if_not( + Sql_.begin() + static_cast(parsePos), + Sql_.end(), + [](unsigned char byte) { + return std::isalpha(byte) != 0 || byte == '_'; + }); + parsePos = static_cast(sqlTypeEnd - Sql_.begin()); + const std::string_view sqlTypeToken(Sql_.data() + sqlTypeStart, parsePos - sqlTypeStart); + SkipLeadingWhitespace(Sql_, parsePos); + if (parsePos >= Sql_.size() || Sql_[parsePos] != ')') { + Rewritten_.append(Sql_.substr(convertKeywordPos)); + return false; + } + + const std::string yqlType = MapSqlTypeToken(sqlTypeToken); + Rewritten_ += "CAST("; + Rewritten_ += RewriteOdbcConvertCalls(RewriteOdbcEscapesImpl(firstArg)); + Rewritten_ += " AS "; + Rewritten_ += yqlType; + Rewritten_ += ')'; + SegmentStart_ = parsePos + 1; + return true; + } + + std::string_view Sql_; + std::string Rewritten_; + size_t SegmentStart_ = 0; +}; + +std::string RewriteOdbcConvertCalls(std::string_view sql) { + TOdbcConvertCallRewriter rewriter(sql); + rewriter.Run(); + return std::move(rewriter).TakeResult(); +} + +} // namespace + + + +std::string RewriteOdbcEscapes(const std::string& sql) { + std::string afterBraceRewrite = RewriteOdbcEscapesImpl(sql); + return RewriteOdbcConvertCalls(afterBraceRewrite); +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/escape.h b/odbc/src/utils/escape.h new file mode 100644 index 00000000000..7397a128450 --- /dev/null +++ b/odbc/src/utils/escape.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace NYdb::NOdbc { + +std::string RewriteOdbcEscapes(const std::string& sql); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/sql_like.h b/odbc/src/utils/sql_like.h new file mode 100644 index 00000000000..f51c10ca28c --- /dev/null +++ b/odbc/src/utils/sql_like.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +namespace NYdb::NOdbc { + +// SQL LIKE — '%' is any substring, '_' is any single character. +inline bool SqlLikeMatch(std::string_view text, std::string_view pattern) { + size_t textPos = 0; + size_t patPos = 0; + size_t lastPercentPat = std::string_view::npos; + size_t textStartAfterPercent = 0; + + const size_t textLen = text.size(); + const size_t patLen = pattern.size(); + + while (textPos < textLen) { + const bool morePat = patPos < patLen; + const char patCh = morePat ? pattern[patPos] : '\0'; + + if (morePat && patCh != '%' && (patCh == '_' || patCh == text[textPos])) { + ++textPos; + ++patPos; + continue; + } + + if (morePat && patCh == '%') { + lastPercentPat = patPos++; + textStartAfterPercent = textPos; + continue; + } + + if (lastPercentPat != std::string_view::npos) { + patPos = lastPercentPat + 1; + ++textStartAfterPercent; + textPos = textStartAfterPercent; + continue; + } + + return false; + } + + while (patPos < patLen && pattern[patPos] == '%') { + ++patPos; + } + return patPos == patLen; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 39128437ced..43925350b02 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -12,3 +12,8 @@ add_odbc_test(NAME odbc-attr_it SOURCES attr_it.cpp ) + +add_odbc_test(NAME odbc-stmt_attr_it + SOURCES + stmt_attr_it.cpp +) \ No newline at end of file diff --git a/odbc/tests/integration/attr_it.cpp b/odbc/tests/integration/attr_it.cpp index 514278f8e67..2dc30446498 100644 --- a/odbc/tests/integration/attr_it.cpp +++ b/odbc/tests/integration/attr_it.cpp @@ -3,26 +3,6 @@ #include #include -namespace { - -bool SqlStatePrefix(const std::string& diag, const char* state5) { - return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; -} - -void AllocEnv(SQLHENV* env) { - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); - ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); -} - -void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { - AllocEnv(env); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, *env, dbc), SQL_SUCCESS); - SQLRETURN rc = SQLDriverConnect( - *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); - CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); -} - -} // namespace TEST(OdbcAttrEnv, OdbcVersionAttr) { SQLHENV env; @@ -145,7 +125,6 @@ TEST(OdbcAttrConn, TxnIsolationAttr) { ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, ¤tIsolation, sizeof(currentIsolation), nullptr), SQL_SUCCESS); ASSERT_EQ(static_cast(SQL_TXN_REPEATABLE_READ), currentIsolation); - // In read-only mode all four standard levels are accepted and remain executable. CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_ONLY, 0), dbc, SQL_HANDLE_DBC); CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_UNCOMMITTED, 0), dbc, SQL_HANDLE_DBC); CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); diff --git a/odbc/tests/integration/env_it.cpp b/odbc/tests/integration/env_it.cpp index fd351d127af..952c1459ad6 100644 --- a/odbc/tests/integration/env_it.cpp +++ b/odbc/tests/integration/env_it.cpp @@ -2,15 +2,6 @@ namespace { -void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); - ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, *env, dbc), SQL_SUCCESS); - SQLRETURN rc = SQLDriverConnect( - *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); - CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); -} - void StartManualTx(SQLHDBC dbc, SQLHSTMT* stmt) { CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, stmt), SQL_SUCCESS); diff --git a/odbc/tests/integration/stmt_attr_it.cpp b/odbc/tests/integration/stmt_attr_it.cpp new file mode 100644 index 00000000000..89faf9abed0 --- /dev/null +++ b/odbc/tests/integration/stmt_attr_it.cpp @@ -0,0 +1,334 @@ +#include "test_utils.h" + +#include +#include +#include +#include + +#ifndef SQL_ATTR_METADATA_ID +#define SQL_ATTR_METADATA_ID 10029 +#endif + + +TEST(OdbcStmtAttr, QueryTimeoutAttr) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLUINTEGER timeoutSec = 1; + CHECK_ODBC_OK( + SQLSetStmtAttr(stmt, SQL_ATTR_QUERY_TIMEOUT, (SQLPOINTER)(uintptr_t)timeoutSec, 0), + stmt, + SQL_HANDLE_STMT); + + SQLCHAR longQuery[] = + "SELECT COUNT(*) FROM AS_TABLE(ListMap(ListFromRange(1u, 100000000u), ($x)->(AsStruct($x AS v))))"; + ASSERT_EQ(SQLExecDirect(stmt, longQuery, SQL_NTS), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYT00")); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcStmtAttr, MaxRowsAttr) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_max_rows"; + SQLCHAR createQuery[] = + "CREATE TABLE test_attr_max_rows (id Int32, value Int32, PRIMARY KEY (id))"; + SQLCHAR upsert1Query[] = "UPSERT INTO test_attr_max_rows (id, value) VALUES (1, 10)"; + SQLCHAR upsert2Query[] = "UPSERT INTO test_attr_max_rows (id, value) VALUES (2, 20)"; + SQLCHAR selectQuery[] = "SELECT value FROM test_attr_max_rows ORDER BY id"; + + CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, upsert1Query, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, upsert2Query, SQL_NTS), stmt, SQL_HANDLE_STMT); + + const SQLULEN maxRows = 1; + CHECK_ODBC_OK( + SQLSetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, (SQLPOINTER)(uintptr_t)maxRows, 0), + stmt, + SQL_HANDLE_STMT); + + SQLULEN maxRowsOut = 0; + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, &maxRowsOut, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(maxRowsOut, maxRows); + + CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcStmtAttr, NoScanAttr) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR selectEscapeFnQuery[] = "SELECT {fn ABS(-12)} AS value"; + + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 12); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_ON, 0), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), SQL_ERROR); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcStmtAttr, OdbcEscapeSequences) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), stmt, SQL_HANDLE_STMT); + + { + SQLCHAR convertQuery[] = "SELECT {fn CONVERT(42, SQL_SMALLINT)} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, convertQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLSMALLINT valueSmall = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_SSHORT, &valueSmall, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueSmall, 42); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + SQLCHAR convertDoubleQuery[] = "SELECT {fn CONVERT(2.5, SQL_DOUBLE)} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, convertDoubleQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + double valueDouble = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_DOUBLE, &valueDouble, 0, &valueInd), SQL_SUCCESS); + ASSERT_LT(std::fabs(valueDouble - 2.5), 1e-9); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + SQLCHAR nestedFnQuery[] = "SELECT {fn {fn ABS(-10)}} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, nestedFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 10); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + SQLCHAR asciiLowerQuery[] = "SELECT {fn String::AsciiToLower('AbC')} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, asciiLowerQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[32] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "abc"); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + SQLCHAR dateQuery[] = "SELECT {d '2024-06-15'} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, dateQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[32] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "2024-06-15"); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + SQLCHAR tsQuery[] = "SELECT {ts '2024-06-15 14:30:00'} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, tsQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[64] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "2024-06-15 14:30:00"); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcStmtAttr, MetadataIdSqlLikeForTableNames) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR ddl[] = R"( + DROP TABLE IF EXISTS test_odbc_meta_like_a; + DROP TABLE IF EXISTS test_odbc_meta_like_b; + CREATE TABLE test_odbc_meta_like_a (id Int32, PRIMARY KEY (id)); + CREATE TABLE test_odbc_meta_like_b (id Int32, PRIMARY KEY (id)); + )"; + CHECK_ODBC_OK(SQLExecDirect(stmt, ddl, SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLULEN metadataId = SQL_TRUE; + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(metadataId, SQL_FALSE); + + const char* likePattern = "%/test_odbc_meta_like_%"; + CHECK_ODBC_OK( + SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + stmt, + SQL_HANDLE_STMT); + int tableRows = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableRows; + } + ASSERT_EQ(tableRows, 2); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK( + SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, + SQL_HANDLE_STMT); + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(metadataId, SQL_TRUE); + + ASSERT_EQ( + SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + const std::string exactPath = "/local/test_odbc_meta_like_a"; + CHECK_ODBC_OK( + SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)exactPath.c_str(), SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + stmt, + SQL_HANDLE_STMT); + tableRows = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableRows; + } + ASSERT_EQ(tableRows, 1); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK( + SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_FALSE, 0), + stmt, + SQL_HANDLE_STMT); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(OdbcStmtAttr, MetadataIdSqlLikeForColumnNames) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR ddl[] = R"( + DROP TABLE IF EXISTS test_odbc_meta_col; + CREATE TABLE test_odbc_meta_col (id Int32, value_x Int32, PRIMARY KEY (id)); + )"; + CHECK_ODBC_OK(SQLExecDirect(stmt, ddl, SQL_NTS), stmt, SQL_HANDLE_STMT); + + constexpr SQLUSMALLINT kColumnNameCol = 4; + char colName[256] = {}; + SQLLEN colInd = 0; + const std::string exactTable = "/local/test_odbc_meta_col"; + + { + CHECK_ODBC_OK( + SQLColumns( + stmt, + nullptr, + 0, + nullptr, + 0, + (SQLCHAR*)"%/test_odbc_meta_col", + SQL_NTS, + (SQLCHAR*)"val%", + SQL_NTS), + stmt, + SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, kColumnNameCol, SQL_C_CHAR, colName, sizeof(colName), &colInd), SQL_SUCCESS); + ASSERT_STREQ(colName, "value_x"); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + CHECK_ODBC_OK( + SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, + SQL_HANDLE_STMT); + + ASSERT_EQ( + SQLColumns( + stmt, + nullptr, + 0, + nullptr, + 0, + (SQLCHAR*)"%/test_odbc_meta_col", + SQL_NTS, + (SQLCHAR*)"value_x", + SQL_NTS), + SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + { + ASSERT_EQ( + SQLColumns( + stmt, + nullptr, + 0, + nullptr, + 0, + (SQLCHAR*)exactTable.c_str(), + SQL_NTS, + (SQLCHAR*)"val%", + SQL_NTS), + SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "42S22")); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h index c43272f0f54..950ffef9508 100644 --- a/odbc/tests/integration/test_utils.h +++ b/odbc/tests/integration/test_utils.h @@ -5,11 +5,9 @@ #include #include +#include #include -#define CHECK_ODBC_OK(rc, handle, type) \ - ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) - inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { SQLCHAR sqlState[6] = {0}; SQLCHAR message[256] = {0}; @@ -22,4 +20,24 @@ inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { return "Unknown ODBC error"; } +#define CHECK_ODBC_OK(rc, handle, type) \ + ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) + inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; + +inline bool SqlStatePrefix(const std::string& diag, const char* state5) { + return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; +} + +inline void AllocEnv(SQLHENV* env) { + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); +} + +inline void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { + AllocEnv(env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, *env, dbc), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); +} diff --git a/odbc/tests/unit/CMakeLists.txt b/odbc/tests/unit/CMakeLists.txt index d1eac199615..d23e837d2f3 100644 --- a/odbc/tests/unit/CMakeLists.txt +++ b/odbc/tests/unit/CMakeLists.txt @@ -8,3 +8,26 @@ add_ydb_test(NAME odbc-convert_ut GTEST LABELS unit ) + +add_ydb_test(NAME odbc-escape_ut GTEST + SOURCES + escape_ut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/escape.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + LINK_LIBRARIES + yutil + LABELS + unit +) + +add_ydb_test(NAME odbc-sql_like_ut GTEST + SOURCES + sql_like_ut.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + LINK_LIBRARIES + yutil + LABELS + unit +) diff --git a/odbc/tests/unit/escape_ut.cpp b/odbc/tests/unit/escape_ut.cpp new file mode 100644 index 00000000000..60b3e582e69 --- /dev/null +++ b/odbc/tests/unit/escape_ut.cpp @@ -0,0 +1,71 @@ +#include "utils/escape.h" + +#include + +using NYdb::NOdbc::RewriteOdbcEscapes; + +TEST(OdbcEscapeRewrite, FnUnwraps) { + EXPECT_EQ(RewriteOdbcEscapes("SELECT {fn ABS(-12)} AS v"), "SELECT ABS(-12) AS v"); +} + +TEST(OdbcEscapeRewrite, FnCaseInsensitive) { + EXPECT_EQ(RewriteOdbcEscapes("{FN LOWER('A')}"), "LOWER('A')"); +} + +TEST(OdbcEscapeRewrite, OjUnwraps) { + EXPECT_EQ(RewriteOdbcEscapes("{oj LEFT OUTER JOIN t ON a=b}"), "LEFT OUTER JOIN t ON a=b"); +} + +TEST(OdbcEscapeRewrite, DateLiteral) { + EXPECT_EQ(RewriteOdbcEscapes("SELECT {d '2024-01-01'}"), "SELECT CAST('2024-01-01' AS Date)"); +} + +TEST(OdbcEscapeRewrite, TimeLiteral) { + EXPECT_EQ(RewriteOdbcEscapes("{t '14:30:00'}"), "CAST('14:30:00' AS Time)"); +} + +TEST(OdbcEscapeRewrite, TimestampLiteralNormalizesSpaceToT) { + EXPECT_EQ( + RewriteOdbcEscapes("SELECT {ts '2024-06-15 14:30:00'} AS v"), + "SELECT CAST('2024-06-15T14:30:00Z' AS Datetime) AS v"); +} + +TEST(OdbcEscapeRewrite, TimestampLiteralKeepsExistingZ) { + EXPECT_EQ( + RewriteOdbcEscapes("SELECT {ts '2024-06-15T14:30:00Z'} AS v"), + "SELECT CAST('2024-06-15T14:30:00Z' AS Datetime) AS v"); +} + +TEST(OdbcEscapeRewrite, Call) { + EXPECT_EQ(RewriteOdbcEscapes("{call sp_demo(1, 2)}"), "CALL sp_demo(1, 2)"); +} + +TEST(OdbcEscapeRewrite, OutputCallBecomesPlainCall) { + EXPECT_EQ(RewriteOdbcEscapes("{?= call sp(1)}"), "CALL sp(1)"); +} + +TEST(OdbcEscapeRewrite, EscapeClause) { + EXPECT_EQ(RewriteOdbcEscapes("LIKE 'a%' {escape '\\'}"), "LIKE 'a%' ESCAPE '\\'"); +} + +TEST(OdbcEscapeRewrite, ConvertOdbcToYqlCast) { + EXPECT_EQ( + RewriteOdbcEscapes("SELECT {fn CONVERT(42, SQL_SMALLINT)} AS v"), + "SELECT CAST(42 AS Int16) AS v"); +} + +TEST(OdbcEscapeRewrite, ConvertNestedInFn) { + EXPECT_EQ(RewriteOdbcEscapes("{fn CONVERT(x, SQL_INTEGER)}"), "CAST(x AS Int32)"); +} + +TEST(OdbcEscapeRewrite, NestedFnEscapes) { + EXPECT_EQ(RewriteOdbcEscapes("{fn {fn ABS(1)}}"), "ABS(1)"); +} + +TEST(OdbcEscapeRewrite, UnknownBraceLeftUnchanged) { + EXPECT_EQ(RewriteOdbcEscapes("{not_a_keyword 1}"), "{not_a_keyword 1}"); +} + +TEST(OdbcEscapeRewrite, EmptyInput) { + EXPECT_EQ(RewriteOdbcEscapes(""), ""); +} diff --git a/odbc/tests/unit/sql_like_ut.cpp b/odbc/tests/unit/sql_like_ut.cpp new file mode 100644 index 00000000000..e0b8d87ee01 --- /dev/null +++ b/odbc/tests/unit/sql_like_ut.cpp @@ -0,0 +1,28 @@ +#include "utils/sql_like.h" + +#include + +using NYdb::NOdbc::SqlLikeMatch; + +TEST(SqlLikeMatch, PercentMatchesSubstring) { + EXPECT_TRUE(SqlLikeMatch("/local/foo_bar", "%foo%")); + EXPECT_TRUE(SqlLikeMatch("/local/pfx_foo_sfx", "%foo%")); + EXPECT_FALSE(SqlLikeMatch("/local/other", "%foo%")); +} + +TEST(SqlLikeMatch, UnderscoreMatchesSingleChar) { + EXPECT_TRUE(SqlLikeMatch("a_c", "a_c")); + EXPECT_TRUE(SqlLikeMatch("abc", "a_c")); + EXPECT_FALSE(SqlLikeMatch("abbc", "a_c")); +} + +TEST(SqlLikeMatch, EmptyPatternMatchesOnlyEmptyText) { + EXPECT_TRUE(SqlLikeMatch("", "")); + EXPECT_FALSE(SqlLikeMatch("anything", "")); +} + +TEST(SqlLikeMatch, PercentAtEnds) { + EXPECT_TRUE(SqlLikeMatch("hello", "%hello%")); + EXPECT_TRUE(SqlLikeMatch("hello", "hel%")); + EXPECT_TRUE(SqlLikeMatch("hello", "%llo")); +} diff --git a/tests/unit/library/operation_id/CMakeLists.txt b/tests/unit/library/operation_id/CMakeLists.txt index a6f2143949a..63d77da600a 100644 --- a/tests/unit/library/operation_id/CMakeLists.txt +++ b/tests/unit/library/operation_id/CMakeLists.txt @@ -5,6 +5,7 @@ add_ydb_test(NAME operation_id_ut GTEST yutil lib-operation_id-protos library-operation_id + cpp-testing-unittest LABELS unit ) From a06fd334ebc4588830f3f47f02c37959a588794b Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sat, 18 Apr 2026 21:09:41 +0000 Subject: [PATCH 20/55] retry qyery for autocommit --- odbc/src/connection.cpp | 6 ++- odbc/src/connection.h | 3 +- odbc/src/statement.cpp | 97 ++++++++++++++++++++++++++++++----------- odbc/src/statement.h | 2 +- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index a52d5036f04..85724670f9d 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -200,10 +200,14 @@ void TConnection::SetTx(const NQuery::TTransaction& tx) { Tx_ = tx; } -void TConnection::Reset() { +void TConnection::ResetTx() { Tx_.reset(); } +void TConnection::ResetQuerySession() { + QuerySession_.reset(); +} + SQLRETURN TConnection::CommitTx() { auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 284ac36cf65..dac7721c000 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -68,7 +68,8 @@ class TConnection : public TErrorManager { const std::optional& GetTx(); void SetTx(const NQuery::TTransaction& tx); - void Reset(); + void ResetTx(); + void ResetQuerySession(); SQLRETURN CommitTx(); SQLRETURN RollbackTx(); diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index f4b04ec0be2..16efb84aa1c 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -8,12 +8,42 @@ #include #include +#include +#include #include +#include + namespace NYdb { namespace NOdbc { +namespace { + NYdb::TStatus StatusFrom(const NYdb::TStatus& ydb_status) { + return NYdb::TStatus(ydb_status.GetStatus(), NYdb::NIssue::TIssues(ydb_status.GetIssues())); + } + + + NYdb::TStatus PrefetchFirstPartStatus(NQuery::TExecuteQueryIterator& iterator, std::optional* prefetchedResultPart){ + prefetchedResultPart->reset(); + while (true) { + auto part = iterator.ReadNext().ExtractValueSync(); + if (part.EOS()) { + break; + } + if (!part.IsSuccess()) { + return StatusFrom(part); + + } + if (part.HasResultSet()) { + prefetchedResultPart->emplace(std::move(part)); + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + } + } + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + } +} + TStatement::TStatement(TConnection* conn) : Conn_(conn) {} @@ -39,18 +69,41 @@ SQLRETURN TStatement::Execute() { } NYdb::TParams params = BuildParams(); - if (Conn_->GetAutocommit()){ - Conn_->Reset(); - } + std::optional iterator; + std::optional prefetchedResultPart; - auto& session = Conn_->GetOrCreateQuerySession(); + if (Conn_->GetAutocommit()){ + Conn_->ResetTx(); + Conn_->ResetQuerySession(); + const NYdb::NRetry::TRetryOperationSettings retrySettings = + MakeAutocommitRetrySettings(); + + NYdb::TStatus execStatus = client->RetryQuerySync( + [this, ¶ms, &iterator, &prefetchedResultPart](NQuery::TSession session) -> NYdb::TStatus{ + auto retry_iterator = CreateExecuteIterator(session, params); + if (!retry_iterator.IsSuccess()) { + return StatusFrom(retry_iterator); + } + std::optional retry_prefetched; + const NYdb::TStatus prefetchStatus = PrefetchFirstPartStatus(retry_iterator, &retry_prefetched); + if (!prefetchStatus.IsSuccess()) { + return prefetchStatus; + } + iterator.emplace(std::move(retry_iterator)); + prefetchedResultPart = std::move(retry_prefetched); + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + }, retrySettings); - auto iterator = CreateExecuteIterator(session, params); - NStatusHelpers::ThrowOnError(iterator); + NStatusHelpers::ThrowOnError(execStatus); + } else { + NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); + iterator.emplace(CreateExecuteIterator(session, params)); + NStatusHelpers::ThrowOnError(*iterator); + NStatusHelpers::ThrowOnError(PrefetchFirstPartStatus(*iterator, &prefetchedResultPart)); + } - std::optional prefetchedResultPart = PrefetchFirstResultPart(iterator); if (prefetchedResultPart) { - Cursor_ = CreateExecCursor(this, std::move(iterator), std::move(prefetchedResultPart)); + Cursor_ = CreateExecCursor(this, std::move(*iterator), std::move(prefetchedResultPart)); } else { Cursor_.reset(); } @@ -59,6 +112,16 @@ SQLRETURN TStatement::Execute() { return SQL_SUCCESS; } +NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() { + NYdb::NRetry::TRetryOperationSettings settings; + SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); + if (queryTimeoutSec > 0) { + const TDuration deadline = TDuration::Seconds(queryTimeoutSec); + settings.MaxTimeout(deadline).GetSessionClientTimeout(deadline); + } + return settings; +} + NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ const std::string sqlText = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ @@ -94,23 +157,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession execSettings).ExtractValueSync(); } -std::optional TStatement::PrefetchFirstResultPart(NQuery::TExecuteQueryIterator& iterator){ - std::optional prefetchedResultPart; - while (true) { - auto part = iterator.ReadNext().ExtractValueSync(); - if (part.EOS()) { - break; - } - if (!part.IsSuccess()) { - NStatusHelpers::ThrowOnError(part); - } - if (part.HasResultSet()) { - prefetchedResultPart.emplace(std::move(part)); - break; - } - } - return prefetchedResultPart; -} + SQLRETURN TStatement::Fetch() { if (!Cursor_) { diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 702fe56c71e..754250bd269 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -74,8 +74,8 @@ class TStatement : public TErrorManager, public IBindingFiller { NYdb::TParams BuildParams(); NQuery::TExecuteQueryIterator CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params); - std::optional PrefetchFirstResultPart(NQuery::TExecuteQueryIterator& iterator); + NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); std::vector GetPatternEntries(const std::string& pattern); SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); bool IsPatternMatch(const std::string& path, const std::string& pattern); From 248574adcd7df85f35c9ffbae371978ecab61cfc Mon Sep 17 00:00:00 2001 From: Ylonies Date: Mon, 27 Apr 2026 19:42:05 +0300 Subject: [PATCH 21/55] getdiagfield + fixes --- odbc/src/odbc_driver.cpp | 12 +++- odbc/src/statement.cpp | 16 +++++ odbc/src/statement.h | 3 + odbc/src/utils/bindings.h | 3 +- odbc/src/utils/error_manager.cpp | 115 +++++++++++++++++++++++-------- odbc/src/utils/error_manager.h | 14 +++- 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index cba323453af..97d9ffd8a5a 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -32,7 +32,9 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, return NYdb::NOdbc::HandleOdbcExceptions( inputHandle, [&]() { - *outputHandle = new NYdb::NOdbc::TEnvironment(); + auto* const env = new NYdb::NOdbc::TEnvironment(); + *outputHandle = env; + env->SetLastReturnCode(SQL_SUCCESS); return SQL_SUCCESS; }, NYdb::NOdbc::ENullInputHandlePolicy::Allow); @@ -43,14 +45,18 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, auto conn = std::make_unique(); conn->SetEnvironment(env); env->RegisterConnection(conn.get()); - *outputHandle = conn.release(); + auto* const raw = conn.release(); + *outputHandle = raw; + raw->SetLastReturnCode(SQL_SUCCESS); return SQL_SUCCESS; }); } case SQL_HANDLE_STMT: { return NYdb::NOdbc::HandleOdbcExceptions(inputHandle, [&](auto* conn) { auto stmt = conn->CreateStatement(); - *outputHandle = stmt.release(); + auto* const raw = stmt.release(); + *outputHandle = raw; + raw->SetLastReturnCode(SQL_SUCCESS); return SQL_SUCCESS; }); } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 16efb84aa1c..5d6bb38f152 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -518,5 +518,21 @@ SQLRETURN TStatement::GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER return Attributes_.GetStmtAttr(attr, value, bufferLength, stringLengthPtr, *this); } +SQLRETURN TStatement::GetDiagField( + SQLSMALLINT recNumber, + SQLSMALLINT diagIdentifier, + SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + if (recNumber == 0 && diagIdentifier == SQL_DIAG_ROW_COUNT) { + if (!diagInfoPtr) { + return SQL_ERROR; + } + *reinterpret_cast(diagInfoPtr) = -1; + return SQL_SUCCESS; + } + return TErrorManager::GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 754250bd269..9f2eb8ade64 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -55,6 +55,9 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength); SQLRETURN GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); + SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) override; + TConnection* GetConnection() { return Conn_; } diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h index 443d9787d70..2480f5367af 100644 --- a/odbc/src/utils/bindings.h +++ b/odbc/src/utils/bindings.h @@ -31,8 +31,7 @@ struct TBoundColumn { class IBindingFiller { public: virtual void FillBoundColumns() = 0; - virtual void OnStreamPartError(const TStatus& status) { - (void)status; + virtual void OnStreamPartError([[maybe_unused]] const TStatus& status) { } virtual ~IBindingFiller() = default; diff --git a/odbc/src/utils/error_manager.cpp b/odbc/src/utils/error_manager.cpp index fbb577e3824..92c8ec1750f 100644 --- a/odbc/src/utils/error_manager.cpp +++ b/odbc/src/utils/error_manager.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace NYdb { namespace NOdbc { @@ -56,13 +58,57 @@ namespace { } } // namespace +namespace { + +SQLRETURN WriteDiagCStr( + const std::string& str, + SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr, + bool sqlStateField = false) { + std::string storage; + const std::string* src = &str; + if (sqlStateField) { + storage = str; + if (storage.size() < 5) { + storage.append(5U - storage.size(), ' '); + } else { + storage.resize(5U); + } + src = &storage; + } + if (!diagInfoPtr) { + return SQL_ERROR; + } + if (bufferLength < 0) { + return SQL_ERROR; + } + const size_t fullLen = src->size(); + if (stringLengthPtr) { + *stringLengthPtr = static_cast(std::min(fullLen, 0x7FFFU)); + } + if (bufferLength == 0) { + return fullLen == 0 ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + } + auto* out = static_cast(diagInfoPtr); + const size_t maxData = static_cast(bufferLength - 1U); + const size_t copyLen = std::min(fullLen, maxData); + std::memcpy(out, src->data(), copyLen); + out[copyLen] = 0; + return (fullLen > maxData) ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; +} + +} // namespace + SQLRETURN TErrorManager::AddError(const std::string& sqlState, SQLINTEGER nativeError, const std::string& message, SQLRETURN returnCode) { Errors_.push_back({sqlState, nativeError, message, returnCode}); + LastReturnCode_ = returnCode; return returnCode; } SQLRETURN TErrorManager::AddError(const TOdbcException& ex) { Errors_.push_back({ex.GetSqlState(), ex.GetNativeError(), ex.GetMessage(), ex.GetReturnCode()}); + LastReturnCode_ = ex.GetReturnCode(); return ex.GetReturnCode(); } @@ -73,6 +119,7 @@ SQLRETURN TErrorManager::AddError(const TStatus& status) { message += ": " + status.GetIssues().ToString(); } Errors_.push_back({mapping.sqlState, static_cast(status.GetStatus()), message, mapping.returnCode}); + LastReturnCode_ = mapping.returnCode; return mapping.returnCode; } @@ -104,19 +151,26 @@ SQLRETURN TErrorManager::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQ return SQL_SUCCESS; } -SQLRETURN TErrorManager::GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, - SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { +SQLRETURN TErrorManager::GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { const SQLSMALLINT count = static_cast(Errors_.size()); - + if (diagInfoPtr == nullptr) { + return SQL_ERROR; + } if (recNumber == 0) { - if (diagIdentifier == SQL_DIAG_NUMBER) { - if (!diagInfoPtr) { - return SQL_ERROR; + switch (diagIdentifier) { + case SQL_DIAG_RETURNCODE: + *static_cast(diagInfoPtr) = LastReturnCode_; + return SQL_SUCCESS; + case SQL_DIAG_NUMBER: { + *static_cast(diagInfoPtr) = static_cast(count); + return SQL_SUCCESS; } - *static_cast(diagInfoPtr) = count; - return SQL_SUCCESS; + case SQL_DIAG_ROW_COUNT: + return SQL_ERROR; + default: + return SQL_ERROR; } - return SQL_NO_DATA; } if (recNumber < 1 || recNumber > count) { @@ -126,28 +180,28 @@ SQLRETURN TErrorManager::GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIde const auto& err = Errors_[recNumber - 1]; switch (diagIdentifier) { case SQL_DIAG_SQLSTATE: - if (!diagInfoPtr) { - return SQL_ERROR; - } - strncpy((char*)diagInfoPtr, err.SqlState.c_str(), 6); - return SQL_SUCCESS; - case SQL_DIAG_NATIVE: - if (!diagInfoPtr) { - return SQL_ERROR; - } + return WriteDiagCStr(err.SqlState, diagInfoPtr, bufferLength, stringLengthPtr, true); + case SQL_DIAG_NATIVE: { *static_cast(diagInfoPtr) = err.NativeError; return SQL_SUCCESS; + } case SQL_DIAG_MESSAGE_TEXT: - if (!diagInfoPtr || bufferLength <= 0) { - return SQL_ERROR; - } - strncpy((char*)diagInfoPtr, err.Message.c_str(), bufferLength); - if (stringLengthPtr) { - *stringLengthPtr = static_cast(err.Message.size()); - } + return WriteDiagCStr(err.Message, diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_CLASS_ORIGIN: + return WriteDiagCStr("ODBC 3.0", diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_SUBCLASS_ORIGIN: + return WriteDiagCStr("ODBC 3.0", diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_CONNECTION_NAME: + case SQL_DIAG_SERVER_NAME: + return WriteDiagCStr("", diagInfoPtr, bufferLength, stringLengthPtr); + case SQL_DIAG_COLUMN_NUMBER: + *static_cast(diagInfoPtr) = SQL_COLUMN_NUMBER_UNKNOWN; + return SQL_SUCCESS; + case SQL_DIAG_ROW_NUMBER: + *static_cast(diagInfoPtr) = SQL_ROW_NUMBER_UNKNOWN; return SQL_SUCCESS; default: - return SQL_NO_DATA; + return SQL_ERROR; } } @@ -160,8 +214,15 @@ SQLRETURN HandleOdbcExceptions( } try { - return func(); + const SQLRETURN r = func(); + if (handlePtr) { + static_cast(handlePtr)->SetLastReturnCode(r); + } + return r; } catch (...) { + if (handlePtr) { + static_cast(handlePtr)->SetLastReturnCode(SQL_ERROR); + } return SQL_ERROR; } } diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h index 5f72a69f563..9f91fab8a1d 100644 --- a/odbc/src/utils/error_manager.h +++ b/odbc/src/utils/error_manager.h @@ -64,13 +64,21 @@ class TErrorManager { void ClearErrors(); + void SetLastReturnCode(SQLRETURN code) { + LastReturnCode_ = code; + } + [[nodiscard]] SQLRETURN GetLastReturnCode() const { + return LastReturnCode_; + } + SQLRETURN GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQLINTEGER* nativeError, SQLCHAR* messageText, SQLSMALLINT bufferLength, SQLSMALLINT* textLength); - SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, + virtual SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr); private: TErrorList Errors_; + SQLRETURN LastReturnCode_ = SQL_SUCCESS; }; enum class ENullInputHandlePolicy : unsigned char { @@ -86,7 +94,9 @@ SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function(handlePtr); try { - return func(handle); + const SQLRETURN ret = func(handle); + handle->SetLastReturnCode(ret); + return ret; } catch (const NStatusHelpers::TYdbErrorException& ex) { return handle->AddError(ex.GetStatus()); } catch (const TOdbcException& ex) { From 06c92f32627dd9bcbb8872b029b09015ec09832f Mon Sep 17 00:00:00 2001 From: Ylonies Date: Tue, 5 May 2026 19:34:07 +0000 Subject: [PATCH 22/55] review fixes --- odbc/CMakeLists.txt | 2 + odbc/README.md | 10 ++++- odbc/odbc.ini | 6 +-- odbc/odbcinst.ini | 4 +- odbc/src/connection.cpp | 14 ++++--- odbc/src/connection_attr.cpp | 13 +----- odbc/src/statement.cpp | 27 ++++++++++-- odbc/src/utils/convert.cpp | 41 +++++++++++++++++- odbc/src/utils/types.cpp | 80 +++++++++++++++++++++++++++++++----- odbc/src/utils/types.h | 1 + odbc/src/utils/util.cpp | 6 +++ 11 files changed, 164 insertions(+), 40 deletions(-) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 06386fd31dd..a728041fa88 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -27,6 +27,8 @@ target_link_libraries(ydb-odbc YDB-CPP-SDK::Table YDB-CPP-SDK::Scheme YDB-CPP-SDK::Driver + ODBC::ODBC + odbcinst ) set_target_properties(ydb-odbc PROPERTIES diff --git a/odbc/README.md b/odbc/README.md index c73f9b8704a..f240a8cb2ee 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -33,8 +33,8 @@ odbcinst -q -s [YDB] Driver=YDB Description=YDB Database Connection -Server=your-server:port -Database=/path/to/database +Server=localhost:2136 +Database=/local ``` ## Usage @@ -71,6 +71,12 @@ SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); ``` +Alternatively, use `SQLDriverConnect` with a connection string (does not require DSN in odbc.ini): +```c +SQLCHAR connStr[] = "Driver=YDB;Endpoint=localhost:2136;Database=/local"; +SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); +``` + ## Parameters Use names $p1, $p2, ... for parameter names diff --git a/odbc/odbc.ini b/odbc/odbc.ini index 6335b3ee389..a1ba3c951c3 100644 --- a/odbc/odbc.ini +++ b/odbc/odbc.ini @@ -4,6 +4,6 @@ YDB=YDB ODBC Driver [YDB] Driver=YDB Description=YDB Database Connection -Server=grpc://localhost:2136 -Database=local -AuthMode=none \ No newline at end of file +Server=localhost:2136 +Database=/local +AuthMode=none diff --git a/odbc/odbcinst.ini b/odbc/odbcinst.ini index fd0b3f27650..db2a9b8378e 100644 --- a/odbc/odbcinst.ini +++ b/odbc/odbcinst.ini @@ -1,4 +1,4 @@ [YDB] Description=YDB ODBC Driver -Driver=/home/brgayazov/ydbwork/ydb-cpp-sdk/build/odbc/libydb-odbc.so -Setup=/home/brgayazov/ydbwork/ydb-cpp-sdk/build/odbc/libydb-odbc.so \ No newline at end of file +Driver=/app/build/odbc/libydb-odbc.so +Setup=/app/build/odbc/libydb-odbc.so diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 85724670f9d..8a3e33db59e 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -86,11 +86,11 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { } pos = sc+1; } - Endpoint_ = params["Endpoint"]; + Endpoint_ = params.contains("Server") ? params["Server"] : params["Endpoint"]; Database_ = params["Database"]; if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint or Database in connection string"); + throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in connection string"); } TConnectionAttributes::NormalizeCatalogPath(Database_); @@ -105,16 +105,18 @@ SQLRETURN TConnection::Connect(const std::string& serverName, const std::string& auth) { char endpoint[256] = {0}; + char server[256] = {0}; char database[256] = {0}; - //SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); - //SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); + SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); + SQLGetPrivateProfileString(serverName.c_str(), "Server", "", server, sizeof(server), nullptr); + SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); - Endpoint_ = endpoint; + Endpoint_ = endpoint[0] ? endpoint : server; Database_ = database; if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint or Database in DSN"); + throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in DSN"); } TConnectionAttributes::NormalizeCatalogPath(Database_); diff --git a/odbc/src/connection_attr.cpp b/odbc/src/connection_attr.cpp index 6197a4ad4a9..4872b1cdef1 100644 --- a/odbc/src/connection_attr.cpp +++ b/odbc/src/connection_attr.cpp @@ -65,21 +65,12 @@ bool IsKnownTxnIsolation(SQLUINTEGER txnIsolation) { std::optional ResolveTxMode(SQLUINTEGER accessMode, SQLUINTEGER txnIsolation) { if (accessMode == SQL_MODE_READ_ONLY) { - switch (txnIsolation) { - case SQL_TXN_READ_UNCOMMITTED: - return NQuery::TTxSettings::TS_STALE_RO; - case SQL_TXN_READ_COMMITTED: - return NQuery::TTxSettings::TS_ONLINE_RO; - case SQL_TXN_REPEATABLE_READ: - case SQL_TXN_SERIALIZABLE: - return NQuery::TTxSettings::TS_SNAPSHOT_RO; - default: - return std::nullopt; - } + return NQuery::TTxSettings::TS_SNAPSHOT_RO; } switch (txnIsolation) { case SQL_TXN_REPEATABLE_READ: + return NQuery::TTxSettings::TS_SNAPSHOT_RW; case SQL_TXN_SERIALIZABLE: return NQuery::TTxSettings::TS_SERIALIZABLE_RW; default: diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 5d6bb38f152..b32355954cc 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include namespace NYdb { namespace NOdbc { @@ -23,7 +25,6 @@ namespace { return NYdb::TStatus(ydb_status.GetStatus(), NYdb::NIssue::TIssues(ydb_status.GetIssues())); } - NYdb::TStatus PrefetchFirstPartStatus(NQuery::TExecuteQueryIterator& iterator, std::optional* prefetchedResultPart){ prefetchedResultPart->reset(); while (true) { @@ -130,9 +131,27 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession NQuery::TExecuteQuerySettings execSettings; const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); + const auto txSettings = Conn_->MakeTxSettings(); if (Conn_->GetAutocommit()) { - const auto txSettings = Conn_->MakeTxSettings(); - if (txSettings.GetMode() == NQuery::TTxSettings::TS_SERIALIZABLE_RW) { + // TS_SNAPSHOT_RW doesn't support explicit BeginTx() - we use NoTx() instead + // DDL must use NoTx() per YDB documentation + const bool isSnapshotRw = (txSettings.GetMode() == NQuery::TTxSettings::TS_SNAPSHOT_RW); + + const bool isDdl = [&queryText] { + size_t pos = 0; + while (pos < queryText.size() && std::isspace(static_cast(queryText[pos]))) { + ++pos; + } + if (queryText.size() - pos >= 6) { + const char* start = queryText.c_str() + pos; + return (strncasecmp(start, "CREATE", 6) == 0 || + strncasecmp(start, "DROP", 4) == 0 || + strncasecmp(start, "ALTER", 5) == 0); + } + return false; + }(); + + if (isSnapshotRw || isDdl) { return session.StreamExecuteQuery( queryText, NQuery::TTxControl::NoTx(), @@ -146,7 +165,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession execSettings).ExtractValueSync(); } if (!Conn_->GetTx()) { - auto beginTxResult = session.BeginTransaction(Conn_->MakeTxSettings()).ExtractValueSync(); + auto beginTxResult = session.BeginTransaction(txSettings).ExtractValueSync(); NStatusHelpers::ThrowOnError(beginTxResult); Conn_->SetTx(beginTxResult.GetTransaction()); } diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 4e415c65521..1349e677c62 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -39,12 +39,49 @@ struct TTypedValue { template<> TTypedValue::TTypedValue(const TBoundParam& param) { - Data = std::string(static_cast(param.ParameterValuePtr), param.BufferLength); + if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { + Data.clear(); + return; + } + + const char* ptr = static_cast(param.ParameterValuePtr); + if (!ptr) { + Data.clear(); + return; + } + + if (param.StrLenOrIndPtr) { + SQLLEN len = *param.StrLenOrIndPtr; + if (len == SQL_NTS) { + Data = std::string(ptr); + } else if (len >= 0) { + Data = std::string(ptr, static_cast(len)); + } else { + Data = std::string(ptr, param.BufferLength); + } + } else { + Data = std::string(ptr, param.BufferLength); + } } template<> TTypedValue::TTypedValue(const TBoundParam& param) { - Data = std::string(static_cast(param.ParameterValuePtr), param.BufferLength); + if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { + Data.clear(); + return; + } + + const char* ptr = static_cast(param.ParameterValuePtr); + if (!ptr) { + Data.clear(); + return; + } + + if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr >= 0) { + Data = std::string(ptr, static_cast(*param.StrLenOrIndPtr)); + } else { + Data = std::string(ptr, param.BufferLength); + } } class IConverter { diff --git a/odbc/src/utils/types.cpp b/odbc/src/utils/types.cpp index ce5ead462cc..5174101b760 100644 --- a/odbc/src/utils/types.cpp +++ b/odbc/src/utils/types.cpp @@ -4,8 +4,75 @@ namespace NYdb { namespace NOdbc { SQLSMALLINT GetTypeId(const TType& type) { - // TODO: implement - return 0; + TTypeParser typeParser(type); + size_t openedOptionals = 0; + while (typeParser.GetKind() == TTypeParser::ETypeKind::Optional) { + typeParser.OpenOptional(); + ++openedOptionals; + } + + auto closeOpenedOptionals = [&]() { + while (openedOptionals > 0) { + typeParser.CloseOptional(); + --openedOptionals; + } + }; + + const auto kind = typeParser.GetKind(); + if (kind == TTypeParser::ETypeKind::Primitive) { + const auto primitive = typeParser.GetPrimitive(); + closeOpenedOptionals(); + switch (primitive) { + case EPrimitiveType::Bool: + return SQL_BIT; + case EPrimitiveType::Int8: + case EPrimitiveType::Uint8: + return SQL_TINYINT; + case EPrimitiveType::Int16: + case EPrimitiveType::Uint16: + return SQL_SMALLINT; + case EPrimitiveType::Int32: + case EPrimitiveType::Uint32: + return SQL_INTEGER; + case EPrimitiveType::Int64: + case EPrimitiveType::Uint64: + return SQL_BIGINT; + case EPrimitiveType::Float: + return SQL_REAL; + case EPrimitiveType::Double: + return SQL_DOUBLE; + case EPrimitiveType::Date: + case EPrimitiveType::Date32: + case EPrimitiveType::TzDate: + return SQL_TYPE_DATE; + case EPrimitiveType::Datetime: + case EPrimitiveType::Timestamp: + case EPrimitiveType::Datetime64: + case EPrimitiveType::Timestamp64: + case EPrimitiveType::TzDatetime: + case EPrimitiveType::TzTimestamp: + return SQL_TYPE_TIMESTAMP; + case EPrimitiveType::Interval: + case EPrimitiveType::Interval64: + return SQL_BIGINT; + case EPrimitiveType::String: + return SQL_VARBINARY; + case EPrimitiveType::Utf8: + case EPrimitiveType::Yson: + case EPrimitiveType::Json: + case EPrimitiveType::JsonDocument: + case EPrimitiveType::DyNumber: + return SQL_VARCHAR; + case EPrimitiveType::Uuid: + return SQL_GUID; + } + } + + closeOpenedOptionals(); + if (kind == TTypeParser::ETypeKind::Decimal) { + return SQL_DECIMAL; + } + return SQL_UNKNOWN_TYPE; } SQLSMALLINT IsNullable(const TType& type) { @@ -25,21 +92,14 @@ std::optional GetDecimalDigits(const TType& type) { switch (typeParser.GetPrimitive()) { case EPrimitiveType::Int64: - return 64; case EPrimitiveType::Uint64: - return 64; case EPrimitiveType::Int32: - return 32; case EPrimitiveType::Uint32: - return 32; case EPrimitiveType::Int16: - return 16; case EPrimitiveType::Uint16: - return 16; case EPrimitiveType::Int8: - return 8; case EPrimitiveType::Uint8: - return 8; + return 0; default: return std::nullopt; } diff --git a/odbc/src/utils/types.h b/odbc/src/utils/types.h index 3f481702902..73a0a32bb13 100644 --- a/odbc/src/utils/types.h +++ b/odbc/src/utils/types.h @@ -3,6 +3,7 @@ #include #include +#include namespace NYdb { namespace NOdbc { diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp index 9097ce80dbf..54700d06091 100644 --- a/odbc/src/utils/util.cpp +++ b/odbc/src/utils/util.cpp @@ -3,9 +3,15 @@ namespace NYdb::NOdbc { std::string GetString(SQLCHAR* str, SQLSMALLINT length) { + if (!str) { + return {}; + } if (length == SQL_NTS) { return std::string(reinterpret_cast(str)); } + if (length <= 0) { + return {}; + } return std::string(reinterpret_cast(str), length); } From 9923f3352bc6fdcb8344107e1746181bb95356ec Mon Sep 17 00:00:00 2001 From: Ylonies Date: Wed, 6 May 2026 10:08:19 +0000 Subject: [PATCH 23/55] full core api tests --- odbc/CMakeLists.txt | 1 + odbc/src/get_info.cpp | 208 +++++++ odbc/src/get_info.h | 17 + odbc/src/odbc_driver.cpp | 11 + odbc/tests/integration/CMakeLists.txt | 30 +- odbc/tests/integration/attr_it.cpp | 223 ------- odbc/tests/integration/basic_it.cpp | 25 + odbc/tests/integration/connection_api_it.cpp | 193 ++++++ odbc/tests/integration/env_it.cpp | 90 --- odbc/tests/integration/environment_api_it.cpp | 225 +++++++ odbc/tests/integration/error_handling_it.cpp | 124 ++++ odbc/tests/integration/metadata_api_it.cpp | 224 +++++++ odbc/tests/integration/statement_api_it.cpp | 578 ++++++++++++++++++ odbc/tests/integration/stmt_attr_it.cpp | 334 ---------- odbc/tests/integration/test_utils.h | 75 ++- odbc/tests/integration/transaction_api_it.cpp | 179 ++++++ odbc/tests/unit/convert_ut.cpp | 216 +++++++ 17 files changed, 2098 insertions(+), 655 deletions(-) create mode 100644 odbc/src/get_info.cpp create mode 100644 odbc/src/get_info.h delete mode 100644 odbc/tests/integration/attr_it.cpp create mode 100644 odbc/tests/integration/connection_api_it.cpp delete mode 100644 odbc/tests/integration/env_it.cpp create mode 100644 odbc/tests/integration/environment_api_it.cpp create mode 100644 odbc/tests/integration/error_handling_it.cpp create mode 100644 odbc/tests/integration/metadata_api_it.cpp create mode 100644 odbc/tests/integration/statement_api_it.cpp delete mode 100644 odbc/tests/integration/stmt_attr_it.cpp create mode 100644 odbc/tests/integration/transaction_api_it.cpp diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index a728041fa88..b819fbcf35f 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(ydb-odbc SHARED src/statement_attr.cpp src/statement.cpp src/environment.cpp + src/get_info.cpp ) target_include_directories(ydb-odbc diff --git a/odbc/src/get_info.cpp b/odbc/src/get_info.cpp new file mode 100644 index 00000000000..6f63f590230 --- /dev/null +++ b/odbc/src/get_info.cpp @@ -0,0 +1,208 @@ +#include "get_info.h" + +#include +#include + +namespace NYdb::NOdbc { +namespace { + +SQLRETURN WriteInfoString( + TConnection* conn, + const char* value, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + if (!infoValuePtr) { + return conn->AddError("HY009", 0, "Invalid use of null pointer"); + } + if (bufferLength < 0) { + return conn->AddError("HY090", 0, "Invalid string or buffer length"); + } + const SQLSMALLINT fullLen = static_cast(std::strlen(value)); + if (stringLengthPtr) { + *stringLengthPtr = fullLen; + } + if (bufferLength == 0) { + return fullLen == 0 ? SQL_SUCCESS : conn->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + + auto* out = reinterpret_cast(infoValuePtr); + const SQLSMALLINT copyLen = static_cast(std::min(fullLen, bufferLength - 1)); + if (copyLen > 0) { + std::memcpy(out, value, static_cast(copyLen)); + } + out[copyLen] = '\0'; + if (copyLen < fullLen) { + return conn->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + return SQL_SUCCESS; +} + +template +SQLRETURN WriteInfoScalar( + TConnection* conn, + T value, + SQLPOINTER infoValuePtr, + SQLSMALLINT* stringLengthPtr) { + if (!infoValuePtr) { + return conn->AddError("HY009", 0, "Invalid use of null pointer"); + } + *reinterpret_cast(infoValuePtr) = value; + if (stringLengthPtr) { + *stringLengthPtr = static_cast(sizeof(T)); + } + return SQL_SUCCESS; +} + +} // namespace + +SQLRETURN TInfoProvider::GetInfo( + TConnection* conn, + SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + switch (infoType) { + // Driver Information + case SQL_DRIVER_NAME: + return WriteInfoString(conn, "ydb-odbc", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_DRIVER_VER: + return WriteInfoString(conn, "03.80.0000", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_DRIVER_ODBC_VER: + return WriteInfoString(conn, "03.80", infoValuePtr, bufferLength, stringLengthPtr); + + // DBMS Information + case SQL_DBMS_NAME: + return WriteInfoString(conn, "YDB", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_DBMS_VER: + return WriteInfoString(conn, "3.8.0", infoValuePtr, bufferLength, stringLengthPtr); + + // Identifier Handling + case SQL_IDENTIFIER_QUOTE_CHAR: + return WriteInfoString(conn, "\"", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_IDENTIFIER_CASE: + return WriteInfoScalar(conn, SQL_IC_MIXED, infoValuePtr, stringLengthPtr); + + // Catalog Support + case SQL_CATALOG_NAME: + return WriteInfoString(conn, "Y", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_CATALOG_NAME_SEPARATOR: + return WriteInfoString(conn, "/", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_CATALOG_TERM: + return WriteInfoString(conn, "path", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_CATALOG_USAGE: + return WriteInfoScalar(conn, SQL_CU_DML_STATEMENTS, infoValuePtr, stringLengthPtr); + + // Schema Support (YDB doesn't use schemas) + case SQL_SCHEMA_USAGE: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_SCHEMA_TERM: + return WriteInfoString(conn, "", infoValuePtr, bufferLength, stringLengthPtr); + + // Data Source Capabilities + case SQL_DATA_SOURCE_READ_ONLY: + return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_DATA_SOURCE_NAME: + return WriteInfoString(conn, "YDB", infoValuePtr, bufferLength, stringLengthPtr); + + // Result Set Capabilities + case SQL_MULT_RESULT_SETS: + return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_DYNAMIC_CURSOR_ATTRIBUTES1: + case SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1: + case SQL_STATIC_CURSOR_ATTRIBUTES1: + return WriteInfoScalar(conn, SQL_CA1_NEXT, infoValuePtr, stringLengthPtr); + case SQL_CURSOR_COMMIT_BEHAVIOR: + case SQL_CURSOR_ROLLBACK_BEHAVIOR: + return WriteInfoScalar(conn, SQL_CB_CLOSE, infoValuePtr, stringLengthPtr); + + // Transaction Support + case SQL_TXN_CAPABLE: + return WriteInfoScalar(conn, SQL_TC_ALL, infoValuePtr, stringLengthPtr); + case SQL_DEFAULT_TXN_ISOLATION: + return WriteInfoScalar(conn, SQL_TXN_SERIALIZABLE, infoValuePtr, stringLengthPtr); + case SQL_TXN_ISOLATION_OPTION: + return WriteInfoScalar( + conn, + SQL_TXN_READ_UNCOMMITTED | SQL_TXN_READ_COMMITTED | SQL_TXN_REPEATABLE_READ | SQL_TXN_SERIALIZABLE, + infoValuePtr, + stringLengthPtr); + + // Connection Limits + case SQL_MAX_CONCURRENT_ACTIVITIES: + return WriteInfoScalar(conn, 1, infoValuePtr, stringLengthPtr); + case SQL_MAX_DRIVER_CONNECTIONS: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + + // SQL Support + case SQL_SQL_CONFORMANCE: + return WriteInfoScalar(conn, SQL_SC_SQL92_ENTRY, infoValuePtr, stringLengthPtr); + case SQL_SQL92_RELATIONAL_JOIN_OPERATORS: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_SUBQUERIES: + return WriteInfoScalar(conn, SQL_SQ_CORRELATED_SUBQUERIES, infoValuePtr, stringLengthPtr); + + // Supported Statements + case SQL_SQL92_PREDICATES: + return WriteInfoScalar(conn, SQL_SP_IN | SQL_SP_ISNOTNULL | SQL_SP_ISNULL | SQL_SP_LIKE, infoValuePtr, stringLengthPtr); + case SQL_SQL92_VALUE_EXPRESSIONS: + return WriteInfoScalar(conn, SQL_SVE_CAST | SQL_SVE_CASE | SQL_SVE_COALESCE, infoValuePtr, stringLengthPtr); + case SQL_AGGREGATE_FUNCTIONS: + return WriteInfoScalar(conn, SQL_AF_ALL | SQL_AF_AVG | SQL_AF_COUNT | SQL_AF_MAX | SQL_AF_MIN | SQL_AF_SUM, infoValuePtr, stringLengthPtr); + + // Data Type Limits + case SQL_MAX_CHAR_LITERAL_LEN: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_MAX_COLUMN_NAME_LEN: + return WriteInfoScalar(conn, 255, infoValuePtr, stringLengthPtr); + case SQL_MAX_TABLE_NAME_LEN: + return WriteInfoScalar(conn, 255, infoValuePtr, stringLengthPtr); + case SQL_MAX_COLUMNS_IN_TABLE: + return WriteInfoScalar(conn, 256, infoValuePtr, stringLengthPtr); + case SQL_MAX_COLUMNS_IN_SELECT: + return WriteInfoScalar(conn, 1024, infoValuePtr, stringLengthPtr); + + // Stored Procedures (not supported) + case SQL_PROCEDURES: + return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + + // Outer Joins (limited support) + case SQL_OUTER_JOINS: + return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + + // Positioned Operations (not supported) + case SQL_POSITIONED_STATEMENTS: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + + // Batch Operations (not supported) + case SQL_BATCH_SUPPORT: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_BATCH_ROW_COUNT: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + + // Bookmarks (not supported) + case SQL_BOOKMARK_PERSISTENCE: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + + // Named Cursors (not supported) + case SQL_FILE_USAGE: + return WriteInfoScalar(conn, SQL_FILE_NOT_SUPPORTED, infoValuePtr, stringLengthPtr); + + // GetData Extensions + case SQL_GETDATA_EXTENSIONS: + return WriteInfoScalar(conn, SQL_GD_ANY_COLUMN | SQL_GD_ANY_ORDER, infoValuePtr, stringLengthPtr); + + // Async Execution (not supported) + case SQL_ASYNC_MODE: + return WriteInfoScalar(conn, SQL_AM_NONE, infoValuePtr, stringLengthPtr); + + // Case Sensitivity + case SQL_QUOTED_IDENTIFIER_CASE: + return WriteInfoScalar(conn, SQL_IC_SENSITIVE, infoValuePtr, stringLengthPtr); + + default: + return conn->AddError("HYC00", 0, "Optional feature not implemented"); + } +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/get_info.h b/odbc/src/get_info.h new file mode 100644 index 00000000000..d4e05d88447 --- /dev/null +++ b/odbc/src/get_info.h @@ -0,0 +1,17 @@ +#pragma once + +#include "connection.h" + +namespace NYdb::NOdbc { + +class TInfoProvider { +public: + static SQLRETURN GetInfo( + TConnection* conn, + SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr); +}; + +} // namespace NYdb::NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 97d9ffd8a5a..63b07dd256e 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -1,6 +1,7 @@ #include "environment.h" #include "connection.h" #include "statement.h" +#include "get_info.h" #include "utils/util.h" #include "utils/error_manager.h" @@ -409,4 +410,14 @@ SQLRETURN SQL_API SQLGetStmtAttr( }); } +SQLRETURN SQL_API SQLGetInfo(SQLHDBC connectionHandle, + SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + return NYdb::NOdbc::TInfoProvider::GetInfo(conn, infoType, infoValuePtr, bufferLength, stringLengthPtr); + }); +} + } diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 43925350b02..3e9a4fcf80d 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -3,17 +3,33 @@ add_odbc_test(NAME odbc-basic_it basic_it.cpp ) -add_odbc_test(NAME odbc-env_it +add_odbc_test(NAME odbc-environment_api_it SOURCES - env_it.cpp + environment_api_it.cpp ) -add_odbc_test(NAME odbc-attr_it +# Core API Tests +add_odbc_test(NAME odbc-connection_api_it SOURCES - attr_it.cpp + connection_api_it.cpp ) -add_odbc_test(NAME odbc-stmt_attr_it +add_odbc_test(NAME odbc-statement_api_it SOURCES - stmt_attr_it.cpp -) \ No newline at end of file + statement_api_it.cpp +) + +add_odbc_test(NAME odbc-transaction_api_it + SOURCES + transaction_api_it.cpp +) + +add_odbc_test(NAME odbc-error_handling_it + SOURCES + error_handling_it.cpp +) + +add_odbc_test(NAME odbc-metadata_api_it + SOURCES + metadata_api_it.cpp +) diff --git a/odbc/tests/integration/attr_it.cpp b/odbc/tests/integration/attr_it.cpp deleted file mode 100644 index 2dc30446498..00000000000 --- a/odbc/tests/integration/attr_it.cpp +++ /dev/null @@ -1,223 +0,0 @@ -#include "test_utils.h" - -#include -#include - - -TEST(OdbcAttrEnv, OdbcVersionAttr) { - SQLHENV env; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); - ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); - ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0), SQL_SUCCESS); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcAttrEnv, OutputNtsAttr) { - SQLHENV env; - AllocEnv(&env); - ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_TRUE, 0), SQL_SUCCESS); - ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_FALSE, 0), SQL_SUCCESS); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcAttrConn, AutocommitAttr) { - SQLHENV env; - SQLHDBC dbc; - AllocEnvAndConnect(&env, &dbc); - SQLHSTMT stmt; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_autocommit"; - SQLCHAR createQuery[] = - "CREATE TABLE test_attr_autocommit (id Int32, value Int32, PRIMARY KEY (id))"; - SQLCHAR upsertRollbackQuery[] = "UPSERT INTO test_attr_autocommit (id, value) VALUES (1, 100)"; - SQLCHAR upsertCommitQuery[] = "UPSERT INTO test_attr_autocommit (id, value) VALUES (1, 200)"; - SQLCHAR selectQuery[] = "SELECT value FROM test_attr_autocommit WHERE id = 1"; - - CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLExecDirect(stmt, upsertRollbackQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_ROLLBACK), SQL_SUCCESS); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - CHECK_ODBC_OK(SQLExecDirect(stmt, upsertCommitQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT), SQL_SUCCESS); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLINTEGER valueInt = 0; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueInt, 200); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0), dbc, SQL_HANDLE_DBC); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcAttrConn, AccessModeAttr) { - SQLHENV env; - SQLHDBC dbc; - AllocEnvAndConnect(&env, &dbc); - - constexpr SQLUINTEGER readWriteMode = SQL_MODE_READ_WRITE; - constexpr SQLUINTEGER readOnlyMode = SQL_MODE_READ_ONLY; - SQLUINTEGER currentMode = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); - ASSERT_EQ(readWriteMode, currentMode); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readOnlyMode, 0), dbc, SQL_HANDLE_DBC); - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); - ASSERT_EQ(readOnlyMode, currentMode); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readWriteMode, 0), dbc, SQL_HANDLE_DBC); - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, ¤tMode, sizeof(currentMode), nullptr), SQL_SUCCESS); - ASSERT_EQ(readWriteMode, currentMode); - - ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)9999, 0), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); - - SQLHSTMT stmt; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_read_only"; - SQLCHAR createQuery[] = "CREATE TABLE test_attr_read_only (id Int32, PRIMARY KEY (id))"; - SQLCHAR selectOneQuery[] = "SELECT 1 AS value"; - SQLCHAR upsertQuery[] = "UPSERT INTO test_attr_read_only (id) VALUES (1)"; - CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)readOnlyMode, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLExecDirect(stmt, upsertQuery, SQL_NTS), SQL_ERROR); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcAttrConn, TxnIsolationAttr) { - SQLHENV env; - SQLHDBC dbc; - AllocEnvAndConnect(&env, &dbc); - SQLHSTMT stmt; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - SQLCHAR selectOneQuery[] = "SELECT 1 AS value"; - - SQLUINTEGER currentIsolation = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, ¤tIsolation, sizeof(currentIsolation), nullptr), SQL_SUCCESS); - ASSERT_EQ(static_cast(SQL_TXN_SERIALIZABLE), currentIsolation); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_REPEATABLE_READ, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_COMMITTED, 0), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HYC00")); - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, ¤tIsolation, sizeof(currentIsolation), nullptr), SQL_SUCCESS); - ASSERT_EQ(static_cast(SQL_TXN_REPEATABLE_READ), currentIsolation); - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_ONLY, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_UNCOMMITTED, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectOneQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_COMMITTED, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_REPEATABLE_READ, 0), dbc, SQL_HANDLE_DBC); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_SERIALIZABLE, 0), dbc, SQL_HANDLE_DBC); - - ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)9999, 0), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcAttrConn, CurrentCatalogAttr) { - SQLHENV env; - SQLHDBC dbc; - AllocEnvAndConnect(&env, &dbc); - SQLHSTMT stmt; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - constexpr const char* dbRoot = "/local"; - const std::string catA = std::string(dbRoot) + "/odbc_cat_a"; - const std::string catB = std::string(dbRoot) + "/odbc_cat_b"; - SQLCHAR dropAQuery[] = "DROP TABLE IF EXISTS `odbc_cat_a/probe`"; - SQLCHAR dropBQuery[] = "DROP TABLE IF EXISTS `odbc_cat_b/probe`"; - SQLCHAR createAQuery[] = - "CREATE TABLE `odbc_cat_a/probe` (id Int32, value Int32, PRIMARY KEY (id))"; - SQLCHAR createBQuery[] = - "CREATE TABLE `odbc_cat_b/probe` (id Int32, value Int32, PRIMARY KEY (id))"; - SQLCHAR upsertAQuery[] = "UPSERT INTO `odbc_cat_a/probe` (id, value) VALUES (1, 100)"; - SQLCHAR upsertBQuery[] = "UPSERT INTO `odbc_cat_b/probe` (id, value) VALUES (1, 200)"; - SQLCHAR selectAQuery[] = "SELECT value FROM `odbc_cat_a/probe` WHERE id = 1"; - SQLCHAR selectQuery[] = "SELECT value FROM probe WHERE id = 1"; - - char catalog[256] = {0}; - SQLINTEGER textLen = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); - ASSERT_STREQ(catalog, dbRoot); - - - CHECK_ODBC_OK(SQLExecDirect(stmt, dropAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, dropBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, createAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, createBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, upsertAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, upsertBQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectAQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - SQLINTEGER valueInt = 0; - SQLLEN valueInd = 0; - - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catA.c_str(), SQL_NTS), dbc, - SQL_HANDLE_DBC); - std::memset(catalog, 0, sizeof(catalog)); - textLen = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); - ASSERT_STREQ(catalog, catA.c_str()); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueInt, 100); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - valueInt = 0; - valueInd = 0; - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catB.c_str(), SQL_NTS), dbc, - SQL_HANDLE_DBC); - std::memset(catalog, 0, sizeof(catalog)); - textLen = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); - ASSERT_STREQ(catalog, catB.c_str()); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueInt, 200); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - const std::string catWithSlashes = catB + "///"; - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)catWithSlashes.c_str(), SQL_NTS), dbc, - SQL_HANDLE_DBC); - std::memset(catalog, 0, sizeof(catalog)); - textLen = 0; - ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &textLen), SQL_SUCCESS); - ASSERT_STREQ(catalog, catB.c_str()); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - diff --git a/odbc/tests/integration/basic_it.cpp b/odbc/tests/integration/basic_it.cpp index 37973667147..e7af877b37a 100644 --- a/odbc/tests/integration/basic_it.cpp +++ b/odbc/tests/integration/basic_it.cpp @@ -99,3 +99,28 @@ TEST(OdbcBasic, ColumnBinding) { SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); } + +TEST(OdbcBasic, SQLConnect) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + CHECK_ODBC_OK(SQLConnect(dbc, (SQLCHAR*)"YDB", SQL_NTS, (SQLCHAR*)"", SQL_NTS, (SQLCHAR*)"", SQL_NTS), + dbc, SQL_HANDLE_DBC); + + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLINTEGER val; + SQLLEN ind; + SQLBindCol(stmt, 1, SQL_C_SLONG, &val, 0, &ind); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(val, 1); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/connection_api_it.cpp b/odbc/tests/integration/connection_api_it.cpp new file mode 100644 index 00000000000..aff067a9e20 --- /dev/null +++ b/odbc/tests/integration/connection_api_it.cpp @@ -0,0 +1,193 @@ +#include "test_utils.h" + +TEST(ConnectionApi, AllocFreeEnvHandle) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_ENV, env), SQL_SUCCESS); +} + +TEST(ConnectionApi, AllocFreeDbcHandle) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_DBC, dbc), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, AllocFreeHandleInvalid) { + SQLHENV env; + SQLRETURN rc = SQLAllocHandle(999, SQL_NULL_HANDLE, &env); + ASSERT_TRUE(rc == SQL_ERROR || rc == SQL_INVALID_HANDLE); +} + +TEST(ConnectionApi, SQLConnectWithDSN) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLConnect(dbc, (SQLCHAR*)"YDB", SQL_NTS, (SQLCHAR*)"", SQL_NTS, (SQLCHAR*)"", SQL_NTS), + dbc, SQL_HANDLE_DBC); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectComplete) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + SQLCHAR outStr[256]; + SQLSMALLINT outLen; + SQLRETURN rc = SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, + outStr, sizeof(outStr), &outLen, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc, SQL_HANDLE_DBC); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectNoPrompt) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLRETURN rc = SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + CHECK_ODBC_OK(rc, dbc, SQL_HANDLE_DBC); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectInvalidConnString) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLRETURN rc = SQLDriverConnect(dbc, nullptr, (SQLCHAR*)"InvalidParam=test", SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + ASSERT_EQ(rc, SQL_ERROR); + + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLConnectMissingDSN) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLRETURN rc = SQLConnect(dbc, (SQLCHAR*)"NONEXISTENT_DSN", SQL_NTS, (SQLCHAR*)"", SQL_NTS, (SQLCHAR*)"", SQL_NTS); + ASSERT_EQ(rc, SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "IM002")); + + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, EnvAttrOdbcVersion) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, EnvAttrOutputNts) { + SQLHENV env; + AllocEnv(&env); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_TRUE, 0), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_FALSE, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, ConnAttrAccessMode) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLUINTEGER mode; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, &mode, sizeof(mode), nullptr), SQL_SUCCESS); + ASSERT_EQ(mode, SQL_MODE_READ_WRITE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_ONLY, 0), + dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, &mode, sizeof(mode), nullptr), SQL_SUCCESS); + ASSERT_EQ(mode, SQL_MODE_READ_ONLY); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_WRITE, 0), + dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)9999, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, ConnAttrCurrentCatalog) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + char catalog[256]; + SQLINTEGER len; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &len), SQL_SUCCESS); + ASSERT_STREQ(catalog, "/local"); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local/test", SQL_NTS), + dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, catalog, sizeof(catalog), &len), SQL_SUCCESS); + ASSERT_STREQ(catalog, "/local/test"); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, ConnAttrCurrentCatalogAffectsQueries) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS `/local/cat_a/probe`", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE `/local/cat_a/probe` (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO `/local/cat_a/probe` (id, value) VALUES (1, 100)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS `/local/cat_b/probe`", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE `/local/cat_b/probe` (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO `/local/cat_b/probe` (id, value) VALUES (1, 200)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local/cat_a", SQL_NTS), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT value FROM probe WHERE id = 1", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER value; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &value, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(value, 100); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local/cat_b", SQL_NTS), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT value FROM probe WHERE id = 1", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &value, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(value, 200); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/env_it.cpp b/odbc/tests/integration/env_it.cpp deleted file mode 100644 index 952c1459ad6..00000000000 --- a/odbc/tests/integration/env_it.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "test_utils.h" - -namespace { - -void StartManualTx(SQLHDBC dbc, SQLHSTMT* stmt) { - CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, stmt), SQL_SUCCESS); - CHECK_ODBC_OK(SQLExecDirect(*stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), *stmt, SQL_HANDLE_STMT); -} - -} // namespace - -TEST(OdbcEnv, EndTranCommitOnEnv) { - SQLHENV env; - SQLHDBC dbc1, dbc2; - SQLHSTMT stmt1, stmt2; - - AllocEnvAndConnect(&env, &dbc1); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); - SQLRETURN rc = SQLDriverConnect( - dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); - CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); - - StartManualTx(dbc1, &stmt1); - StartManualTx(dbc2, &stmt2); - - CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT), env, SQL_HANDLE_ENV); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt1); - SQLFreeHandle(SQL_HANDLE_STMT, stmt2); - SQLDisconnect(dbc1); - SQLDisconnect(dbc2); - SQLFreeHandle(SQL_HANDLE_DBC, dbc1); - SQLFreeHandle(SQL_HANDLE_DBC, dbc2); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcEnv, EndTranRollbackOnEnv) { - SQLHENV env; - SQLHDBC dbc1, dbc2; - SQLHSTMT stmt1, stmt2; - - AllocEnvAndConnect(&env, &dbc1); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); - SQLRETURN rc = SQLDriverConnect( - dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); - CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); - - StartManualTx(dbc1, &stmt1); - StartManualTx(dbc2, &stmt2); - - CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_ROLLBACK), env, SQL_HANDLE_ENV); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt1); - SQLFreeHandle(SQL_HANDLE_STMT, stmt2); - SQLDisconnect(dbc1); - SQLDisconnect(dbc2); - SQLFreeHandle(SQL_HANDLE_DBC, dbc1); - SQLFreeHandle(SQL_HANDLE_DBC, dbc2); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcEnv, EndTranPartialFailureReturnsInfo) { - SQLHENV env; - SQLHDBC dbc1, dbc2; - SQLHSTMT stmt1, stmt2; - - AllocEnvAndConnect(&env, &dbc1); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); - SQLRETURN rc = SQLDriverConnect( - dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); - CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); - - StartManualTx(dbc1, &stmt1); - CHECK_ODBC_OK(SQLSetConnectAttr(dbc2, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc2, SQL_HANDLE_DBC); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc2, &stmt2), SQL_SUCCESS); - (void)SQLExecDirect(stmt2, (SQLCHAR*)"SELECT FROM", SQL_NTS); - - rc = SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT); - ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO || rc == SQL_ERROR) - << GetOdbcError(env, SQL_HANDLE_ENV); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt1); - SQLFreeHandle(SQL_HANDLE_STMT, stmt2); - SQLDisconnect(dbc1); - SQLDisconnect(dbc2); - SQLFreeHandle(SQL_HANDLE_DBC, dbc1); - SQLFreeHandle(SQL_HANDLE_DBC, dbc2); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} diff --git a/odbc/tests/integration/environment_api_it.cpp b/odbc/tests/integration/environment_api_it.cpp new file mode 100644 index 00000000000..b3395dc620a --- /dev/null +++ b/odbc/tests/integration/environment_api_it.cpp @@ -0,0 +1,225 @@ +#include "test_utils.h" + +TEST(EnvironmentApi, AllocFreeEnv) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_ENV, env), SQL_SUCCESS); +} + +TEST(EnvironmentApi, AllocEnvInvalidType) { + SQLHENV env; + SQLRETURN rc = SQLAllocHandle(999, SQL_NULL_HANDLE, &env); + ASSERT_TRUE(rc == SQL_ERROR || rc == SQL_INVALID_HANDLE); +} + +TEST(EnvironmentApi, FreeInvalidEnvHandle) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_ENV, env), SQL_SUCCESS); + SQLRETURN rc = SQLFreeHandle(SQL_HANDLE_ENV, env); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_INVALID_HANDLE || rc == SQL_ERROR); +} + +TEST(EnvironmentApi, DoubleFreeEnv) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_ENV, env), SQL_SUCCESS); + // Second free may return error or success depending on implementation + // but should not crash + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, SetOdbcVersion) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, SetOdbcVersionInvalid) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, nullptr, 0), SQL_SUCCESS); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)9999, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, GetOdbcVersion) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + + SQLINTEGER version; + ASSERT_EQ(SQLGetEnvAttr(env, SQL_ATTR_ODBC_VERSION, &version, sizeof(version), nullptr), SQL_SUCCESS); + ASSERT_EQ(version, SQL_OV_ODBC3); + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, SetOutputNtsTrue) { + SQLHENV env; + AllocEnv(&env); + + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_TRUE, 0), SQL_SUCCESS); + + SQLINTEGER outputNts; + ASSERT_EQ(SQLGetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, &outputNts, sizeof(outputNts), nullptr), SQL_SUCCESS); + ASSERT_EQ(outputNts, SQL_TRUE); + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, SetOutputNtsFalse) { + SQLHENV env; + AllocEnv(&env); + ASSERT_NE(SQLSetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, (void*)SQL_FALSE, 0), SQL_SUCCESS); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, GetOutputNtsDefault) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + ASSERT_EQ(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); + SQLINTEGER outputNts; + ASSERT_EQ(SQLGetEnvAttr(env, SQL_ATTR_OUTPUT_NTS, &outputNts, sizeof(outputNts), nullptr), SQL_SUCCESS); + ASSERT_EQ(outputNts, SQL_TRUE); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, SetInvalidEnvAttr) { + SQLHENV env; + AllocEnv(&env); + ASSERT_EQ(SQLSetEnvAttr(env, 9999, (void*)1, 0), SQL_ERROR); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, GetInvalidEnvAttr) { + SQLHENV env; + AllocEnv(&env); + char buffer[256]; + SQLINTEGER len; + ASSERT_EQ(SQLGetEnvAttr(env, 9999, buffer, sizeof(buffer), &len), SQL_ERROR); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, MultipleConnectionsSequential) { + SQLHENV env; + AllocEnv(&env); + for (int i = 0; i < 3; ++i) { + SQLHDBC dbc; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + CHECK_ODBC_OK(SQLConnect(dbc, (SQLCHAR*)"YDB", SQL_NTS, nullptr, 0, nullptr, 0), dbc, SQL_HANDLE_DBC); + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + char query[32]; + snprintf(query, sizeof(query), "SELECT %d", i + 1); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)query, SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +namespace { + +void StartManualTx(SQLHDBC dbc, SQLHSTMT* stmt) { + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(*stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), *stmt, SQL_HANDLE_STMT); +} + +} // namespace + +TEST(EnvironmentApi, EndTranCommitOnEnv) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + StartManualTx(dbc2, &stmt2); + + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT), env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, EndTranRollbackOnEnv) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + StartManualTx(dbc2, &stmt2); + + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_ROLLBACK), env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, EndTranPartialFailureReturnsInfo) { + SQLHENV env; + SQLHDBC dbc1, dbc2; + SQLHSTMT stmt1, stmt2; + + AllocEnvAndConnect(&env, &dbc1); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc2), SQL_SUCCESS); + SQLRETURN rc = SQLDriverConnect( + dbc2, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); + CHECK_ODBC_OK(rc, dbc2, SQL_HANDLE_DBC); + + StartManualTx(dbc1, &stmt1); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc2, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), dbc2, SQL_HANDLE_DBC); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc2, &stmt2), SQL_SUCCESS); + (void)SQLExecDirect(stmt2, (SQLCHAR*)"SELECT FROM", SQL_NTS); + + rc = SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO || rc == SQL_ERROR) + << GetOdbcError(env, SQL_HANDLE_ENV); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt2); + SQLDisconnect(dbc1); + SQLDisconnect(dbc2); + SQLFreeHandle(SQL_HANDLE_DBC, dbc1); + SQLFreeHandle(SQL_HANDLE_DBC, dbc2); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(EnvironmentApi, GetDiagRecEnv) { + SQLHENV env; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env), SQL_SUCCESS); + (void)SQLSetEnvAttr(env, 9999, (void*)1, 0); + SQLCHAR sqlState[6]; + SQLINTEGER nativeError; + SQLCHAR msg[256]; + SQLSMALLINT msgLen; + (void)SQLGetDiagRec(SQL_HANDLE_ENV, env, 1, sqlState, &nativeError, msg, sizeof(msg), &msgLen); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/error_handling_it.cpp b/odbc/tests/integration/error_handling_it.cpp new file mode 100644 index 00000000000..efa95527a3a --- /dev/null +++ b/odbc/tests/integration/error_handling_it.cpp @@ -0,0 +1,124 @@ +#include "test_utils.h" + +TEST(ErrorHandling, GetDiagRecAfterError) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + SQLRETURN rc = SQLConnect(dbc, (SQLCHAR*)"NONEXISTENT_DSN", SQL_NTS, + (SQLCHAR*)"", SQL_NTS, (SQLCHAR*)"", SQL_NTS); + ASSERT_EQ(rc, SQL_ERROR); + SQLCHAR sqlState[6]; + SQLINTEGER nativeError; + SQLCHAR msg[256]; + SQLSMALLINT msgLen; + SQLRETURN diagRc = SQLGetDiagRec(SQL_HANDLE_DBC, dbc, 1, sqlState, &nativeError, + msg, sizeof(msg), &msgLen); + ASSERT_TRUE(diagRc == SQL_SUCCESS || diagRc == SQL_SUCCESS_WITH_INFO); + ASSERT_EQ(msgLen, strlen((char*)msg)); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ErrorHandling, GetDiagRecMultipleErrors) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLExecDirect(stmt, (SQLCHAR*)"INVALID SYNTAX HERE", SQL_NTS); + + SQLSMALLINT numRecs; + SQLGetDiagField(SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_NUMBER, &numRecs, 0, nullptr); + + for (SQLSMALLINT i = 1; i <= numRecs; ++i) { + SQLCHAR sqlState[6]; + SQLINTEGER nativeError; + SQLCHAR msg[256]; + SQLSMALLINT msgLen; + SQLRETURN rc = SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i, sqlState, &nativeError, + msg, sizeof(msg), &msgLen); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO); + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ErrorHandling, GetDiagFieldState) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"SELECT invalid_column FROM nonexistent_table", SQL_NTS); + SQLCHAR sqlState[6]; + SQLRETURN rc = SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_SQLSTATE, + sqlState, sizeof(sqlState), nullptr); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ErrorHandling, GetDiagFieldNativeError) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM nonexistent_table", SQL_NTS); + SQLINTEGER nativeError; + SQLRETURN rc = SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_NATIVE, + &nativeError, sizeof(nativeError), nullptr); + ASSERT_TRUE(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + + +TEST(ErrorHandling, SuccessWithInfo) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + SQLCHAR outStr[10]; + SQLSMALLINT outLen; + SQLRETURN rc = SQLDriverConnect(dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, + outStr, sizeof(outStr), &outLen, SQL_DRIVER_NOPROMPT); + if (rc == SQL_SUCCESS_WITH_INFO) { + SQLCHAR sqlState[6]; + SQLINTEGER nativeError; + SQLCHAR msg[256]; + SQLSMALLINT msgLen; + SQLGetDiagRec(SQL_HANDLE_DBC, dbc, 1, sqlState, &nativeError, msg, sizeof(msg), &msgLen); + } + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ErrorHandling, ClearErrors) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM nonexistent_table", SQL_NTS); + SQLSMALLINT numRecs1; + SQLGetDiagField(SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_NUMBER, &numRecs1, 0, nullptr); + ASSERT_GT(numRecs1, 0); + SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS); + SQLSMALLINT numRecs2; + SQLGetDiagField(SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_NUMBER, &numRecs2, 0, nullptr); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp new file mode 100644 index 00000000000..007deec9f90 --- /dev/null +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -0,0 +1,224 @@ +#include "test_utils.h" + +#ifndef SQL_ATTR_METADATA_ID +#define SQL_ATTR_METADATA_ID 10029 +#endif + +TEST(MetadataApi, SQLTablesAll) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0), + stmt, SQL_HANDLE_STMT); + int rowCount = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++rowCount; + } + ASSERT_GT(rowCount, 0); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLTablesWithPattern) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_metadata_pattern_a", SQL_NTS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_metadata_pattern_b", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_metadata_pattern_a (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_metadata_pattern_b (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"%/test_metadata_pattern_%", SQL_NTS, nullptr, 0), + stmt, SQL_HANDLE_STMT); + int tableCount = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableCount; + } + ASSERT_EQ(tableCount, 2); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLTablesExactMatch) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_exact_table", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_exact_table (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + const std::string exactPath = "/local/test_exact_table"; + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)exactPath.c_str(), SQL_NTS, nullptr, 0), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLTablesLikePatternWithMetadataId) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_meta_table_1", SQL_NTS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_meta_table_2", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_meta_table_1 (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_meta_table_2 (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + SQLULEN metadataId = SQL_FALSE; + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(metadataId, SQL_FALSE); + const char* likePattern = "%/test_meta_table_%"; + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + stmt, SQL_HANDLE_STMT); + int tableRows = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableRows; + } + ASSERT_EQ(tableRows, 2); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(metadataId, SQL_TRUE); + ASSERT_EQ(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); + SQLFreeStmt(stmt, SQL_CLOSE); + const std::string exactPath = "/local/test_meta_table_1"; + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)exactPath.c_str(), SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + stmt, SQL_HANDLE_STMT); + tableRows = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableRows; + } + ASSERT_EQ(tableRows, 1); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_FALSE, 0), + stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLColumnsAll) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_columns_all", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_columns_all (id Int32, name Text, value Int32, PRIMARY KEY (id))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLColumns(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"/local/test_columns_all", SQL_NTS, nullptr, 0), + stmt, SQL_HANDLE_STMT); + int colCount = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++colCount; + } + ASSERT_EQ(colCount, 3); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLColumnsWithPattern) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_columns_pattern", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_columns_pattern (id Int32, value_x Int32, value_y Int32, PRIMARY KEY (id))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + constexpr SQLUSMALLINT kColumnNameCol = 4; + char colName[256] = {}; + SQLLEN colInd = 0; + CHECK_ODBC_OK(SQLColumns(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"/local/test_columns_pattern", SQL_NTS, + (SQLCHAR*)"val%", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, kColumnNameCol, SQL_C_CHAR, colName, sizeof(colName), &colInd), SQL_SUCCESS); + ASSERT_STREQ(colName, "value_x"); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, kColumnNameCol, SQL_C_CHAR, colName, sizeof(colName), &colInd), SQL_SUCCESS); + ASSERT_STREQ(colName, "value_y"); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLColumnsMetadataId) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_columns_metadata", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_columns_metadata (id Int32, value_x Int32, PRIMARY KEY (id))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + const std::string exactTable = "/local/test_columns_metadata"; + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLColumns(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)exactTable.c_str(), SQL_NTS, + (SQLCHAR*)"val%", SQL_NTS), + SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "42S22")); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_FALSE, 0), + stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp new file mode 100644 index 00000000000..df0bb22d6ab --- /dev/null +++ b/odbc/tests/integration/statement_api_it.cpp @@ -0,0 +1,578 @@ +#include "test_utils.h" + +#include + +#ifndef SQL_ATTR_METADATA_ID +#define SQL_ATTR_METADATA_ID 10029 +#endif + +TEST(StatementApi, AllocFreeStmtHandle) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_STMT, stmt), SQL_SUCCESS); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, ExecDirectSimple) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS value", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, ExecDirectMultipleColumns) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT 1 AS int_col, 'hello' AS str_col, CAST(3.14 AS Double) AS float_col", + SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, ExecDirectInvalidSyntax) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + ASSERT_EQ(SQLExecDirect(stmt, (SQLCHAR*)"INVALID SYNTAX HERE", SQL_NTS), SQL_ERROR); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, ExecDirectInvalidTable) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + ASSERT_EQ(SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM nonexistent_table", SQL_NTS), SQL_ERROR); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, PrepareAndExecute) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT $p1 + $p2 AS result", SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLINTEGER p1 = 10, p2 = 20; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &p1, 0, nullptr), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &p2, 0, nullptr), stmt, SQL_HANDLE_STMT); + + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, PrepareAndExecuteReused) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT $p1", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER param; + SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, ¶m, 0, nullptr); + param = 100; + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER result; + SQLGetData(stmt, 1, SQL_C_LONG, &result, 0, nullptr); + ASSERT_EQ(result, 100); + SQLCloseCursor(stmt); + param = 200; + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLGetData(stmt, 1, SQL_C_LONG, &result, 0, nullptr); + ASSERT_EQ(result, 200); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, FetchSingleRow) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 42", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, FetchMultipleRows) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT * FROM AS_TABLE(ListMap(ListFromRange(1, 4), ($x) -> (AsStruct($x AS a)))) ORDER BY a", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER value; + SQLLEN ind; + SQLBindCol(stmt, 1, SQL_C_LONG, &value, 0, &ind); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(value, 1); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(value, 2); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(value, 3); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, BindColMultipleTypes) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 42 AS col1, 'test' AS col2", SQL_NTS), + stmt, SQL_HANDLE_STMT); + + SQLINTEGER col1; + char col2[64]; + SQLLEN col1Ind, col2Ind; + + SQLBindCol(stmt, 1, SQL_C_LONG, &col1, 0, &col1Ind); + SQLBindCol(stmt, 2, SQL_C_CHAR, col2, sizeof(col2), &col2Ind); + + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(col1, 42); + ASSERT_STREQ(col2, "test"); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, BindColThenGetData) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 100", SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLINTEGER value; + SQLLEN ind; + SQLBindCol(stmt, 1, SQL_C_LONG, &value, 0, &ind); + + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(value, 100); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, GetDataWithoutBindCol) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 100", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLINTEGER value; + SQLLEN ind; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &value, 0, &ind), SQL_SUCCESS); + ASSERT_EQ(value, 100); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, GetDataMultipleColumns) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1, 'hello world'", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLINTEGER col1; + SQLLEN col1Ind; + SQLGetData(stmt, 1, SQL_C_LONG, &col1, 0, &col1Ind); + ASSERT_EQ(col1, 1); + + char col2[64]; + SQLLEN col2Ind; + SQLGetData(stmt, 2, SQL_C_CHAR, col2, sizeof(col2), &col2Ind); + ASSERT_STREQ(col2, "hello world"); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, CloseCursor) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLCloseCursor(stmt), stmt, SQL_HANDLE_STMT); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, FreeStmtClose) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLFreeStmt(stmt, SQL_CLOSE), stmt, SQL_HANDLE_STMT); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, FreeStmtResetParams) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLINTEGER param = 42; + SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, ¶m, 0, nullptr); + + CHECK_ODBC_OK(SQLFreeStmt(stmt, SQL_RESET_PARAMS), stmt, SQL_HANDLE_STMT); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, NumResultCols) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1, 2, 3, 4, 5", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLSMALLINT numCols; + CHECK_ODBC_OK(SQLNumResultCols(stmt, &numCols), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(numCols, 5); + SQLFetch(stmt); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, RowCount) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT * FROM AS_TABLE(ListMap(ListFromRange(1, 4), ($x) -> (AsStruct($x AS v))))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLLEN rowCount; + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, AttrQueryTimeout) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLUINTEGER timeoutSec = 1; + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_QUERY_TIMEOUT, (SQLPOINTER)(uintptr_t)timeoutSec, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR longQuery[] = + "SELECT COUNT(*) FROM AS_TABLE(ListMap(ListFromRange(1u, 100000000u), ($x)->(AsStruct($x AS v))))"; + ASSERT_EQ(SQLExecDirect(stmt, longQuery, SQL_NTS), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYT00")); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, AttrMaxRows) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_max_rows (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO test_max_rows (id) VALUES (1), (2)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + const SQLULEN maxRows = 1; + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, (SQLPOINTER)(uintptr_t)maxRows, 0), + stmt, SQL_HANDLE_STMT); + SQLULEN maxRowsOut; + ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, &maxRowsOut, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(maxRowsOut, maxRows); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT id FROM test_max_rows ORDER BY id", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, AttrNoScan) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLCHAR selectEscapeFnQuery[] = "SELECT {fn ABS(-12)} AS value"; + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 12); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_ON, 0), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), SQL_ERROR); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceConvert) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR convertQuery[] = "SELECT {fn CONVERT(42, SQL_SMALLINT)} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, convertQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLSMALLINT valueSmall = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_SSHORT, &valueSmall, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueSmall, 42); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceDouble) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR convertDoubleQuery[] = "SELECT {fn CONVERT(2.5, SQL_DOUBLE)} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, convertDoubleQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + double valueDouble = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_DOUBLE, &valueDouble, 0, &valueInd), SQL_SUCCESS); + ASSERT_LT(std::fabs(valueDouble - 2.5), 1e-9); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceNested) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR nestedFnQuery[] = "SELECT {fn {fn ABS(-10)}} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, nestedFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER valueInt = 0; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); + ASSERT_EQ(valueInt, 10); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceString) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR asciiLowerQuery[] = "SELECT {fn String::AsciiToLower('AbC')} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, asciiLowerQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[32] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "abc"); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceDate) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR dateQuery[] = "SELECT {d '2024-06-15'} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, dateQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[32] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "2024-06-15"); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, EscapeSequenceTimestamp) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), + stmt, SQL_HANDLE_STMT); + + SQLCHAR tsQuery[] = "SELECT {ts '2024-06-15 14:30:00'} AS value"; + CHECK_ODBC_OK(SQLExecDirect(stmt, tsQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char buf[64] = {}; + SQLLEN valueInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); + ASSERT_STREQ(buf, "2024-06-15 14:30:00"); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/stmt_attr_it.cpp b/odbc/tests/integration/stmt_attr_it.cpp deleted file mode 100644 index 89faf9abed0..00000000000 --- a/odbc/tests/integration/stmt_attr_it.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#include "test_utils.h" - -#include -#include -#include -#include - -#ifndef SQL_ATTR_METADATA_ID -#define SQL_ATTR_METADATA_ID 10029 -#endif - - -TEST(OdbcStmtAttr, QueryTimeoutAttr) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLUINTEGER timeoutSec = 1; - CHECK_ODBC_OK( - SQLSetStmtAttr(stmt, SQL_ATTR_QUERY_TIMEOUT, (SQLPOINTER)(uintptr_t)timeoutSec, 0), - stmt, - SQL_HANDLE_STMT); - - SQLCHAR longQuery[] = - "SELECT COUNT(*) FROM AS_TABLE(ListMap(ListFromRange(1u, 100000000u), ($x)->(AsStruct($x AS v))))"; - ASSERT_EQ(SQLExecDirect(stmt, longQuery, SQL_NTS), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYT00")); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcStmtAttr, MaxRowsAttr) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLCHAR dropQuery[] = "DROP TABLE IF EXISTS test_attr_max_rows"; - SQLCHAR createQuery[] = - "CREATE TABLE test_attr_max_rows (id Int32, value Int32, PRIMARY KEY (id))"; - SQLCHAR upsert1Query[] = "UPSERT INTO test_attr_max_rows (id, value) VALUES (1, 10)"; - SQLCHAR upsert2Query[] = "UPSERT INTO test_attr_max_rows (id, value) VALUES (2, 20)"; - SQLCHAR selectQuery[] = "SELECT value FROM test_attr_max_rows ORDER BY id"; - - CHECK_ODBC_OK(SQLExecDirect(stmt, dropQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, createQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, upsert1Query, SQL_NTS), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, upsert2Query, SQL_NTS), stmt, SQL_HANDLE_STMT); - - const SQLULEN maxRows = 1; - CHECK_ODBC_OK( - SQLSetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, (SQLPOINTER)(uintptr_t)maxRows, 0), - stmt, - SQL_HANDLE_STMT); - - SQLULEN maxRowsOut = 0; - ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_MAX_ROWS, &maxRowsOut, 0, nullptr), SQL_SUCCESS); - ASSERT_EQ(maxRowsOut, maxRows); - - CHECK_ODBC_OK(SQLExecDirect(stmt, selectQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcStmtAttr, NoScanAttr) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLCHAR selectEscapeFnQuery[] = "SELECT {fn ABS(-12)} AS value"; - - CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), stmt, SQL_HANDLE_STMT); - CHECK_ODBC_OK(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLINTEGER valueInt = 0; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueInt, 12); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_ON, 0), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLExecDirect(stmt, selectEscapeFnQuery, SQL_NTS), SQL_ERROR); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcStmtAttr, OdbcEscapeSequences) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_NOSCAN, (SQLPOINTER)SQL_NOSCAN_OFF, 0), stmt, SQL_HANDLE_STMT); - - { - SQLCHAR convertQuery[] = "SELECT {fn CONVERT(42, SQL_SMALLINT)} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, convertQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLSMALLINT valueSmall = 0; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_SSHORT, &valueSmall, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueSmall, 42); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - SQLCHAR convertDoubleQuery[] = "SELECT {fn CONVERT(2.5, SQL_DOUBLE)} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, convertDoubleQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - double valueDouble = 0; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_DOUBLE, &valueDouble, 0, &valueInd), SQL_SUCCESS); - ASSERT_LT(std::fabs(valueDouble - 2.5), 1e-9); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - SQLCHAR nestedFnQuery[] = "SELECT {fn {fn ABS(-10)}} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, nestedFnQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLINTEGER valueInt = 0; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &valueInt, 0, &valueInd), SQL_SUCCESS); - ASSERT_EQ(valueInt, 10); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - SQLCHAR asciiLowerQuery[] = "SELECT {fn String::AsciiToLower('AbC')} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, asciiLowerQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - char buf[32] = {}; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); - ASSERT_STREQ(buf, "abc"); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - SQLCHAR dateQuery[] = "SELECT {d '2024-06-15'} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, dateQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - char buf[32] = {}; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); - ASSERT_STREQ(buf, "2024-06-15"); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - SQLCHAR tsQuery[] = "SELECT {ts '2024-06-15 14:30:00'} AS value"; - CHECK_ODBC_OK(SQLExecDirect(stmt, tsQuery, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - char buf[64] = {}; - SQLLEN valueInd = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, buf, sizeof(buf), &valueInd), SQL_SUCCESS); - ASSERT_STREQ(buf, "2024-06-15 14:30:00"); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcStmtAttr, MetadataIdSqlLikeForTableNames) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLCHAR ddl[] = R"( - DROP TABLE IF EXISTS test_odbc_meta_like_a; - DROP TABLE IF EXISTS test_odbc_meta_like_b; - CREATE TABLE test_odbc_meta_like_a (id Int32, PRIMARY KEY (id)); - CREATE TABLE test_odbc_meta_like_b (id Int32, PRIMARY KEY (id)); - )"; - CHECK_ODBC_OK(SQLExecDirect(stmt, ddl, SQL_NTS), stmt, SQL_HANDLE_STMT); - - SQLULEN metadataId = SQL_TRUE; - ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); - ASSERT_EQ(metadataId, SQL_FALSE); - - const char* likePattern = "%/test_odbc_meta_like_%"; - CHECK_ODBC_OK( - SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), - stmt, - SQL_HANDLE_STMT); - int tableRows = 0; - while (SQLFetch(stmt) == SQL_SUCCESS) { - ++tableRows; - } - ASSERT_EQ(tableRows, 2); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - CHECK_ODBC_OK( - SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), - stmt, - SQL_HANDLE_STMT); - ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); - ASSERT_EQ(metadataId, SQL_TRUE); - - ASSERT_EQ( - SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), - SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - const std::string exactPath = "/local/test_odbc_meta_like_a"; - CHECK_ODBC_OK( - SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)exactPath.c_str(), SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), - stmt, - SQL_HANDLE_STMT); - tableRows = 0; - while (SQLFetch(stmt) == SQL_SUCCESS) { - ++tableRows; - } - ASSERT_EQ(tableRows, 1); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - - CHECK_ODBC_OK( - SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_FALSE, 0), - stmt, - SQL_HANDLE_STMT); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - -TEST(OdbcStmtAttr, MetadataIdSqlLikeForColumnNames) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - - SQLCHAR ddl[] = R"( - DROP TABLE IF EXISTS test_odbc_meta_col; - CREATE TABLE test_odbc_meta_col (id Int32, value_x Int32, PRIMARY KEY (id)); - )"; - CHECK_ODBC_OK(SQLExecDirect(stmt, ddl, SQL_NTS), stmt, SQL_HANDLE_STMT); - - constexpr SQLUSMALLINT kColumnNameCol = 4; - char colName[256] = {}; - SQLLEN colInd = 0; - const std::string exactTable = "/local/test_odbc_meta_col"; - - { - CHECK_ODBC_OK( - SQLColumns( - stmt, - nullptr, - 0, - nullptr, - 0, - (SQLCHAR*)"%/test_odbc_meta_col", - SQL_NTS, - (SQLCHAR*)"val%", - SQL_NTS), - stmt, - SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - ASSERT_EQ(SQLGetData(stmt, kColumnNameCol, SQL_C_CHAR, colName, sizeof(colName), &colInd), SQL_SUCCESS); - ASSERT_STREQ(colName, "value_x"); - ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - CHECK_ODBC_OK( - SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), - stmt, - SQL_HANDLE_STMT); - - ASSERT_EQ( - SQLColumns( - stmt, - nullptr, - 0, - nullptr, - 0, - (SQLCHAR*)"%/test_odbc_meta_col", - SQL_NTS, - (SQLCHAR*)"value_x", - SQL_NTS), - SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - { - ASSERT_EQ( - SQLColumns( - stmt, - nullptr, - 0, - nullptr, - 0, - (SQLCHAR*)exactTable.c_str(), - SQL_NTS, - (SQLCHAR*)"val%", - SQL_NTS), - SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "42S22")); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); -} - diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h index 950ffef9508..362a836991b 100644 --- a/odbc/tests/integration/test_utils.h +++ b/odbc/tests/integration/test_utils.h @@ -23,7 +23,7 @@ inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { #define CHECK_ODBC_OK(rc, handle, type) \ ASSERT_TRUE((rc) == SQL_SUCCESS || (rc) == SQL_SUCCESS_WITH_INFO) << GetOdbcError(handle, type) -inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;"; +inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Server=localhost:2136;Database=/local;"; inline bool SqlStatePrefix(const std::string& diag, const char* state5) { return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; @@ -41,3 +41,76 @@ inline void AllocEnvAndConnect(SQLHENV* env, SQLHDBC* dbc) { *dbc, nullptr, (SQLCHAR*)kConnStr, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); CHECK_ODBC_OK(rc, *dbc, SQL_HANDLE_DBC); } + +// ============================================================================ +// Type and Parameter Utilities +// ============================================================================ + +// Bind integer parameter and return result +inline SQLRETURN BindIntParam(SQLHSTMT stmt, SQLUSMALLINT paramNum, SQLINTEGER* value) { + return SQLBindParameter(stmt, paramNum, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, value, 0, nullptr); +} + +inline SQLRETURN BindInt64Param(SQLHSTMT stmt, SQLUSMALLINT paramNum, SQLBIGINT* value) { + return SQLBindParameter(stmt, paramNum, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, value, 0, nullptr); +} + +inline SQLRETURN BindStringParam(SQLHSTMT stmt, SQLUSMALLINT paramNum, char* value, SQLLEN len) { + SQLLEN indicator = (len >= 0) ? len : SQL_NTS; + return SQLBindParameter(stmt, paramNum, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, + 0, 0, value, (indicator == SQL_NTS) ? 0 : indicator, &indicator); +} + +inline SQLRETURN BindDoubleParam(SQLHSTMT stmt, SQLUSMALLINT paramNum, double* value) { + return SQLBindParameter(stmt, paramNum, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, value, 0, nullptr); +} + +inline SQLRETURN BindNullParam(SQLHSTMT stmt, SQLUSMALLINT paramNum, SQLINTEGER* placeholder) { + static SQLLEN nullIndicator = SQL_NULL_DATA; + return SQLBindParameter(stmt, paramNum, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, placeholder, 0, &nullIndicator); +} + +// Fetch and verify integer result +inline SQLINTEGER FetchIntResult(SQLHSTMT stmt, SQLUSMALLINT colNum = 1) { + SQLINTEGER result = 0; + SQLLEN indicator = 0; + SQLBindCol(stmt, colNum, SQL_C_LONG, &result, 0, &indicator); + SQLFetch(stmt); + return result; +} + +inline SQLBIGINT FetchInt64Result(SQLHSTMT stmt, SQLUSMALLINT colNum = 1) { + SQLBIGINT result = 0; + SQLLEN indicator = 0; + SQLBindCol(stmt, colNum, SQL_C_SBIGINT, &result, 0, &indicator); + SQLFetch(stmt); + return result; +} + +inline double FetchDoubleResult(SQLHSTMT stmt, SQLUSMALLINT colNum = 1) { + double result = 0.0; + SQLLEN indicator = 0; + SQLBindCol(stmt, colNum, SQL_C_DOUBLE, &result, 0, &indicator); + SQLFetch(stmt); + return result; +} + +inline std::string FetchStringResult(SQLHSTMT stmt, SQLUSMALLINT colNum = 1, size_t maxLen = 256) { + std::string result(maxLen, '\0'); + SQLLEN indicator = 0; + SQLBindCol(stmt, colNum, SQL_C_CHAR, &result[0], maxLen, &indicator); + SQLFetch(stmt); + if (indicator > 0 && indicator != SQL_NULL_DATA) { + result.resize(indicator); + } + return result; +} + +inline bool IsNullResult(SQLHSTMT stmt, SQLUSMALLINT colNum = 1) { + SQLINTEGER dummy; + SQLLEN indicator = 0; + SQLBindCol(stmt, colNum, SQL_C_LONG, &dummy, 0, &indicator); + SQLFetch(stmt); + return indicator == SQL_NULL_DATA; +} diff --git a/odbc/tests/integration/transaction_api_it.cpp b/odbc/tests/integration/transaction_api_it.cpp new file mode 100644 index 00000000000..b26953e4dd0 --- /dev/null +++ b/odbc/tests/integration/transaction_api_it.cpp @@ -0,0 +1,179 @@ +#include "test_utils.h" + +TEST(TransactionApi, AutocommitDefaultOn) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + + SQLUINTEGER autocommit; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, &autocommit, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(autocommit, SQL_AUTOCOMMIT_ON); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, AutocommitOnOffToggle) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), + dbc, SQL_HANDLE_DBC); + SQLUINTEGER autocommit; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, &autocommit, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(autocommit, SQL_AUTOCOMMIT_OFF); + + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0), + dbc, SQL_HANDLE_DBC); + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, &autocommit, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(autocommit, SQL_AUTOCOMMIT_ON); + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, AutocommitOffRollback) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_rollback", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_rollback (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO test_rollback (id, value) VALUES (1, 100)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_ROLLBACK), dbc, SQL_HANDLE_DBC); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT value FROM test_rollback WHERE id = 1", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, AutocommitOffCommit) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_commit", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_commit (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO test_commit (id, value) VALUES (1, 200)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT), dbc, SQL_HANDLE_DBC); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT value FROM test_commit WHERE id = 1", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER value; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &value, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(value, 200); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, MultipleStatementsInManualTransaction) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_multi_stmt", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_multi_stmt (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), + dbc, SQL_HANDLE_DBC); + for (int i = 1; i <= 5; ++i) { + char query[256]; + snprintf(query, sizeof(query), "UPSERT INTO test_multi_stmt (id, value) VALUES (%d, %d)", i, i * 10); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)query, SQL_NTS), stmt, SQL_HANDLE_STMT); + } + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT), dbc, SQL_HANDLE_DBC); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT COUNT(*) FROM test_multi_stmt", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER count; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &count, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(count, 5); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, SQLEndTranOnEnv) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_env_tran", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_env_tran (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO test_env_tran (id) VALUES (1)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLEndTran(SQL_HANDLE_ENV, env, SQL_COMMIT), env, SQL_HANDLE_ENV); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, SQLEndTranInvalid) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLEndTran(SQL_HANDLE_DBC, dbc, 999), SQL_ERROR); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, TxnIsolationDefault) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLUINTEGER isolation; + ASSERT_EQ(SQLGetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, &isolation, sizeof(isolation), nullptr), SQL_SUCCESS); + ASSERT_EQ(isolation, SQL_TXN_SERIALIZABLE); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(TransactionApi, TxnIsolationUnsupportedInReadWrite) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_READ_COMMITTED, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HYC00")); + ASSERT_EQ(SQLSetConnectAttr(dbc, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)9999, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "HY024")); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp index f4bad34a366..16edf49be21 100644 --- a/odbc/tests/unit/convert_ut.cpp +++ b/odbc/tests/unit/convert_ut.cpp @@ -128,3 +128,219 @@ TEST(OdbcConvert, StringNullToYdb) { CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: UTF8\n }\n}\n"); CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); } + +TEST(OdbcConvert, Int32ToYdb) { + SQLINTEGER v = 42; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: INT32\n }\n}\n"); + CheckProto(value->GetProto(), "int32_value: 42\n"); +} + +TEST(OdbcConvert, Int32NegativeToYdb) { + SQLINTEGER v = -999; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "int32_value: -999\n"); +} + +TEST(OdbcConvert, Int32ZeroToYdb) { + SQLINTEGER v = 0; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "int32_value: 0\n"); +} + +TEST(OdbcConvert, Int32MaxToYdb) { + SQLINTEGER v = 2147483647; // INT32_MAX + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "int32_value: 2147483647\n"); +} + +TEST(OdbcConvert, Int32NullToYdb) { + SQLINTEGER v = 42; + SQLLEN nullInd = SQL_NULL_DATA; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &v, sizeof(v), &nullInd + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); +} + +TEST(OdbcConvert, Int64NegativeToYdb) { + SQLBIGINT v = -123456789012345LL; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "int64_value: -123456789012345\n"); +} + +TEST(OdbcConvert, Int64ZeroToYdb) { + SQLBIGINT v = 0; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "int64_value: 0\n"); +} + +TEST(OdbcConvert, DoubleNegativeToYdb) { + SQLDOUBLE v = -2.71828; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); +} + +TEST(OdbcConvert, DoubleZeroToYdb) { + SQLDOUBLE v = 0.0; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &v, sizeof(v), nullptr + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "double_value: 0\n"); +} + +TEST(OdbcConvert, DoubleNullToYdb) { + SQLDOUBLE v = 3.14; + SQLLEN nullInd = SQL_NULL_DATA; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &v, sizeof(v), &nullInd + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); +} + +TEST(OdbcConvert, StringEmptyToYdb) { + const char* str = ""; + SQLLEN len = 0; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, len, &len + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "text_value: \"\"\n"); +} + +TEST(OdbcConvert, StringUnicodeToYdb) { + const char* str = "Привет"; + SQLLEN len = SQL_NTS; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, 0, &len + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); +} + +TEST(OdbcConvert, StringWithLengthToYdb) { + const char* str = "hello world"; + SQLLEN len = 5; // Only "hello" + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, len, &len + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "text_value: \"hello\"\n"); +} + +TEST(OdbcConvert, StringNullTerminatedToYdb) { + const char* str = "test"; + SQLLEN len = SQL_NTS; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 0, 0, (SQLPOINTER)str, 0, &len + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "text_value: \"test\"\n"); +} + + +TEST(OdbcConvert, BinaryNullToYdb) { + const char* data = "\x01\x02\x03"; + SQLLEN nullInd = SQL_NULL_DATA; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 0, 0, (SQLPOINTER)data, 3, &nullInd + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "null_flag_value: NULL_VALUE\n"); +} + +TEST(OdbcConvert, BinaryEmptyToYdb) { + const char* data = ""; + SQLLEN len = 0; + TBoundParam param{ + 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 0, 0, (SQLPOINTER)data, len, &len + }; + TParamsBuilder paramsBuilder; + ConvertParam(param, paramsBuilder.AddParam("$p1")); + auto params = paramsBuilder.Build(); + auto value = params.GetValue("$p1"); + ASSERT_TRUE(value); + CheckProto(value->GetProto(), "bytes_value: \"\"\n"); +} From cff231486f5684eebe46b11931d41330944482e5 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Thu, 7 May 2026 17:37:17 +0000 Subject: [PATCH 24/55] some metadata functions --- odbc/CMakeLists.txt | 2 +- odbc/src/get_info.h | 17 --- odbc/src/{get_info.cpp => metadata.cpp} | 113 +++++++++++++++++++- odbc/src/metadata.h | 33 ++++++ odbc/src/odbc_driver.cpp | 40 ++++++- odbc/src/statement.cpp | 39 ++++--- odbc/src/statement.h | 1 + odbc/src/utils/cursor.cpp | 37 +++---- odbc/src/utils/cursor.h | 1 + odbc/src/utils/types.cpp | 29 +++++ odbc/src/utils/types.h | 1 + odbc/tests/integration/statement_api_it.cpp | 2 + 12 files changed, 258 insertions(+), 57 deletions(-) delete mode 100644 odbc/src/get_info.h rename odbc/src/{get_info.cpp => metadata.cpp} (71%) create mode 100644 odbc/src/metadata.h diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index b819fbcf35f..5b1d735226b 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(ydb-odbc SHARED src/statement_attr.cpp src/statement.cpp src/environment.cpp - src/get_info.cpp + src/metadata.cpp ) target_include_directories(ydb-odbc diff --git a/odbc/src/get_info.h b/odbc/src/get_info.h deleted file mode 100644 index d4e05d88447..00000000000 --- a/odbc/src/get_info.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "connection.h" - -namespace NYdb::NOdbc { - -class TInfoProvider { -public: - static SQLRETURN GetInfo( - TConnection* conn, - SQLUSMALLINT infoType, - SQLPOINTER infoValuePtr, - SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr); -}; - -} // namespace NYdb::NOdbc diff --git a/odbc/src/get_info.cpp b/odbc/src/metadata.cpp similarity index 71% rename from odbc/src/get_info.cpp rename to odbc/src/metadata.cpp index 6f63f590230..2607408fca7 100644 --- a/odbc/src/get_info.cpp +++ b/odbc/src/metadata.cpp @@ -1,4 +1,4 @@ -#include "get_info.h" +#include "metadata.h" #include #include @@ -54,9 +54,49 @@ SQLRETURN WriteInfoScalar( return SQL_SUCCESS; } + +bool IsSupportedFunction(SQLUSMALLINT functionId) { + switch (functionId) { + case SQL_API_SQLALLOCHANDLE: + case SQL_API_SQLBINDCOL: + case SQL_API_SQLBINDPARAMETER: + case SQL_API_SQLCLOSECURSOR: + case SQL_API_SQLCOLUMNS: + case SQL_API_SQLCONNECT: + case SQL_API_SQLDESCRIBECOL: + case SQL_API_SQLDISCONNECT: + case SQL_API_SQLDRIVERCONNECT: + case SQL_API_SQLENDTRAN: + case SQL_API_SQLEXECDIRECT: + case SQL_API_SQLEXECUTE: + case SQL_API_SQLFETCH: + case SQL_API_SQLFETCHSCROLL: + case SQL_API_SQLFREEHANDLE: + case SQL_API_SQLFREESTMT: + case SQL_API_SQLGETDATA: + case SQL_API_SQLGETDIAGFIELD: + case SQL_API_SQLGETDIAGREC: + case SQL_API_SQLGETFUNCTIONS: + case SQL_API_SQLGETCONNECTATTR: + case SQL_API_SQLGETINFO: + case SQL_API_SQLGETSTMTATTR: + case SQL_API_SQLMORERESULTS: + case SQL_API_SQLNUMRESULTCOLS: + case SQL_API_SQLPREPARE: + case SQL_API_SQLROWCOUNT: + case SQL_API_SQLSETCONNECTATTR: + case SQL_API_SQLSETENVATTR: + case SQL_API_SQLSETSTMTATTR: + case SQL_API_SQLTABLES: + return true; + default: + return false; + } +} + } // namespace -SQLRETURN TInfoProvider::GetInfo( +SQLRETURN TMetadata::GetInfo( TConnection* conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, @@ -205,4 +245,73 @@ SQLRETURN TInfoProvider::GetInfo( } } + +SQLRETURN TMetadata::GetFunctions(SQLUSMALLINT functionId, SQLUSMALLINT* supportedPtr) { + if (!supportedPtr) { + return SQL_ERROR; + } + + if (functionId == SQL_API_ALL_FUNCTIONS) { + std::memset(supportedPtr, 0, 100 * sizeof(SQLUSMALLINT)); + for (SQLUSMALLINT id = 0; id < 100; ++id) { + if (IsSupportedFunction(id)) { + supportedPtr[id] = SQL_TRUE; + } + } + return SQL_SUCCESS; + } + + if (functionId == SQL_API_ODBC3_ALL_FUNCTIONS) { + std::memset(supportedPtr, 0, SQL_API_ODBC3_ALL_FUNCTIONS_SIZE * sizeof(SQLUSMALLINT)); + for (SQLUSMALLINT id = 0; id < SQL_API_ODBC3_ALL_FUNCTIONS_SIZE * 16; ++id) { + if (IsSupportedFunction(id)) { + supportedPtr[id >> 4] |= (1 << (id & 0x000F)); + } + } + return SQL_SUCCESS; + } + + *supportedPtr = IsSupportedFunction(functionId) ? SQL_TRUE : SQL_FALSE; + return SQL_SUCCESS; +} + +SQLRETURN TMetadata::DescribeCol( + TStatement* stmt, + SQLUSMALLINT columnNumber, + SQLCHAR* columnName, + SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr) { + const auto& columns = stmt->GetColumnMeta(); + if (columnNumber < 1 || columnNumber > columns.size()) { + throw TOdbcException("07009", 0, "Invalid descriptor index"); + } + + const auto& column = columns[columnNumber - 1]; + if (nameLengthPtr) { + *nameLengthPtr = static_cast(column.Name.size()); + } + if (columnName && bufferLength > 0) { + const auto copyLength = std::min(column.Name.size(), static_cast(bufferLength - 1)); + std::memcpy(columnName, column.Name.data(), copyLength); + columnName[copyLength] = '\0'; + } + if (dataTypePtr) { + *dataTypePtr = column.SqlType; + } + if (columnSizePtr) { + *columnSizePtr = column.Size; + } + if (decimalDigitsPtr) { + *decimalDigitsPtr = column.DecimalDigits; + } + if (nullablePtr) { + *nullablePtr = column.Nullable; + } + return SQL_SUCCESS; +} + } // namespace NYdb::NOdbc diff --git a/odbc/src/metadata.h b/odbc/src/metadata.h new file mode 100644 index 00000000000..7374e45e53a --- /dev/null +++ b/odbc/src/metadata.h @@ -0,0 +1,33 @@ +#pragma once + +#include "connection.h" +#include "statement.h" + +namespace NYdb::NOdbc { + +class TMetadata { +public: + static SQLRETURN GetInfo( + TConnection* conn, + SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr); + + static SQLRETURN GetFunctions( + SQLUSMALLINT functionId, + SQLUSMALLINT* supportedPtr); + + static SQLRETURN DescribeCol( + TStatement* stmt, + SQLUSMALLINT columnNumber, + SQLCHAR* columnName, + SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr); +}; + +} // namespace NYdb::NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 63b07dd256e..3d9a2019c3a 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -1,7 +1,7 @@ #include "environment.h" #include "connection.h" #include "statement.h" -#include "get_info.h" +#include "metadata.h" #include "utils/util.h" #include "utils/error_manager.h" @@ -17,6 +17,7 @@ namespace { } return static_cast(handle); } + } extern "C" { @@ -393,6 +394,41 @@ SQLRETURN SQL_API SQLNumResultCols(SQLHSTMT statementHandle, SQLSMALLINT* colCou }); } +SQLRETURN SQL_API SQLDescribeCol( + SQLHSTMT statementHandle, + SQLUSMALLINT columnNumber, + SQLCHAR* columnName, + SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return NYdb::NOdbc::TMetadata::DescribeCol( + stmt, + columnNumber, + columnName, + bufferLength, + nameLengthPtr, + dataTypePtr, + columnSizePtr, + decimalDigitsPtr, + nullablePtr); + }); +} + +SQLRETURN SQL_API SQLMoreResults(SQLHSTMT) { + // YDB ODBC currently exposes only one result set per statement. + return SQL_NO_DATA; +} + +SQLRETURN SQL_API SQLGetFunctions(SQLHDBC connectionHandle, SQLUSMALLINT functionId, SQLUSMALLINT* supportedPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto*) { + return NYdb::NOdbc::TMetadata::GetFunctions(functionId, supportedPtr); + }); +} + SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT statementHandle, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength) { return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { return stmt->SetStmtAttr(attribute, value, stringLength); @@ -416,7 +452,7 @@ SQLRETURN SQL_API SQLGetInfo(SQLHDBC connectionHandle, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { - return NYdb::NOdbc::TInfoProvider::GetInfo(conn, infoType, infoValuePtr, bufferLength, stringLengthPtr); + return NYdb::NOdbc::TMetadata::GetInfo(conn, infoType, infoValuePtr, bufferLength, stringLengthPtr); }); } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index b32355954cc..ff89a0d9e1b 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -108,8 +108,6 @@ SQLRETURN TStatement::Execute() { } else { Cursor_.reset(); } - IsPrepared_ = false; - PreparedQuery_.clear(); return SQL_SUCCESS; } @@ -317,23 +315,23 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, auto columns = result.GetTableDescription().GetTableColumns(); - auto columnIt = std::find_if(columns.begin(), columns.end(), [&](const NTable::TTableColumn& column) { + auto columnMatches = [&](const NTable::TTableColumn& column) { + if (columnName.empty()) { + return true; + } if (Attributes_.GetMetadataId() == SQL_TRUE) { return column.Name == columnName; } - if (columnName.empty()) { - return column.Name.empty(); - } return SqlLikeMatch(column.Name, columnName); - }); - - if (columnIt == columns.end()) { - throw TOdbcException("42S22", 0, "Column not found", SQL_ERROR); - } + }; - auto column = *columnIt; - - TTypeParser typeParser(column.Type); + bool foundColumn = false; + for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) { + const auto& column = columns[columnIndex]; + if (!columnMatches(column)) { + continue; + } + foundColumn = true; table.push_back({ TValueBuilder().OptionalUtf8(std::nullopt).Build(), @@ -352,9 +350,13 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, TValueBuilder().Int16(GetTypeId(column.Type)).Build(), TValueBuilder().OptionalInt16(std::nullopt).Build(), TValueBuilder().OptionalInt32(8).Build(), - TValueBuilder().OptionalInt32(columnIt - columns.begin() + 1).Build(), + TValueBuilder().OptionalInt32(columnIndex + 1).Build(), TValueBuilder().Utf8(column.NotNull && *column.NotNull ? "NO" : "YES").Build(), }); + } + if (!foundColumn) { + throw TOdbcException("42S22", 0, "Column not found", SQL_ERROR); + } return TStatus(EStatus::SUCCESS, {}); }); @@ -494,8 +496,6 @@ SQLRETURN TStatement::Close(bool force) { Cursor_.reset(); RowsFetched_ = 0; - PreparedQuery_.clear(); - IsPrepared_ = false; ClearErrors(); return SQL_SUCCESS; } @@ -529,6 +529,11 @@ SQLRETURN TStatement::NumResultCols(SQLSMALLINT* colCount) { return SQL_SUCCESS; } +const std::vector& TStatement::GetColumnMeta() const { + static const std::vector EmptyColumns; + return Cursor_ ? Cursor_->GetColumnMeta() : EmptyColumns; +} + SQLRETURN TStatement::SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength) { return Attributes_.SetStmtAttr(attr, value, stringLength, *this); } diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 9f2eb8ade64..f3ecd017562 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -52,6 +52,7 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN RowCount(SQLLEN* rowCount); SQLRETURN NumResultCols(SQLSMALLINT* colCount); + const std::vector& GetColumnMeta() const; SQLRETURN SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength); SQLRETURN GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index 26ad393b03a..aa2268c21e1 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -15,7 +15,11 @@ class TExecCursor : public ICursor { : BindingFiller_(bindingFiller) , Iterator_(std::move(iterator)) , PrefetchedPart_(std::move(prefetchedPart)) - {} + { + if (PrefetchedPart_ && PrefetchedPart_->HasResultSet()) { + FillColumnsMeta(PrefetchedPart_->GetResultSet()); + } + } bool Fetch() override { while (true) { @@ -42,17 +46,10 @@ class TExecCursor : public ICursor { return false; } if (part.HasResultSet()) { - TResultSet rs = part.ExtractResultSet(); + TResultSet resultSet = part.ExtractResultSet(); Columns_.clear(); - Columns_.reserve(rs.ColumnsCount()); - for (const auto& col : rs.GetColumnsMeta()) { - Columns_.push_back(TColumnMeta{ - col.Name, - GetTypeId(col.Type), - 0, - IsNullable(col.Type)}); - } - ResultSetParser_ = std::make_unique(rs); + FillColumnsMeta(resultSet); + ResultSetParser_ = std::make_unique(resultSet); } } return false; @@ -74,13 +71,17 @@ class TExecCursor : public ICursor { } private: - // void GetNextPart() { - // auto part = Iterator_.ReadNext().ExtractValueSync(); - // while (!part.EOS() && part.IsSuccess() && !part.HasResultSet()) { - // part = Iterator_.ReadNext().ExtractValueSync(); - // } - // Part_ = std::move(part); - // } + void FillColumnsMeta(const TResultSet& resultSet) { + for (const auto& col : resultSet.GetColumnsMeta()) { + const SQLSMALLINT sqlType = GetTypeId(col.Type); + Columns_.push_back(TColumnMeta{ + col.Name, + sqlType, + GetColumnSize(sqlType), + IsNullable(col.Type), + GetDecimalDigits(col.Type).value_or(0)}); + } + } IBindingFiller* BindingFiller_; NQuery::TExecuteQueryIterator Iterator_; diff --git a/odbc/src/utils/cursor.h b/odbc/src/utils/cursor.h index 22828f66144..a44fc02e340 100644 --- a/odbc/src/utils/cursor.h +++ b/odbc/src/utils/cursor.h @@ -18,6 +18,7 @@ struct TColumnMeta { SQLSMALLINT SqlType; SQLULEN Size; SQLSMALLINT Nullable; + SQLSMALLINT DecimalDigits = 0; }; using TTable = std::vector>; diff --git a/odbc/src/utils/types.cpp b/odbc/src/utils/types.cpp index 5174101b760..3c9c70549fa 100644 --- a/odbc/src/utils/types.cpp +++ b/odbc/src/utils/types.cpp @@ -84,6 +84,35 @@ SQLSMALLINT IsNullable(const TType& type) { return SQL_NO_NULLS; } +SQLULEN GetColumnSize(SQLSMALLINT sqlType) { + switch (sqlType) { + case SQL_BIT: + return 1; + case SQL_TINYINT: + return 3; + case SQL_SMALLINT: + return 5; + case SQL_INTEGER: + return 10; + case SQL_BIGINT: + return 20; + case SQL_REAL: + return 7; + case SQL_DOUBLE: + return 15; + case SQL_TYPE_DATE: + return 10; + case SQL_TYPE_TIMESTAMP: + return 26; + case SQL_GUID: + return 36; + case SQL_VARCHAR: + case SQL_VARBINARY: + default: + return 4096; + } +} + std::optional GetDecimalDigits(const TType& type) { TTypeParser typeParser(type); if (typeParser.GetKind() != TTypeParser::ETypeKind::Primitive) { diff --git a/odbc/src/utils/types.h b/odbc/src/utils/types.h index 73a0a32bb13..0decb215fe3 100644 --- a/odbc/src/utils/types.h +++ b/odbc/src/utils/types.h @@ -10,6 +10,7 @@ namespace NOdbc { SQLSMALLINT GetTypeId(const TType& type); SQLSMALLINT IsNullable(const TType& type); +SQLULEN GetColumnSize(SQLSMALLINT sqlType); std::optional GetDecimalDigits(const TType& type); std::optional GetRadix(const TType& type); diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index df0bb22d6ab..1413e40229a 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -392,6 +392,8 @@ TEST(StatementApi, AttrMaxRows) { SQLHSTMT stmt; AllocEnvAndConnect(&env, &dbc); ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_max_rows", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"CREATE TABLE test_max_rows (id Int32, PRIMARY KEY (id))", SQL_NTS), stmt, SQL_HANDLE_STMT); SQLFreeStmt(stmt, SQL_CLOSE); From 82b3bb65c7a9414361a0ed7b842752f051cdb006 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Thu, 7 May 2026 18:09:46 +0000 Subject: [PATCH 25/55] erlang odbc client --- examples/erlang_odbc_client/Makefile | 50 ++++ examples/erlang_odbc_client/README.md | 30 +++ .../erlang_odbc_client/src/sample_data.erl | 64 +++++ .../src/ydb_series_client.erl | 223 ++++++++++++++++++ odbc/src/utils/convert.cpp | 17 ++ 5 files changed, 384 insertions(+) create mode 100644 examples/erlang_odbc_client/Makefile create mode 100644 examples/erlang_odbc_client/README.md create mode 100644 examples/erlang_odbc_client/src/sample_data.erl create mode 100644 examples/erlang_odbc_client/src/ydb_series_client.erl diff --git a/examples/erlang_odbc_client/Makefile b/examples/erlang_odbc_client/Makefile new file mode 100644 index 00000000000..5f402ef9720 --- /dev/null +++ b/examples/erlang_odbc_client/Makefile @@ -0,0 +1,50 @@ +.PHONY: all compile run run-shell clean distclean check help + +CONN ?= Driver=YDB;Endpoint=localhost:2136;Database=/local; +ERLC ?= erlc +ERL ?= erl + +all: compile + +help: + @echo "YDB Series Example - Erlang ODBC" + @echo "" + @echo "Targets:" + @echo " make compile Compile Erlang modules" + @echo " make run Run the example" + @echo " make run CONN='...' Run with a custom connection string" + @echo " make run-shell Start Erlang shell with compiled modules" + @echo " make check Check Erlang ODBC availability" + @echo " make clean Remove compiled files" + @echo "" + @echo "Examples:" + @echo " make run" + @echo " make run CONN=\"Driver=YDB;Endpoint=myhost:2136;Database=/mydb;\"" + +prepare: + @mkdir -p ebin + +compile: prepare + @echo "Compiling Erlang modules..." + $(ERLC) -o ebin src/*.erl + @echo "Done." + +run: compile + @echo "Running YDB Series Example..." + $(ERL) -pa ebin -noshell -eval 'application:load(odbc), application:start(odbc), ydb_series_client:run("$(CONN)"), halt().' + +run-shell: compile + @echo "Starting Erlang shell with ydb_series_client..." + $(ERL) -pa ebin + +check: + @echo "Checking ODBC support in Erlang..." + @$(ERL) -noshell -eval 'application:load(odbc), io:format("~p~n", [application:start(odbc)]), halt().' + +clean: + @rm -rf ebin/*.beam + @rm -rf *.beam + @rm -rf erl_crash.dump + +distclean: clean + @rm -rf ebin diff --git a/examples/erlang_odbc_client/README.md b/examples/erlang_odbc_client/README.md new file mode 100644 index 00000000000..a296e9edd70 --- /dev/null +++ b/examples/erlang_odbc_client/README.md @@ -0,0 +1,30 @@ +# Erlang ODBC Series Example + +Минимальный Erlang-клиент для YDB ODBC. Повторяет основной сценарий C++ `basic_example`: создает таблицы `series`, `seasons`, `episodes`, заполняет их тестовыми данными, выполняет несколько запросов и удаляет таблицы. + +## Требования + +- Erlang/OTP с модулем `odbc` +- unixODBC +- Собранный и зарегистрированный YDB ODBC driver +- Запущенная YDB, доступная по строке подключения + +Проверить регистрацию драйвера: + +```bash +odbcinst -q -d +``` + +## Запуск + +По умолчанию используется `Driver=YDB;Endpoint=localhost:2136;Database=/local;`. + +```bash +make run +``` + +С другой строкой подключения: + +```bash +make run CONN='...' +``` diff --git a/examples/erlang_odbc_client/src/sample_data.erl b/examples/erlang_odbc_client/src/sample_data.erl new file mode 100644 index 00000000000..5fccdce72ee --- /dev/null +++ b/examples/erlang_odbc_client/src/sample_data.erl @@ -0,0 +1,64 @@ +-module(sample_data). +-export([series/0, seasons/0, episodes/0, format_date/1]). + +series() -> + [ + [1, "IT Crowd", "The IT Crowd is a British sitcom by Channel 4.", days_from_date({2006, 2, 3})], + [2, "Silicon Valley", "Silicon Valley is an American comedy series.", days_from_date({2014, 4, 6})] + ]. + +seasons() -> + [ + [1, 1, "Season 1", days_from_date({2006, 2, 3}), days_from_date({2006, 5, 5})], + [1, 2, "Season 2", days_from_date({2007, 8, 24}), days_from_date({2007, 11, 16})], + [1, 3, "Season 3", days_from_date({2008, 11, 21}), days_from_date({2008, 12, 26})], + [1, 4, "Season 4", days_from_date({2010, 6, 25}), days_from_date({2010, 7, 30})], + [2, 1, "Season 1", days_from_date({2014, 4, 6}), days_from_date({2014, 6, 15})], + [2, 2, "Season 2", days_from_date({2015, 4, 12}), days_from_date({2015, 6, 14})], + [2, 3, "Season 3", days_from_date({2016, 4, 24}), days_from_date({2016, 6, 26})], + [2, 4, "Season 4", days_from_date({2017, 4, 23}), days_from_date({2017, 6, 25})], + [2, 5, "Season 5", days_from_date({2018, 3, 25}), days_from_date({2018, 5, 13})], + [2, 6, "Season 6", days_from_date({2019, 10, 27}), days_from_date({2019, 12, 8})] + ]. + +episodes() -> + [ + [1, 1, 1, "Yesterday's Jam", days_from_date({2006, 2, 3})], + [1, 1, 2, "Calamity Jen", days_from_date({2006, 2, 10})], + [1, 1, 3, "Fifty-Fifty", days_from_date({2006, 2, 17})], + [1, 1, 4, "The Red Door", days_from_date({2006, 2, 24})], + [1, 1, 5, "The Haunting of Bill Crouse", days_from_date({2006, 3, 3})], + [1, 1, 6, "Aunt Irma Visits", days_from_date({2006, 3, 10})], + [1, 2, 1, "The Work Outing", days_from_date({2007, 8, 24})], + [1, 2, 2, "Return of the Golden Child", days_from_date({2007, 8, 31})], + [1, 2, 3, "Moss and the German", days_from_date({2007, 9, 7})], + [2, 1, 1, "Minimum Viable Product", days_from_date({2014, 4, 6})], + [2, 1, 2, "The Cap Table", days_from_date({2014, 4, 13})], + [2, 1, 3, "Articles of Incorporation", days_from_date({2014, 4, 20})], + [2, 1, 4, "Fiduciary Duties", days_from_date({2014, 4, 27})], + [2, 1, 5, "Signaling Risk", days_from_date({2014, 5, 4})], + [2, 3, 1, "Founder Friendly", days_from_date({2016, 4, 24})], + [2, 3, 2, "Two in the Box", days_from_date({2016, 5, 1})], + [2, 3, 3, "Meinertzhagen's Haversack", days_from_date({2016, 5, 8})], + [2, 3, 4, "Maleant Data Systems Solutions", days_from_date({2016, 5, 15})], + [2, 5, 1, "Grow Fast or Die Slow", days_from_date({2018, 3, 25})], + [2, 5, 2, "Reorientation", days_from_date({2018, 4, 1})], + [2, 5, 3, "Chief Operating Officer", days_from_date({2018, 4, 8})], + [2, 5, 4, "Tech Evangelist", days_from_date({2018, 4, 15})], + [2, 5, 5, "Facial Recognition", days_from_date({2018, 4, 22})], + [2, 6, 1, "Artificial Emotional Intelligence", days_from_date({2019, 10, 27})], + [2, 6, 2, "Blood Money", days_from_date({2019, 11, 3})], + [2, 6, 3, "Hooli Smokes!", days_from_date({2019, 11, 10})], + [2, 6, 4, "Maximizing Alphaness", days_from_date({2019, 11, 17})], + [2, 6, 5, "Tethics", days_from_date({2019, 11, 24})], + [2, 6, 6, "RussFest", days_from_date({2019, 12, 1})], + [2, 6, 7, "Exit Event", days_from_date({2019, 12, 8})] + ]. + +days_from_date({Year, Month, Day}) -> + calendar:date_to_gregorian_days(Year, Month, Day) - calendar:date_to_gregorian_days(1970, 1, 1). + +format_date(Days) -> + Date = calendar:gregorian_days_to_date(Days + calendar:date_to_gregorian_days(1970, 1, 1)), + {Year, Month, Day} = Date, + io_lib:format("~4..0B-~2..0B-~2..0B", [Year, Month, Day]). diff --git a/examples/erlang_odbc_client/src/ydb_series_client.erl b/examples/erlang_odbc_client/src/ydb_series_client.erl new file mode 100644 index 00000000000..7b52b6687b5 --- /dev/null +++ b/examples/erlang_odbc_client/src/ydb_series_client.erl @@ -0,0 +1,223 @@ +-module(ydb_series_client). +-export([run/0, run/1, run_with_dsn/1]). + +run() -> + ConnectionString = "Driver=YDB;Endpoint=localhost:2136;Database=/local;", + run(ConnectionString). + +run(ConnectionString) when is_list(ConnectionString) -> + io:format("=== ODBC YDB Series Example ===~n"), + + application:load(odbc), + application:start(odbc), + + case odbc:connect(ConnectionString, [{tuple_format, list}]) of + {ok, Ref} -> + Result = run_example(Ref), + odbc:disconnect(Ref), + Result; + {error, Reason} -> + io:format("Connection failed: ~p~n", [Reason]), + error + end. + +run_with_dsn(DSN) -> + ConnectionString = lists:flatten(io_lib:format("DSN=~s;", [DSN])), + run(ConnectionString). + +run_example(Ref) -> + try + drop_tables(Ref), + create_tables(Ref), + fill_table_data(Ref), + select_simple(Ref), + upsert_simple(Ref), + select_with_params(Ref), + multistep(Ref), + select_seasons_by_series(Ref), + drop_tables(Ref), + + io:format("Completed successfully~n"), + ok + catch + Class:Reason:Stacktrace -> + io:format("~nError: ~p:~p~n", [Class, Reason]), + io:format("Stacktrace: ~p~n", [Stacktrace]), + error + end. + +create_tables(Ref) -> + Tables = [ + {"CREATE TABLE series ( + series_id Uint64, + title Utf8, + series_info Utf8, + release_date Uint64, + PRIMARY KEY (series_id) + );"}, + {"CREATE TABLE seasons ( + series_id Uint64, + season_id Uint64, + title Utf8, + first_aired Uint64, + last_aired Uint64, + PRIMARY KEY (series_id, season_id) + );"}, + {"CREATE TABLE episodes ( + series_id Uint64, + season_id Uint64, + episode_id Uint64, + title Utf8, + air_date Uint64, + PRIMARY KEY (series_id, season_id, episode_id) + );"} + ], + + lists:foreach(fun({Query}) -> + execute_update(Ref, Query) + end, Tables). + +fill_table_data(Ref) -> + SeriesData = sample_data:series(), + SeasonsData = sample_data:seasons(), + EpisodesData = sample_data:episodes(), + + lists:foreach(fun(Row) -> + [Id, Title, Info, Date] = Row, + Query = io_lib:format( + "UPSERT INTO series (series_id, title, series_info, release_date) VALUES (~p, \"~s\", \"~s\", ~p);", + [Id, escape_string(Title), escape_string(Info), Date] + ), + execute_update(Ref, Query) + end, SeriesData), + + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, Title, FirstAired, LastAired] = Row, + Query = io_lib:format( + "UPSERT INTO seasons (series_id, season_id, title, first_aired, last_aired) VALUES (~p, ~p, \"~s\", ~p, ~p);", + [SeriesId, SeasonId, escape_string(Title), FirstAired, LastAired] + ), + execute_update(Ref, Query) + end, SeasonsData), + + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, EpisodeId, Title, AirDate] = Row, + Query = io_lib:format( + "UPSERT INTO episodes (series_id, season_id, episode_id, title, air_date) VALUES (~p, ~p, ~p, \"~s\", ~p);", + [SeriesId, SeasonId, EpisodeId, escape_string(Title), AirDate] + ), + execute_update(Ref, Query) + end, EpisodesData), + + io:format("Inserted ~p series, ~p seasons, ~p episodes~n", + [length(SeriesData), length(SeasonsData), length(EpisodesData)]). + +select_simple(Ref) -> + Query = "SELECT CAST(series_id AS Utf8) AS series_id, title, CAST(release_date AS Date) AS release_date FROM series WHERE series_id = 1;", + Rows = selected_rows(Ref, select_simple, Query), + lists:foreach(fun(Row) -> + [Id, Title, ReleaseDate] = row_values(Row), + io:format("Series: Id=~p, Title=~p, Release=~p~n", [Id, Title, ReleaseDate]) + end, Rows). + +upsert_simple(Ref) -> + Query = "UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES (2, 6, 1, \"TBD\");", + execute_update(Ref, Query). + +select_with_params(Ref) -> + SeriesId = 2, + SeasonId = 3, + + Query = + "SELECT sa.title AS season_title, sr.title AS series_title " + "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id " + "WHERE sa.series_id = CAST($p1 AS Uint64) AND sa.season_id = CAST($p2 AS Uint64);", + Params = [{sql_integer, [SeriesId]}, {sql_integer, [SeasonId]}], + + Rows = selected_param_rows(Ref, select_with_params, Query, Params), + lists:foreach(fun(Row) -> + [SeasonTitle, SeriesTitle] = row_values(Row), + io:format("Season: ~p (Series: ~p)~n", [SeasonTitle, SeriesTitle]) + end, Rows). + +multistep(Ref) -> + SeriesId = 2, + SeasonId = 5, + + Query1 = io_lib:format( + "SELECT CAST(first_aired AS Utf8) AS first_aired FROM seasons WHERE series_id = ~p AND season_id = ~p;", + [SeriesId, SeasonId] + ), + + [FirstAiredRow] = selected_rows(Ref, multistep_step1, Query1), + [Date] = row_values(FirstAiredRow), + FromDate = list_to_integer(Date), + + ToDate = FromDate + 15, + + Query2 = io_lib:format( + "SELECT CAST(season_id AS Utf8) AS season_id, CAST(episode_id AS Utf8) AS episode_id, title, CAST(air_date AS Utf8) AS air_date FROM episodes " + "WHERE series_id = ~p AND air_date >= ~p AND air_date <= ~p;", + [SeriesId, FromDate, ToDate] + ), + + Rows = selected_rows(Ref, multistep_step2, Query2), + lists:foreach(fun(Row) -> + [SId, EId, Title, AirDate] = row_values(Row), + io:format("Episode: S~pE~p ~p (aired: ~p)~n", [SId, EId, Title, AirDate]) + end, Rows). + +select_seasons_by_series(Ref) -> + SeriesList = [1, 2], + InClause = string:join([integer_to_list(X) || X <- SeriesList], ", "), + + Query = io_lib:format( + "SELECT CAST(series_id AS Utf8) AS series_id, CAST(season_id AS Utf8) AS season_id, title, CAST(first_aired AS Date) AS first_aired " + "FROM seasons WHERE series_id IN (~s) ORDER BY season_id;", + [InClause] + ), + + Rows = selected_rows(Ref, select_seasons_by_series, Query), + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, Title, FirstAired] = row_values(Row), + io:format("Season: Series=~p, Season=~p, Title=~p, FirstAired=~p~n", + [SeriesId, SeasonId, Title, FirstAired]) + end, Rows). + +drop_tables(Ref) -> + Tables = ["series", "seasons", "episodes"], + + lists:foreach(fun(Table) -> + Query = io_lib:format("DROP TABLE ~s;", [Table]), + case odbc:sql_query(Ref, lists:flatten(Query)) of + {updated, _} -> ok; + {error, _} -> ok + end + end, Tables). + +escape_string(String) -> + EscapedBackslash = string:replace(String, "\\", "\\\\", all), + lists:flatten(string:replace(EscapedBackslash, "\"", "\\\"", all)). + +execute_update(Ref, Query) -> + case odbc:sql_query(Ref, lists:flatten(Query)) of + {updated, _} -> ok; + Error -> throw({query_failed, update, Error}) + end. + +selected_rows(Ref, Step, Query) -> + case odbc:sql_query(Ref, lists:flatten(Query)) of + {selected, _, Rows} -> Rows; + Error -> throw({query_failed, Step, Error}) + end. + +selected_param_rows(Ref, Step, Query, Params) -> + case odbc:param_query(Ref, lists:flatten(Query), Params) of + {selected, _, Rows} -> Rows; + Error -> throw({query_failed, Step, Error}) + end. + +row_values(Row) when is_tuple(Row) -> + tuple_to_list(Row); +row_values(Row) -> + Row. diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 1349e677c62..a19845d62e3 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -16,6 +16,7 @@ template<> struct TSqlTypeTraits { using Type = std::string; }; template<> struct TSqlTypeTraits { using Type = SQLBIGINT; }; template<> struct TSqlTypeTraits { using Type = SQLUBIGINT; }; template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; +template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; template<> struct TSqlTypeTraits { using Type = SQLUINTEGER; }; template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; @@ -155,6 +156,10 @@ REGISTER_CONVERTER(SQL_C_LONG, SQL_BIGINT, EPrimitiveType::Int64) { builder.OptionalInt64(static_cast(data)); } +REGISTER_CONVERTER(SQL_C_SLONG, SQL_BIGINT, EPrimitiveType::Int64) { + builder.OptionalInt64(static_cast(data)); +} + REGISTER_CONVERTER(SQL_C_SHORT, SQL_BIGINT, EPrimitiveType::Int64) { builder.OptionalInt64(static_cast(data)); } @@ -187,6 +192,10 @@ REGISTER_CONVERTER(SQL_C_LONG, SQL_INTEGER, EPrimitiveType::Int32) { builder.OptionalInt32(static_cast(data)); } +REGISTER_CONVERTER(SQL_C_SLONG, SQL_INTEGER, EPrimitiveType::Int32) { + builder.OptionalInt32(static_cast(data)); +} + REGISTER_CONVERTER(SQL_C_SHORT, SQL_INTEGER, EPrimitiveType::Int32) { builder.OptionalInt32(static_cast(data)); } @@ -219,6 +228,10 @@ REGISTER_CONVERTER(SQL_C_LONG, SQL_SMALLINT, EPrimitiveType::Int16) { builder.OptionalInt16(static_cast(data)); } +REGISTER_CONVERTER(SQL_C_SLONG, SQL_SMALLINT, EPrimitiveType::Int16) { + builder.OptionalInt16(static_cast(data)); +} + REGISTER_CONVERTER(SQL_C_SHORT, SQL_SMALLINT, EPrimitiveType::Int16) { builder.OptionalInt16(static_cast(data)); } @@ -251,6 +264,10 @@ REGISTER_CONVERTER(SQL_C_LONG, SQL_TINYINT, EPrimitiveType::Int8) { builder.OptionalInt8(static_cast(data)); } +REGISTER_CONVERTER(SQL_C_SLONG, SQL_TINYINT, EPrimitiveType::Int8) { + builder.OptionalInt8(static_cast(data)); +} + REGISTER_CONVERTER(SQL_C_SHORT, SQL_TINYINT, EPrimitiveType::Int8) { builder.OptionalInt8(static_cast(data)); } From 0c33d8b62ea930386c47caed894fbdb5e058291d Mon Sep 17 00:00:00 2001 From: Ylonies Date: Tue, 12 May 2026 20:33:10 +0300 Subject: [PATCH 26/55] fixes --- odbc/CMakeLists.txt | 4 +- odbc/src/connection.cpp | 7 ++ odbc/src/environment.cpp | 3 + odbc/src/statement.cpp | 90 ++++++++++++++------ odbc/src/statement.h | 2 +- odbc/src/utils/convert.cpp | 3 +- odbc/src/utils/convert.h | 3 +- odbc/src/utils/cursor.h | 1 + odbc/src/utils/error_manager.cpp | 87 +++++++++---------- odbc/src/utils/error_manager.h | 2 + odbc/src/utils/types.h | 1 + odbc/tests/integration/error_handling_it.cpp | 8 +- odbc/tests/integration/metadata_api_it.cpp | 12 ++- 13 files changed, 139 insertions(+), 84 deletions(-) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index 5b1d735226b..e5fef46a7f0 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -36,6 +36,8 @@ set_target_properties(ydb-odbc PROPERTIES POSITION_INDEPENDENT_CODE ON ) +include(GNUInstallDirs) + install(TARGETS ydb-odbc LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) @@ -47,8 +49,6 @@ install(DIRECTORY include/ add_subdirectory(examples) add_subdirectory(tests) -include(GNUInstallDirs) - install(FILES odbcinst.ini DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 8a3e33db59e..de976892020 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -211,6 +212,9 @@ void TConnection::ResetQuerySession() { } SQLRETURN TConnection::CommitTx() { + if (!Tx_) { + return AddError("25000", 0, "Invalid transaction state: no active transaction"); + } auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); @@ -218,6 +222,9 @@ SQLRETURN TConnection::CommitTx() { } SQLRETURN TConnection::RollbackTx() { + if (!Tx_) { + return AddError("25000", 0, "Invalid transaction state: no active transaction"); + } auto status = Tx_->Rollback().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index 44e3473d023..8df0949faa8 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -1,6 +1,9 @@ #include "environment.h" #include "connection.h" + #include + #include + namespace NYdb { namespace NOdbc { diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index ff89a0d9e1b..b04265be334 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -15,12 +15,41 @@ #include #include -#include +#include namespace NYdb { namespace NOdbc { namespace { + + bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen) { + if (sLen < prefixLen) { + return false; + } + for (size_t i = 0; i < prefixLen; ++i) { + if (std::tolower(static_cast(s[i])) != + std::tolower(static_cast(prefix[i]))) { + return false; + } + } + return true; + } + + bool IsDdlQuery(const std::string& queryText) { + size_t pos = 0; + while (pos < queryText.size() && std::isspace(static_cast(queryText[pos]))) { + ++pos; + } + if (queryText.size() - pos < 6) { + return false; + } + const char* start = queryText.c_str() + pos; + const size_t remaining = queryText.size() - pos; + return StartsWithPrefix(start, remaining, "CREATE", 6) || + StartsWithPrefix(start, remaining, "DROP", 4) || + StartsWithPrefix(start, remaining, "ALTER", 5); + } + NYdb::TStatus StatusFrom(const NYdb::TStatus& ydb_status) { return NYdb::TStatus(ydb_status.GetStatus(), NYdb::NIssue::TIssues(ydb_status.GetIssues())); } @@ -68,7 +97,11 @@ SQLRETURN TStatement::Execute() { if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } - NYdb::TParams params = BuildParams(); + NYdb::TParams params = NYdb::TParamsBuilder().Build(); + const SQLRETURN buildRc = BuildParams(params); + if (buildRc != SQL_SUCCESS) { + return buildRc; + } std::optional iterator; std::optional prefetchedResultPart; @@ -135,19 +168,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession // DDL must use NoTx() per YDB documentation const bool isSnapshotRw = (txSettings.GetMode() == NQuery::TTxSettings::TS_SNAPSHOT_RW); - const bool isDdl = [&queryText] { - size_t pos = 0; - while (pos < queryText.size() && std::isspace(static_cast(queryText[pos]))) { - ++pos; - } - if (queryText.size() - pos >= 6) { - const char* start = queryText.c_str() + pos; - return (strncasecmp(start, "CREATE", 6) == 0 || - strncasecmp(start, "DROP", 4) == 0 || - strncasecmp(start, "ALTER", 5) == 0); - } - return false; - }(); + const bool isDdl = IsDdlQuery(queryText); if (isSnapshotRw || isDdl) { return session.StreamExecuteQuery( @@ -217,8 +238,14 @@ void TStatement::FillBoundColumns() { } SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { - if (!Cursor_) { - return SQL_NO_DATA; + if (targetValue && columnNumber < 1) { + return AddError("07009", 0, "Invalid descriptor index"); + } + if (Cursor_) { + const size_t n = Cursor_->GetColumnMeta().size(); + if (targetValue && n > 0 && static_cast(columnNumber) > n) { + return AddError("07009", 0, "Invalid descriptor index"); + } } BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), @@ -255,15 +282,23 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, return SQL_SUCCESS; } -NYdb::TParams TStatement::BuildParams() { +SQLRETURN TStatement::BuildParams(NYdb::TParams& out) { ClearErrors(); NYdb::TParamsBuilder paramsBuilder; for (const auto& param : BoundParams_) { - std::string paramName = "$p" + std::to_string(param.ParamNumber); - ConvertParam(param, paramsBuilder.AddParam(paramName)); + const std::string paramName = "$p" + std::to_string(param.ParamNumber); + const SQLRETURN convRc = ConvertParam(param, paramsBuilder.AddParam(paramName)); + if (convRc != SQL_SUCCESS) { + return AddError( + "07006", + 0, + "Unsupported or invalid ODBC parameter type for parameter " + std::to_string(param.ParamNumber) + + " (C type " + std::to_string(static_cast(param.ValueType)) + ", SQL type " + + std::to_string(static_cast(param.ParameterType)) + ")"); + } } - - return paramsBuilder.Build(); + out = paramsBuilder.Build(); + return SQL_SUCCESS; } SQLRETURN TStatement::Columns(const std::string& catalogName, @@ -296,13 +331,15 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, }; auto entries = GetPatternEntries(tableName); - if (entries.empty()) { - throw TOdbcException("HYC00", 0, "No tables found"); - } TTable table; table.reserve(entries.size()); + if (entries.empty()) { + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; + } + for (const auto& entry : entries) { if (entry.Type != NScheme::ESchemeEntryType::Table && entry.Type != NScheme::ESchemeEntryType::ColumnTable) { @@ -384,9 +421,6 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, }; auto entries = GetPatternEntries(tableName); - if (entries.empty()) { - throw TOdbcException("HYC00", 0, "No tables found"); - } TTable table; table.reserve(entries.size()); diff --git a/odbc/src/statement.h b/odbc/src/statement.h index f3ecd017562..e74ce58b84e 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -75,7 +75,7 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLULEN RowsFetched_ = 0; TStatementAttributes Attributes_; - NYdb::TParams BuildParams(); + SQLRETURN BuildParams(NYdb::TParams& out); NQuery::TExecuteQueryIterator CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params); diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index a19845d62e3..8f86c296596 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace NYdb { namespace NOdbc { @@ -534,5 +535,5 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER } } -} // namespace NYdb } // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h index 9b8140665e8..8f8195ba1c8 100644 --- a/odbc/src/utils/convert.h +++ b/odbc/src/utils/convert.h @@ -13,5 +13,6 @@ namespace NOdbc { SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder); SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); -} // namespace NYdb } // namespace NOdbc +} // namespace NYdb + diff --git a/odbc/src/utils/cursor.h b/odbc/src/utils/cursor.h index a44fc02e340..41f8e03ff16 100644 --- a/odbc/src/utils/cursor.h +++ b/odbc/src/utils/cursor.h @@ -9,6 +9,7 @@ #include #include #include +#include namespace NYdb { namespace NOdbc { diff --git a/odbc/src/utils/error_manager.cpp b/odbc/src/utils/error_manager.cpp index 92c8ec1750f..8e540e20c83 100644 --- a/odbc/src/utils/error_manager.cpp +++ b/odbc/src/utils/error_manager.cpp @@ -1,12 +1,14 @@ #include "error_manager.h" #include +#include #include #include #include namespace NYdb { namespace NOdbc { + namespace { struct OdbcErrorMapping { const char* sqlState; @@ -56,47 +58,45 @@ namespace { } return DEFAULT_ERROR_MAPPING; } -} // namespace - -namespace { -SQLRETURN WriteDiagCStr( - const std::string& str, - SQLPOINTER diagInfoPtr, - SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr, - bool sqlStateField = false) { - std::string storage; - const std::string* src = &str; - if (sqlStateField) { - storage = str; - if (storage.size() < 5) { - storage.append(5U - storage.size(), ' '); - } else { - storage.resize(5U); + SQLRETURN WriteDiagCStr( + const std::string& str, + SQLPOINTER diagInfoPtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr, + bool sqlStateField = false) { + std::string storage; + const std::string* src = &str; + if (sqlStateField) { + storage = str; + if (storage.size() < 5) { + storage.append(5U - storage.size(), ' '); + } else { + storage.resize(5U); + } + src = &storage; } - src = &storage; - } - if (!diagInfoPtr) { - return SQL_ERROR; - } - if (bufferLength < 0) { - return SQL_ERROR; - } - const size_t fullLen = src->size(); - if (stringLengthPtr) { - *stringLengthPtr = static_cast(std::min(fullLen, 0x7FFFU)); - } - if (bufferLength == 0) { - return fullLen == 0 ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + const size_t fullLen = src->size(); + if (stringLengthPtr) { + *stringLengthPtr = static_cast( + std::min(fullLen, static_cast(std::numeric_limits::max()))); + } + if (!diagInfoPtr) { + return SQL_SUCCESS; + } + if (bufferLength < 0) { + return SQL_ERROR; + } + if (bufferLength == 0) { + return fullLen == 0 ? SQL_SUCCESS : SQL_SUCCESS_WITH_INFO; + } + auto* out = static_cast(diagInfoPtr); + const size_t maxData = static_cast(bufferLength - 1U); + const size_t copyLen = std::min(fullLen, maxData); + std::memcpy(out, src->data(), copyLen); + out[copyLen] = 0; + return (fullLen > maxData) ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; } - auto* out = static_cast(diagInfoPtr); - const size_t maxData = static_cast(bufferLength - 1U); - const size_t copyLen = std::min(fullLen, maxData); - std::memcpy(out, src->data(), copyLen); - out[copyLen] = 0; - return (fullLen > maxData) ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; -} } // namespace @@ -134,21 +134,16 @@ SQLRETURN TErrorManager::GetDiagRec(SQLSMALLINT recNumber, SQLCHAR* sqlState, SQ } const auto& err = Errors_[recNumber-1]; + if (sqlState) { - strncpy((char*)sqlState, err.SqlState.c_str(), 6); + WriteDiagCStr(err.SqlState, sqlState, 6, nullptr, true); } if (nativeError) { *nativeError = err.NativeError; } - if (messageText && bufferLength > 0) { - strncpy((char*)messageText, err.Message.c_str(), bufferLength); - if (textLength) { - *textLength = (SQLSMALLINT)std::min((int)err.Message.size(), (int)bufferLength); - } - } - return SQL_SUCCESS; + return WriteDiagCStr(err.Message, messageText, bufferLength, textLength, false); } SQLRETURN TErrorManager::GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h index 9f91fab8a1d..e08083ed1f0 100644 --- a/odbc/src/utils/error_manager.h +++ b/odbc/src/utils/error_manager.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include diff --git a/odbc/src/utils/types.h b/odbc/src/utils/types.h index 0decb215fe3..9428cafebb0 100644 --- a/odbc/src/utils/types.h +++ b/odbc/src/utils/types.h @@ -4,6 +4,7 @@ #include #include +#include namespace NYdb { namespace NOdbc { diff --git a/odbc/tests/integration/error_handling_it.cpp b/odbc/tests/integration/error_handling_it.cpp index efa95527a3a..96bd01d7ea2 100644 --- a/odbc/tests/integration/error_handling_it.cpp +++ b/odbc/tests/integration/error_handling_it.cpp @@ -15,7 +15,13 @@ TEST(ErrorHandling, GetDiagRecAfterError) { SQLRETURN diagRc = SQLGetDiagRec(SQL_HANDLE_DBC, dbc, 1, sqlState, &nativeError, msg, sizeof(msg), &msgLen); ASSERT_TRUE(diagRc == SQL_SUCCESS || diagRc == SQL_SUCCESS_WITH_INFO); - ASSERT_EQ(msgLen, strlen((char*)msg)); + const size_t copiedLen = std::strlen(reinterpret_cast(msg)); + ASSERT_EQ(msg[copiedLen], static_cast(0)); + if (diagRc == SQL_SUCCESS_WITH_INFO) { + ASSERT_GE(static_cast(msgLen), copiedLen); + } else { + ASSERT_EQ(static_cast(msgLen), copiedLen); + } SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); } diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index 007deec9f90..d39710657fd 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -114,10 +114,14 @@ TEST(MetadataApi, SQLTablesLikePatternWithMetadataId) { stmt, SQL_HANDLE_STMT); ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); ASSERT_EQ(metadataId, SQL_TRUE); - ASSERT_EQ(SQLTables(stmt, nullptr, 0, nullptr, 0, - (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), - SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HYC00")); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), + stmt, SQL_HANDLE_STMT); + tableRows = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ++tableRows; + } + ASSERT_EQ(tableRows, 0); SQLFreeStmt(stmt, SQL_CLOSE); const std::string exactPath = "/local/test_meta_table_1"; CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, From 21f9ebb6003e3136e8c53a3e1d6946437443a8dd Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sat, 16 May 2026 19:59:15 +0300 Subject: [PATCH 27/55] fix odbc covert for chrono --- odbc/src/statement.cpp | 4 +++- odbc/src/utils/convert.cpp | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index b04265be334..04e146f591b 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -161,7 +161,9 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession const std::string queryText = Conn_->WrapQueryForCurrentCatalog(sqlText); NQuery::TExecuteQuerySettings execSettings; const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); - execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); + if (queryTimeoutSec > 0) { + execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); + } const auto txSettings = Conn_->MakeTxSettings(); if (Conn_->GetAutocommit()) { // TS_SNAPSHOT_RW doesn't support explicit BeginTx() - we use NoTx() instead diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index 8f86c296596..db7928ce659 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -463,11 +463,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER break; } case EPrimitiveType::Date32: { - const i32 days = parser.GetDate32(); - if (days < 0) { + const auto days = parser.GetDate32().time_since_epoch(); + if (days.count() < 0) { return SQL_ERROR; } - const TString t = TInstant::Days(static_cast(days)).FormatGmTime("%Y-%m-%d"); + const TString t = + TInstant::Days(static_cast(days.count())).FormatGmTime("%Y-%m-%d"); str.assign(t.data(), t.size()); break; } @@ -477,13 +478,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER break; } case EPrimitiveType::Datetime64: { - const std::int64_t secs = parser.GetDatetime64(); - if (secs < 0) { + const auto secs = parser.GetDatetime64().time_since_epoch(); + if (secs.count() < 0) { return SQL_ERROR; } - const TString t = - TInstant::Seconds(static_cast(static_cast(secs))) - .FormatGmTime("%Y-%m-%d %H:%M:%S"); + const TString t = TInstant::Seconds(static_cast(static_cast(secs.count()))) + .FormatGmTime("%Y-%m-%d %H:%M:%S"); str.assign(t.data(), t.size()); break; } @@ -493,12 +493,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER break; } case EPrimitiveType::Timestamp64: { - const std::int64_t micros = parser.GetTimestamp64(); - if (micros < 0) { + const auto micros = parser.GetTimestamp64().time_since_epoch(); + if (micros.count() < 0) { return SQL_ERROR; } const TString t = - TInstant::MicroSeconds(static_cast(static_cast(micros))) + TInstant::MicroSeconds(static_cast(static_cast(micros.count()))) .FormatGmTime("%Y-%m-%d %H:%M:%S"); str.assign(t.data(), t.size()); break; From 1910fa878247571d59dc1d33f6bf2aae7430ed75 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Tue, 19 May 2026 00:11:08 +0300 Subject: [PATCH 28/55] fix --- CMakeLists.txt | 2 +- examples/odbc/erlang_client/README.md | 30 +++ .../odbc/erlang_client/src/sample_data.erl | 64 +++++ .../erlang_client/src/ydb_series_client.erl | 223 ++++++++++++++++++ odbc/README.md | 34 ++- odbc/src/connection.cpp | 69 ++++++ odbc/src/connection.h | 6 + odbc/src/connection_attr.cpp | 20 ++ odbc/src/connection_attr.h | 2 + odbc/src/metadata.cpp | 56 +---- odbc/src/utils/attr.h | 7 - 11 files changed, 456 insertions(+), 57 deletions(-) create mode 100644 examples/odbc/erlang_client/README.md create mode 100644 examples/odbc/erlang_client/src/sample_data.erl create mode 100644 examples/odbc/erlang_client/src/ydb_series_client.erl diff --git a/CMakeLists.txt b/CMakeLists.txt index fc21d66c269..7cbd9471474 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ option(YDB_SDK_EXAMPLES "Build YDB C++ SDK examples" On) option(YDB_SDK_ENABLE_OTEL_METRICS "Build OpenTelemetry metrics plugin" Off) option(YDB_SDK_ENABLE_OTEL_TRACE "Build OpenTelemetry trace plugin" Off) option(YDB_CPP_SDK_SLO_USE_INSTALLED_SDK "Build only SLO workloads against an installed ydb-cpp-sdk package" Off) -option(YDB_SDK_ODBC "Build YDB ODBC driver" On) +option(YDB_SDK_ODBC "Build YDB ODBC driver" Off) set(YDB_SDK_GOOGLE_COMMON_PROTOS_TARGET "" CACHE STRING "Name of cmake target preparing google common proto library") option(YDB_SDK_USE_RAPID_JSON "Search for rapid json library in system" ON) diff --git a/examples/odbc/erlang_client/README.md b/examples/odbc/erlang_client/README.md new file mode 100644 index 00000000000..3a782ba9444 --- /dev/null +++ b/examples/odbc/erlang_client/README.md @@ -0,0 +1,30 @@ +# Erlang ODBC Series Example + +Minimal Erlang client for YDB ODBC. Mirrors the main scenario of the C++ `basic_example`: creates `series`, `seasons`, and `episodes` tables, fills them with test data, runs several queries, and drops the tables. + +## Requirements + +- Erlang/OTP with the `odbc` module +- unixODBC +- Built and registered YDB ODBC driver +- Running YDB instance reachable via the connection string + +Verify driver registration: + +```bash +odbcinst -q -d +``` + +## Running + +By default, `Driver=YDB;Endpoint=localhost:2136;Database=/local;` is used. + +```bash +make run +``` + +With a different connection string: + +```bash +make run CONN='...' +``` diff --git a/examples/odbc/erlang_client/src/sample_data.erl b/examples/odbc/erlang_client/src/sample_data.erl new file mode 100644 index 00000000000..5fccdce72ee --- /dev/null +++ b/examples/odbc/erlang_client/src/sample_data.erl @@ -0,0 +1,64 @@ +-module(sample_data). +-export([series/0, seasons/0, episodes/0, format_date/1]). + +series() -> + [ + [1, "IT Crowd", "The IT Crowd is a British sitcom by Channel 4.", days_from_date({2006, 2, 3})], + [2, "Silicon Valley", "Silicon Valley is an American comedy series.", days_from_date({2014, 4, 6})] + ]. + +seasons() -> + [ + [1, 1, "Season 1", days_from_date({2006, 2, 3}), days_from_date({2006, 5, 5})], + [1, 2, "Season 2", days_from_date({2007, 8, 24}), days_from_date({2007, 11, 16})], + [1, 3, "Season 3", days_from_date({2008, 11, 21}), days_from_date({2008, 12, 26})], + [1, 4, "Season 4", days_from_date({2010, 6, 25}), days_from_date({2010, 7, 30})], + [2, 1, "Season 1", days_from_date({2014, 4, 6}), days_from_date({2014, 6, 15})], + [2, 2, "Season 2", days_from_date({2015, 4, 12}), days_from_date({2015, 6, 14})], + [2, 3, "Season 3", days_from_date({2016, 4, 24}), days_from_date({2016, 6, 26})], + [2, 4, "Season 4", days_from_date({2017, 4, 23}), days_from_date({2017, 6, 25})], + [2, 5, "Season 5", days_from_date({2018, 3, 25}), days_from_date({2018, 5, 13})], + [2, 6, "Season 6", days_from_date({2019, 10, 27}), days_from_date({2019, 12, 8})] + ]. + +episodes() -> + [ + [1, 1, 1, "Yesterday's Jam", days_from_date({2006, 2, 3})], + [1, 1, 2, "Calamity Jen", days_from_date({2006, 2, 10})], + [1, 1, 3, "Fifty-Fifty", days_from_date({2006, 2, 17})], + [1, 1, 4, "The Red Door", days_from_date({2006, 2, 24})], + [1, 1, 5, "The Haunting of Bill Crouse", days_from_date({2006, 3, 3})], + [1, 1, 6, "Aunt Irma Visits", days_from_date({2006, 3, 10})], + [1, 2, 1, "The Work Outing", days_from_date({2007, 8, 24})], + [1, 2, 2, "Return of the Golden Child", days_from_date({2007, 8, 31})], + [1, 2, 3, "Moss and the German", days_from_date({2007, 9, 7})], + [2, 1, 1, "Minimum Viable Product", days_from_date({2014, 4, 6})], + [2, 1, 2, "The Cap Table", days_from_date({2014, 4, 13})], + [2, 1, 3, "Articles of Incorporation", days_from_date({2014, 4, 20})], + [2, 1, 4, "Fiduciary Duties", days_from_date({2014, 4, 27})], + [2, 1, 5, "Signaling Risk", days_from_date({2014, 5, 4})], + [2, 3, 1, "Founder Friendly", days_from_date({2016, 4, 24})], + [2, 3, 2, "Two in the Box", days_from_date({2016, 5, 1})], + [2, 3, 3, "Meinertzhagen's Haversack", days_from_date({2016, 5, 8})], + [2, 3, 4, "Maleant Data Systems Solutions", days_from_date({2016, 5, 15})], + [2, 5, 1, "Grow Fast or Die Slow", days_from_date({2018, 3, 25})], + [2, 5, 2, "Reorientation", days_from_date({2018, 4, 1})], + [2, 5, 3, "Chief Operating Officer", days_from_date({2018, 4, 8})], + [2, 5, 4, "Tech Evangelist", days_from_date({2018, 4, 15})], + [2, 5, 5, "Facial Recognition", days_from_date({2018, 4, 22})], + [2, 6, 1, "Artificial Emotional Intelligence", days_from_date({2019, 10, 27})], + [2, 6, 2, "Blood Money", days_from_date({2019, 11, 3})], + [2, 6, 3, "Hooli Smokes!", days_from_date({2019, 11, 10})], + [2, 6, 4, "Maximizing Alphaness", days_from_date({2019, 11, 17})], + [2, 6, 5, "Tethics", days_from_date({2019, 11, 24})], + [2, 6, 6, "RussFest", days_from_date({2019, 12, 1})], + [2, 6, 7, "Exit Event", days_from_date({2019, 12, 8})] + ]. + +days_from_date({Year, Month, Day}) -> + calendar:date_to_gregorian_days(Year, Month, Day) - calendar:date_to_gregorian_days(1970, 1, 1). + +format_date(Days) -> + Date = calendar:gregorian_days_to_date(Days + calendar:date_to_gregorian_days(1970, 1, 1)), + {Year, Month, Day} = Date, + io_lib:format("~4..0B-~2..0B-~2..0B", [Year, Month, Day]). diff --git a/examples/odbc/erlang_client/src/ydb_series_client.erl b/examples/odbc/erlang_client/src/ydb_series_client.erl new file mode 100644 index 00000000000..7b52b6687b5 --- /dev/null +++ b/examples/odbc/erlang_client/src/ydb_series_client.erl @@ -0,0 +1,223 @@ +-module(ydb_series_client). +-export([run/0, run/1, run_with_dsn/1]). + +run() -> + ConnectionString = "Driver=YDB;Endpoint=localhost:2136;Database=/local;", + run(ConnectionString). + +run(ConnectionString) when is_list(ConnectionString) -> + io:format("=== ODBC YDB Series Example ===~n"), + + application:load(odbc), + application:start(odbc), + + case odbc:connect(ConnectionString, [{tuple_format, list}]) of + {ok, Ref} -> + Result = run_example(Ref), + odbc:disconnect(Ref), + Result; + {error, Reason} -> + io:format("Connection failed: ~p~n", [Reason]), + error + end. + +run_with_dsn(DSN) -> + ConnectionString = lists:flatten(io_lib:format("DSN=~s;", [DSN])), + run(ConnectionString). + +run_example(Ref) -> + try + drop_tables(Ref), + create_tables(Ref), + fill_table_data(Ref), + select_simple(Ref), + upsert_simple(Ref), + select_with_params(Ref), + multistep(Ref), + select_seasons_by_series(Ref), + drop_tables(Ref), + + io:format("Completed successfully~n"), + ok + catch + Class:Reason:Stacktrace -> + io:format("~nError: ~p:~p~n", [Class, Reason]), + io:format("Stacktrace: ~p~n", [Stacktrace]), + error + end. + +create_tables(Ref) -> + Tables = [ + {"CREATE TABLE series ( + series_id Uint64, + title Utf8, + series_info Utf8, + release_date Uint64, + PRIMARY KEY (series_id) + );"}, + {"CREATE TABLE seasons ( + series_id Uint64, + season_id Uint64, + title Utf8, + first_aired Uint64, + last_aired Uint64, + PRIMARY KEY (series_id, season_id) + );"}, + {"CREATE TABLE episodes ( + series_id Uint64, + season_id Uint64, + episode_id Uint64, + title Utf8, + air_date Uint64, + PRIMARY KEY (series_id, season_id, episode_id) + );"} + ], + + lists:foreach(fun({Query}) -> + execute_update(Ref, Query) + end, Tables). + +fill_table_data(Ref) -> + SeriesData = sample_data:series(), + SeasonsData = sample_data:seasons(), + EpisodesData = sample_data:episodes(), + + lists:foreach(fun(Row) -> + [Id, Title, Info, Date] = Row, + Query = io_lib:format( + "UPSERT INTO series (series_id, title, series_info, release_date) VALUES (~p, \"~s\", \"~s\", ~p);", + [Id, escape_string(Title), escape_string(Info), Date] + ), + execute_update(Ref, Query) + end, SeriesData), + + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, Title, FirstAired, LastAired] = Row, + Query = io_lib:format( + "UPSERT INTO seasons (series_id, season_id, title, first_aired, last_aired) VALUES (~p, ~p, \"~s\", ~p, ~p);", + [SeriesId, SeasonId, escape_string(Title), FirstAired, LastAired] + ), + execute_update(Ref, Query) + end, SeasonsData), + + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, EpisodeId, Title, AirDate] = Row, + Query = io_lib:format( + "UPSERT INTO episodes (series_id, season_id, episode_id, title, air_date) VALUES (~p, ~p, ~p, \"~s\", ~p);", + [SeriesId, SeasonId, EpisodeId, escape_string(Title), AirDate] + ), + execute_update(Ref, Query) + end, EpisodesData), + + io:format("Inserted ~p series, ~p seasons, ~p episodes~n", + [length(SeriesData), length(SeasonsData), length(EpisodesData)]). + +select_simple(Ref) -> + Query = "SELECT CAST(series_id AS Utf8) AS series_id, title, CAST(release_date AS Date) AS release_date FROM series WHERE series_id = 1;", + Rows = selected_rows(Ref, select_simple, Query), + lists:foreach(fun(Row) -> + [Id, Title, ReleaseDate] = row_values(Row), + io:format("Series: Id=~p, Title=~p, Release=~p~n", [Id, Title, ReleaseDate]) + end, Rows). + +upsert_simple(Ref) -> + Query = "UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES (2, 6, 1, \"TBD\");", + execute_update(Ref, Query). + +select_with_params(Ref) -> + SeriesId = 2, + SeasonId = 3, + + Query = + "SELECT sa.title AS season_title, sr.title AS series_title " + "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id " + "WHERE sa.series_id = CAST($p1 AS Uint64) AND sa.season_id = CAST($p2 AS Uint64);", + Params = [{sql_integer, [SeriesId]}, {sql_integer, [SeasonId]}], + + Rows = selected_param_rows(Ref, select_with_params, Query, Params), + lists:foreach(fun(Row) -> + [SeasonTitle, SeriesTitle] = row_values(Row), + io:format("Season: ~p (Series: ~p)~n", [SeasonTitle, SeriesTitle]) + end, Rows). + +multistep(Ref) -> + SeriesId = 2, + SeasonId = 5, + + Query1 = io_lib:format( + "SELECT CAST(first_aired AS Utf8) AS first_aired FROM seasons WHERE series_id = ~p AND season_id = ~p;", + [SeriesId, SeasonId] + ), + + [FirstAiredRow] = selected_rows(Ref, multistep_step1, Query1), + [Date] = row_values(FirstAiredRow), + FromDate = list_to_integer(Date), + + ToDate = FromDate + 15, + + Query2 = io_lib:format( + "SELECT CAST(season_id AS Utf8) AS season_id, CAST(episode_id AS Utf8) AS episode_id, title, CAST(air_date AS Utf8) AS air_date FROM episodes " + "WHERE series_id = ~p AND air_date >= ~p AND air_date <= ~p;", + [SeriesId, FromDate, ToDate] + ), + + Rows = selected_rows(Ref, multistep_step2, Query2), + lists:foreach(fun(Row) -> + [SId, EId, Title, AirDate] = row_values(Row), + io:format("Episode: S~pE~p ~p (aired: ~p)~n", [SId, EId, Title, AirDate]) + end, Rows). + +select_seasons_by_series(Ref) -> + SeriesList = [1, 2], + InClause = string:join([integer_to_list(X) || X <- SeriesList], ", "), + + Query = io_lib:format( + "SELECT CAST(series_id AS Utf8) AS series_id, CAST(season_id AS Utf8) AS season_id, title, CAST(first_aired AS Date) AS first_aired " + "FROM seasons WHERE series_id IN (~s) ORDER BY season_id;", + [InClause] + ), + + Rows = selected_rows(Ref, select_seasons_by_series, Query), + lists:foreach(fun(Row) -> + [SeriesId, SeasonId, Title, FirstAired] = row_values(Row), + io:format("Season: Series=~p, Season=~p, Title=~p, FirstAired=~p~n", + [SeriesId, SeasonId, Title, FirstAired]) + end, Rows). + +drop_tables(Ref) -> + Tables = ["series", "seasons", "episodes"], + + lists:foreach(fun(Table) -> + Query = io_lib:format("DROP TABLE ~s;", [Table]), + case odbc:sql_query(Ref, lists:flatten(Query)) of + {updated, _} -> ok; + {error, _} -> ok + end + end, Tables). + +escape_string(String) -> + EscapedBackslash = string:replace(String, "\\", "\\\\", all), + lists:flatten(string:replace(EscapedBackslash, "\"", "\\\"", all)). + +execute_update(Ref, Query) -> + case odbc:sql_query(Ref, lists:flatten(Query)) of + {updated, _} -> ok; + Error -> throw({query_failed, update, Error}) + end. + +selected_rows(Ref, Step, Query) -> + case odbc:sql_query(Ref, lists:flatten(Query)) of + {selected, _, Rows} -> Rows; + Error -> throw({query_failed, Step, Error}) + end. + +selected_param_rows(Ref, Step, Query, Params) -> + case odbc:param_query(Ref, lists:flatten(Query), Params) of + {selected, _, Rows} -> Rows; + Error -> throw({query_failed, Step, Error}) + end. + +row_values(Row) when is_tuple(Row) -> + tuple_to_list(Row); +row_values(Row) -> + Row. diff --git a/odbc/README.md b/odbc/README.md index f240a8cb2ee..e5b4b4f0838 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -12,23 +12,53 @@ ODBC driver for YDB. ## Build ```bash -cmake -DYDB_SDK_ODBC=1 --preset release-clang +cmake -DYDB_SDK_ODBC=1 --preset release-test-clang cmake --build --preset default ``` +The shared library is produced as `build/odbc/libydb-odbc.so`. + +## Install + +After configure and build, CMake generates `build/odbc/odbcinst.ini` with the correct path to `libydb-odbc.so`. + +Register the driver with unixODBC: + +```bash +sudo odbcinst -i -d -f build/odbc/odbcinst.ini +``` + +Add a DSN — either copy the sample into the system config: + +```bash +sudo cp odbc/odbc.ini /etc/odbc.ini +# edit Server, Database, etc. +``` + +or point applications at the sample in the repo: + +```bash +export ODBCINI=/absolute/path/to/ydb-cpp-sdk/odbc/odbc.ini +``` + + ## Configuration 1. Make sure the driver is registered: + ```bash odbcinst -q -d ``` +You should see an entry named `YDB`. + 2. Check available data sources: + ```bash odbcinst -q -s ``` -3. Edit `/etc/odbc.ini` to configure the connection: +3. Edit `/etc/odbc.ini` (or your `ODBCINI` file) to configure the connection: ```ini [YDB] Driver=YDB diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index de976892020..a3769588558 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -2,6 +2,9 @@ #include "statement.h" #include "utils/error_manager.h" +#include +#include + #include #include #include @@ -89,6 +92,7 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { } Endpoint_ = params.contains("Server") ? params["Server"] : params["Endpoint"]; Database_ = params["Database"]; + DataSourceName_ = params.contains("DSN") ? params["DSN"] : ""; if (Endpoint_.empty() || Database_.empty()) { throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in connection string"); @@ -104,6 +108,7 @@ SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { SQLRETURN TConnection::Connect(const std::string& serverName, const std::string& userName, const std::string& auth) { + DataSourceName_ = serverName; char endpoint[256] = {0}; char server[256] = {0}; @@ -130,6 +135,8 @@ SQLRETURN TConnection::Connect(const std::string& serverName, SQLRETURN TConnection::Disconnect() { QuerySession_.reset(); Tx_.reset(); + DbmsVersionCache_.reset(); + DataSourceName_.clear(); YdbSchemeClient_.reset(); YdbTableClient_.reset(); YdbClient_.reset(); @@ -242,9 +249,71 @@ TEnvironment* TConnection::GetEnvironment(){ return ParentEnv_; } +const std::string& TConnection::GetDataSourceName() const { + return DataSourceName_; +} + +SQLUINTEGER TConnection::GetSupportedTxnIsolationOptions() const { + return Attributes_.GetSupportedTxnIsolationOptions(); +} + +bool TConnection::IsDataSourceReadOnly() const { + return Attributes_.GetAccessMode() == SQL_MODE_READ_ONLY; +} + +const std::string& TConnection::GetDbmsVersion() { + if (DbmsVersionCache_) { + return *DbmsVersionCache_; + } + + static const std::string fallback = "unknown"; + DbmsVersionCache_ = fallback; + + auto* client = GetClient(); + if (!client) { + return *DbmsVersionCache_; + } + + std::optional fetched; + const NYdb::TStatus status = client->RetryQuerySync( + [&fetched](NQuery::TSession session) -> NYdb::TStatus { + auto iterator = session.StreamExecuteQuery( + "SELECT Version();", + NQuery::TTxControl::NoTx(), + NYdb::TParamsBuilder().Build()).ExtractValueSync(); + if (!iterator.IsSuccess()) { + return NYdb::TStatus(iterator.GetStatus(), NYdb::NIssue::TIssues(iterator.GetIssues())); + } + while (true) { + auto part = iterator.ReadNext().ExtractValueSync(); + if (part.EOS()) { + break; + } + if (!part.IsSuccess()) { + return NYdb::TStatus(part.GetStatus(), NYdb::NIssue::TIssues(part.GetIssues())); + } + if (!part.HasResultSet()) { + continue; + } + TResultSetParser parser(part.ExtractResultSet()); + if (parser.TryNextRow()) { + fetched = parser.ColumnParser(0).GetUtf8(); + } + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + } + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + }); + + if (status.IsSuccess() && fetched && !fetched->empty()) { + DbmsVersionCache_ = std::move(*fetched); + } + return *DbmsVersionCache_; +} + void TConnection::RecreateYdbClients() { QuerySession_.reset(); Tx_.reset(); + DbmsVersionCache_.reset(); YdbSchemeClient_.reset(); YdbTableClient_.reset(); YdbClient_.reset(); diff --git a/odbc/src/connection.h b/odbc/src/connection.h index dac7721c000..0bf9cc3d78c 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -34,10 +34,12 @@ class TConnection : public TErrorManager { std::vector> Statements_; std::string Endpoint_; std::string Database_; + std::string DataSourceName_; std::string AuthToken_; TEnvironment* ParentEnv_; TConnectionAttributes Attributes_; + mutable std::optional DbmsVersionCache_; void RecreateYdbClients(); void RebindToDatabase(const std::string& newDatabase); @@ -65,6 +67,10 @@ class TConnection : public TErrorManager { NQuery::TTxSettings MakeTxSettings() const; std::string WrapQueryForCurrentCatalog(const std::string& sql) const; + const std::string& GetDbmsVersion(); + const std::string& GetDataSourceName() const; + SQLUINTEGER GetSupportedTxnIsolationOptions() const; + bool IsDataSourceReadOnly() const; const std::optional& GetTx(); void SetTx(const NQuery::TTransaction& tx); diff --git a/odbc/src/connection_attr.cpp b/odbc/src/connection_attr.cpp index 4872b1cdef1..576f093b656 100644 --- a/odbc/src/connection_attr.cpp +++ b/odbc/src/connection_attr.cpp @@ -228,6 +228,26 @@ SQLRETURN TConnectionAttributes::GetTxnIsolation(SQLPOINTER value) const { return SQL_SUCCESS; } +SQLUINTEGER TConnectionAttributes::GetAccessMode() const { + return AccessMode_; +} + +SQLUINTEGER TConnectionAttributes::GetSupportedTxnIsolationOptions() const { + static constexpr SQLUINTEGER kLevels[] = { + SQL_TXN_READ_UNCOMMITTED, + SQL_TXN_READ_COMMITTED, + SQL_TXN_REPEATABLE_READ, + SQL_TXN_SERIALIZABLE, + }; + SQLUINTEGER mask = 0; + for (const SQLUINTEGER level : kLevels) { + if (Tx::ResolveTxMode(AccessMode_, level)) { + mask |= level; + } + } + return mask; +} + SQLRETURN TConnectionAttributes::GetCurrentCatalog( SQLPOINTER value, SQLINTEGER bufferLength, diff --git a/odbc/src/connection_attr.h b/odbc/src/connection_attr.h index 607f7bf929f..cd530554f2f 100644 --- a/odbc/src/connection_attr.h +++ b/odbc/src/connection_attr.h @@ -56,6 +56,8 @@ class TConnectionAttributes { std::optional& rebindDatabase, TErrorManager& errors); static void NormalizeCatalogPath(std::string& path); + SQLUINTEGER GetSupportedTxnIsolationOptions() const; + SQLUINTEGER GetAccessMode() const; private: SQLRETURN SetAutocommit( diff --git a/odbc/src/metadata.cpp b/odbc/src/metadata.cpp index 2607408fca7..b8857b245d9 100644 --- a/odbc/src/metadata.cpp +++ b/odbc/src/metadata.cpp @@ -107,21 +107,21 @@ SQLRETURN TMetadata::GetInfo( case SQL_DRIVER_NAME: return WriteInfoString(conn, "ydb-odbc", infoValuePtr, bufferLength, stringLengthPtr); case SQL_DRIVER_VER: - return WriteInfoString(conn, "03.80.0000", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, "unknown", infoValuePtr, bufferLength, stringLengthPtr); case SQL_DRIVER_ODBC_VER: - return WriteInfoString(conn, "03.80", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, "03.00", infoValuePtr, bufferLength, stringLengthPtr); // DBMS Information case SQL_DBMS_NAME: return WriteInfoString(conn, "YDB", infoValuePtr, bufferLength, stringLengthPtr); case SQL_DBMS_VER: - return WriteInfoString(conn, "3.8.0", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, conn->GetDbmsVersion().c_str(), infoValuePtr, bufferLength, stringLengthPtr); // Identifier Handling case SQL_IDENTIFIER_QUOTE_CHAR: return WriteInfoString(conn, "\"", infoValuePtr, bufferLength, stringLengthPtr); case SQL_IDENTIFIER_CASE: - return WriteInfoScalar(conn, SQL_IC_MIXED, infoValuePtr, stringLengthPtr); + return WriteInfoScalar(conn, SQL_IC_LOWER, infoValuePtr, stringLengthPtr); // Catalog Support case SQL_CATALOG_NAME: @@ -141,9 +141,10 @@ SQLRETURN TMetadata::GetInfo( // Data Source Capabilities case SQL_DATA_SOURCE_READ_ONLY: - return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString( + conn, conn->IsDataSourceReadOnly() ? "Y" : "N", infoValuePtr, bufferLength, stringLengthPtr); case SQL_DATA_SOURCE_NAME: - return WriteInfoString(conn, "YDB", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, conn->GetDataSourceName().c_str(), infoValuePtr, bufferLength, stringLengthPtr); // Result Set Capabilities case SQL_MULT_RESULT_SETS: @@ -163,52 +164,14 @@ SQLRETURN TMetadata::GetInfo( return WriteInfoScalar(conn, SQL_TXN_SERIALIZABLE, infoValuePtr, stringLengthPtr); case SQL_TXN_ISOLATION_OPTION: return WriteInfoScalar( - conn, - SQL_TXN_READ_UNCOMMITTED | SQL_TXN_READ_COMMITTED | SQL_TXN_REPEATABLE_READ | SQL_TXN_SERIALIZABLE, - infoValuePtr, - stringLengthPtr); - - // Connection Limits - case SQL_MAX_CONCURRENT_ACTIVITIES: - return WriteInfoScalar(conn, 1, infoValuePtr, stringLengthPtr); - case SQL_MAX_DRIVER_CONNECTIONS: - return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); - - // SQL Support - case SQL_SQL_CONFORMANCE: - return WriteInfoScalar(conn, SQL_SC_SQL92_ENTRY, infoValuePtr, stringLengthPtr); - case SQL_SQL92_RELATIONAL_JOIN_OPERATORS: - return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); - case SQL_SUBQUERIES: - return WriteInfoScalar(conn, SQL_SQ_CORRELATED_SUBQUERIES, infoValuePtr, stringLengthPtr); - - // Supported Statements - case SQL_SQL92_PREDICATES: - return WriteInfoScalar(conn, SQL_SP_IN | SQL_SP_ISNOTNULL | SQL_SP_ISNULL | SQL_SP_LIKE, infoValuePtr, stringLengthPtr); - case SQL_SQL92_VALUE_EXPRESSIONS: - return WriteInfoScalar(conn, SQL_SVE_CAST | SQL_SVE_CASE | SQL_SVE_COALESCE, infoValuePtr, stringLengthPtr); - case SQL_AGGREGATE_FUNCTIONS: - return WriteInfoScalar(conn, SQL_AF_ALL | SQL_AF_AVG | SQL_AF_COUNT | SQL_AF_MAX | SQL_AF_MIN | SQL_AF_SUM, infoValuePtr, stringLengthPtr); - - // Data Type Limits - case SQL_MAX_CHAR_LITERAL_LEN: - return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); - case SQL_MAX_COLUMN_NAME_LEN: - return WriteInfoScalar(conn, 255, infoValuePtr, stringLengthPtr); - case SQL_MAX_TABLE_NAME_LEN: - return WriteInfoScalar(conn, 255, infoValuePtr, stringLengthPtr); - case SQL_MAX_COLUMNS_IN_TABLE: - return WriteInfoScalar(conn, 256, infoValuePtr, stringLengthPtr); - case SQL_MAX_COLUMNS_IN_SELECT: - return WriteInfoScalar(conn, 1024, infoValuePtr, stringLengthPtr); + conn, conn->GetSupportedTxnIsolationOptions(), infoValuePtr, stringLengthPtr); // Stored Procedures (not supported) case SQL_PROCEDURES: return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); - // Outer Joins (limited support) case SQL_OUTER_JOINS: - return WriteInfoString(conn, "N", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, "Y", infoValuePtr, bufferLength, stringLengthPtr); // Positioned Operations (not supported) case SQL_POSITIONED_STATEMENTS: @@ -236,7 +199,6 @@ SQLRETURN TMetadata::GetInfo( case SQL_ASYNC_MODE: return WriteInfoScalar(conn, SQL_AM_NONE, infoValuePtr, stringLengthPtr); - // Case Sensitivity case SQL_QUOTED_IDENTIFIER_CASE: return WriteInfoScalar(conn, SQL_IC_SENSITIVE, infoValuePtr, stringLengthPtr); diff --git a/odbc/src/utils/attr.h b/odbc/src/utils/attr.h index 34abeed42da..96695c221aa 100644 --- a/odbc/src/utils/attr.h +++ b/odbc/src/utils/attr.h @@ -20,12 +20,6 @@ SQLRETURN WriteAttributeString( SQLINTEGER* stringLengthPtr, TErrorManager& errors); -template -T ReadIntegerAttr(SQLPOINTER value) noexcept; - -template -std::optional ReadIntegerAttrIfIn(SQLPOINTER value, std::initializer_list allowed) noexcept; - template T ReadIntegerAttr(SQLPOINTER value) noexcept { return static_cast(reinterpret_cast(value)); @@ -42,5 +36,4 @@ std::optional ReadIntegerAttrIfIn(SQLPOINTER value, std::initializer_list return std::nullopt; } - } // namespace NYdb::NOdbc From 80a8f1633252c0c6eb80d70993450526a8f9db97 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Wed, 20 May 2026 19:09:25 +0300 Subject: [PATCH 29/55] review fixes --- examples/erlang_odbc_client/Makefile | 50 ---- examples/erlang_odbc_client/README.md | 30 --- .../erlang_odbc_client/src/sample_data.erl | 64 ----- .../src/ydb_series_client.erl | 223 ------------------ odbc/CMakeLists.txt | 11 - odbc/README.md | 44 +--- odbc/src/connection.cpp | 26 +- odbc/tests/CMakeLists.txt | 28 +++ 8 files changed, 49 insertions(+), 427 deletions(-) delete mode 100644 examples/erlang_odbc_client/Makefile delete mode 100644 examples/erlang_odbc_client/README.md delete mode 100644 examples/erlang_odbc_client/src/sample_data.erl delete mode 100644 examples/erlang_odbc_client/src/ydb_series_client.erl diff --git a/examples/erlang_odbc_client/Makefile b/examples/erlang_odbc_client/Makefile deleted file mode 100644 index 5f402ef9720..00000000000 --- a/examples/erlang_odbc_client/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -.PHONY: all compile run run-shell clean distclean check help - -CONN ?= Driver=YDB;Endpoint=localhost:2136;Database=/local; -ERLC ?= erlc -ERL ?= erl - -all: compile - -help: - @echo "YDB Series Example - Erlang ODBC" - @echo "" - @echo "Targets:" - @echo " make compile Compile Erlang modules" - @echo " make run Run the example" - @echo " make run CONN='...' Run with a custom connection string" - @echo " make run-shell Start Erlang shell with compiled modules" - @echo " make check Check Erlang ODBC availability" - @echo " make clean Remove compiled files" - @echo "" - @echo "Examples:" - @echo " make run" - @echo " make run CONN=\"Driver=YDB;Endpoint=myhost:2136;Database=/mydb;\"" - -prepare: - @mkdir -p ebin - -compile: prepare - @echo "Compiling Erlang modules..." - $(ERLC) -o ebin src/*.erl - @echo "Done." - -run: compile - @echo "Running YDB Series Example..." - $(ERL) -pa ebin -noshell -eval 'application:load(odbc), application:start(odbc), ydb_series_client:run("$(CONN)"), halt().' - -run-shell: compile - @echo "Starting Erlang shell with ydb_series_client..." - $(ERL) -pa ebin - -check: - @echo "Checking ODBC support in Erlang..." - @$(ERL) -noshell -eval 'application:load(odbc), io:format("~p~n", [application:start(odbc)]), halt().' - -clean: - @rm -rf ebin/*.beam - @rm -rf *.beam - @rm -rf erl_crash.dump - -distclean: clean - @rm -rf ebin diff --git a/examples/erlang_odbc_client/README.md b/examples/erlang_odbc_client/README.md deleted file mode 100644 index a296e9edd70..00000000000 --- a/examples/erlang_odbc_client/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Erlang ODBC Series Example - -Минимальный Erlang-клиент для YDB ODBC. Повторяет основной сценарий C++ `basic_example`: создает таблицы `series`, `seasons`, `episodes`, заполняет их тестовыми данными, выполняет несколько запросов и удаляет таблицы. - -## Требования - -- Erlang/OTP с модулем `odbc` -- unixODBC -- Собранный и зарегистрированный YDB ODBC driver -- Запущенная YDB, доступная по строке подключения - -Проверить регистрацию драйвера: - -```bash -odbcinst -q -d -``` - -## Запуск - -По умолчанию используется `Driver=YDB;Endpoint=localhost:2136;Database=/local;`. - -```bash -make run -``` - -С другой строкой подключения: - -```bash -make run CONN='...' -``` diff --git a/examples/erlang_odbc_client/src/sample_data.erl b/examples/erlang_odbc_client/src/sample_data.erl deleted file mode 100644 index 5fccdce72ee..00000000000 --- a/examples/erlang_odbc_client/src/sample_data.erl +++ /dev/null @@ -1,64 +0,0 @@ --module(sample_data). --export([series/0, seasons/0, episodes/0, format_date/1]). - -series() -> - [ - [1, "IT Crowd", "The IT Crowd is a British sitcom by Channel 4.", days_from_date({2006, 2, 3})], - [2, "Silicon Valley", "Silicon Valley is an American comedy series.", days_from_date({2014, 4, 6})] - ]. - -seasons() -> - [ - [1, 1, "Season 1", days_from_date({2006, 2, 3}), days_from_date({2006, 5, 5})], - [1, 2, "Season 2", days_from_date({2007, 8, 24}), days_from_date({2007, 11, 16})], - [1, 3, "Season 3", days_from_date({2008, 11, 21}), days_from_date({2008, 12, 26})], - [1, 4, "Season 4", days_from_date({2010, 6, 25}), days_from_date({2010, 7, 30})], - [2, 1, "Season 1", days_from_date({2014, 4, 6}), days_from_date({2014, 6, 15})], - [2, 2, "Season 2", days_from_date({2015, 4, 12}), days_from_date({2015, 6, 14})], - [2, 3, "Season 3", days_from_date({2016, 4, 24}), days_from_date({2016, 6, 26})], - [2, 4, "Season 4", days_from_date({2017, 4, 23}), days_from_date({2017, 6, 25})], - [2, 5, "Season 5", days_from_date({2018, 3, 25}), days_from_date({2018, 5, 13})], - [2, 6, "Season 6", days_from_date({2019, 10, 27}), days_from_date({2019, 12, 8})] - ]. - -episodes() -> - [ - [1, 1, 1, "Yesterday's Jam", days_from_date({2006, 2, 3})], - [1, 1, 2, "Calamity Jen", days_from_date({2006, 2, 10})], - [1, 1, 3, "Fifty-Fifty", days_from_date({2006, 2, 17})], - [1, 1, 4, "The Red Door", days_from_date({2006, 2, 24})], - [1, 1, 5, "The Haunting of Bill Crouse", days_from_date({2006, 3, 3})], - [1, 1, 6, "Aunt Irma Visits", days_from_date({2006, 3, 10})], - [1, 2, 1, "The Work Outing", days_from_date({2007, 8, 24})], - [1, 2, 2, "Return of the Golden Child", days_from_date({2007, 8, 31})], - [1, 2, 3, "Moss and the German", days_from_date({2007, 9, 7})], - [2, 1, 1, "Minimum Viable Product", days_from_date({2014, 4, 6})], - [2, 1, 2, "The Cap Table", days_from_date({2014, 4, 13})], - [2, 1, 3, "Articles of Incorporation", days_from_date({2014, 4, 20})], - [2, 1, 4, "Fiduciary Duties", days_from_date({2014, 4, 27})], - [2, 1, 5, "Signaling Risk", days_from_date({2014, 5, 4})], - [2, 3, 1, "Founder Friendly", days_from_date({2016, 4, 24})], - [2, 3, 2, "Two in the Box", days_from_date({2016, 5, 1})], - [2, 3, 3, "Meinertzhagen's Haversack", days_from_date({2016, 5, 8})], - [2, 3, 4, "Maleant Data Systems Solutions", days_from_date({2016, 5, 15})], - [2, 5, 1, "Grow Fast or Die Slow", days_from_date({2018, 3, 25})], - [2, 5, 2, "Reorientation", days_from_date({2018, 4, 1})], - [2, 5, 3, "Chief Operating Officer", days_from_date({2018, 4, 8})], - [2, 5, 4, "Tech Evangelist", days_from_date({2018, 4, 15})], - [2, 5, 5, "Facial Recognition", days_from_date({2018, 4, 22})], - [2, 6, 1, "Artificial Emotional Intelligence", days_from_date({2019, 10, 27})], - [2, 6, 2, "Blood Money", days_from_date({2019, 11, 3})], - [2, 6, 3, "Hooli Smokes!", days_from_date({2019, 11, 10})], - [2, 6, 4, "Maximizing Alphaness", days_from_date({2019, 11, 17})], - [2, 6, 5, "Tethics", days_from_date({2019, 11, 24})], - [2, 6, 6, "RussFest", days_from_date({2019, 12, 1})], - [2, 6, 7, "Exit Event", days_from_date({2019, 12, 8})] - ]. - -days_from_date({Year, Month, Day}) -> - calendar:date_to_gregorian_days(Year, Month, Day) - calendar:date_to_gregorian_days(1970, 1, 1). - -format_date(Days) -> - Date = calendar:gregorian_days_to_date(Days + calendar:date_to_gregorian_days(1970, 1, 1)), - {Year, Month, Day} = Date, - io_lib:format("~4..0B-~2..0B-~2..0B", [Year, Month, Day]). diff --git a/examples/erlang_odbc_client/src/ydb_series_client.erl b/examples/erlang_odbc_client/src/ydb_series_client.erl deleted file mode 100644 index 7b52b6687b5..00000000000 --- a/examples/erlang_odbc_client/src/ydb_series_client.erl +++ /dev/null @@ -1,223 +0,0 @@ --module(ydb_series_client). --export([run/0, run/1, run_with_dsn/1]). - -run() -> - ConnectionString = "Driver=YDB;Endpoint=localhost:2136;Database=/local;", - run(ConnectionString). - -run(ConnectionString) when is_list(ConnectionString) -> - io:format("=== ODBC YDB Series Example ===~n"), - - application:load(odbc), - application:start(odbc), - - case odbc:connect(ConnectionString, [{tuple_format, list}]) of - {ok, Ref} -> - Result = run_example(Ref), - odbc:disconnect(Ref), - Result; - {error, Reason} -> - io:format("Connection failed: ~p~n", [Reason]), - error - end. - -run_with_dsn(DSN) -> - ConnectionString = lists:flatten(io_lib:format("DSN=~s;", [DSN])), - run(ConnectionString). - -run_example(Ref) -> - try - drop_tables(Ref), - create_tables(Ref), - fill_table_data(Ref), - select_simple(Ref), - upsert_simple(Ref), - select_with_params(Ref), - multistep(Ref), - select_seasons_by_series(Ref), - drop_tables(Ref), - - io:format("Completed successfully~n"), - ok - catch - Class:Reason:Stacktrace -> - io:format("~nError: ~p:~p~n", [Class, Reason]), - io:format("Stacktrace: ~p~n", [Stacktrace]), - error - end. - -create_tables(Ref) -> - Tables = [ - {"CREATE TABLE series ( - series_id Uint64, - title Utf8, - series_info Utf8, - release_date Uint64, - PRIMARY KEY (series_id) - );"}, - {"CREATE TABLE seasons ( - series_id Uint64, - season_id Uint64, - title Utf8, - first_aired Uint64, - last_aired Uint64, - PRIMARY KEY (series_id, season_id) - );"}, - {"CREATE TABLE episodes ( - series_id Uint64, - season_id Uint64, - episode_id Uint64, - title Utf8, - air_date Uint64, - PRIMARY KEY (series_id, season_id, episode_id) - );"} - ], - - lists:foreach(fun({Query}) -> - execute_update(Ref, Query) - end, Tables). - -fill_table_data(Ref) -> - SeriesData = sample_data:series(), - SeasonsData = sample_data:seasons(), - EpisodesData = sample_data:episodes(), - - lists:foreach(fun(Row) -> - [Id, Title, Info, Date] = Row, - Query = io_lib:format( - "UPSERT INTO series (series_id, title, series_info, release_date) VALUES (~p, \"~s\", \"~s\", ~p);", - [Id, escape_string(Title), escape_string(Info), Date] - ), - execute_update(Ref, Query) - end, SeriesData), - - lists:foreach(fun(Row) -> - [SeriesId, SeasonId, Title, FirstAired, LastAired] = Row, - Query = io_lib:format( - "UPSERT INTO seasons (series_id, season_id, title, first_aired, last_aired) VALUES (~p, ~p, \"~s\", ~p, ~p);", - [SeriesId, SeasonId, escape_string(Title), FirstAired, LastAired] - ), - execute_update(Ref, Query) - end, SeasonsData), - - lists:foreach(fun(Row) -> - [SeriesId, SeasonId, EpisodeId, Title, AirDate] = Row, - Query = io_lib:format( - "UPSERT INTO episodes (series_id, season_id, episode_id, title, air_date) VALUES (~p, ~p, ~p, \"~s\", ~p);", - [SeriesId, SeasonId, EpisodeId, escape_string(Title), AirDate] - ), - execute_update(Ref, Query) - end, EpisodesData), - - io:format("Inserted ~p series, ~p seasons, ~p episodes~n", - [length(SeriesData), length(SeasonsData), length(EpisodesData)]). - -select_simple(Ref) -> - Query = "SELECT CAST(series_id AS Utf8) AS series_id, title, CAST(release_date AS Date) AS release_date FROM series WHERE series_id = 1;", - Rows = selected_rows(Ref, select_simple, Query), - lists:foreach(fun(Row) -> - [Id, Title, ReleaseDate] = row_values(Row), - io:format("Series: Id=~p, Title=~p, Release=~p~n", [Id, Title, ReleaseDate]) - end, Rows). - -upsert_simple(Ref) -> - Query = "UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES (2, 6, 1, \"TBD\");", - execute_update(Ref, Query). - -select_with_params(Ref) -> - SeriesId = 2, - SeasonId = 3, - - Query = - "SELECT sa.title AS season_title, sr.title AS series_title " - "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id " - "WHERE sa.series_id = CAST($p1 AS Uint64) AND sa.season_id = CAST($p2 AS Uint64);", - Params = [{sql_integer, [SeriesId]}, {sql_integer, [SeasonId]}], - - Rows = selected_param_rows(Ref, select_with_params, Query, Params), - lists:foreach(fun(Row) -> - [SeasonTitle, SeriesTitle] = row_values(Row), - io:format("Season: ~p (Series: ~p)~n", [SeasonTitle, SeriesTitle]) - end, Rows). - -multistep(Ref) -> - SeriesId = 2, - SeasonId = 5, - - Query1 = io_lib:format( - "SELECT CAST(first_aired AS Utf8) AS first_aired FROM seasons WHERE series_id = ~p AND season_id = ~p;", - [SeriesId, SeasonId] - ), - - [FirstAiredRow] = selected_rows(Ref, multistep_step1, Query1), - [Date] = row_values(FirstAiredRow), - FromDate = list_to_integer(Date), - - ToDate = FromDate + 15, - - Query2 = io_lib:format( - "SELECT CAST(season_id AS Utf8) AS season_id, CAST(episode_id AS Utf8) AS episode_id, title, CAST(air_date AS Utf8) AS air_date FROM episodes " - "WHERE series_id = ~p AND air_date >= ~p AND air_date <= ~p;", - [SeriesId, FromDate, ToDate] - ), - - Rows = selected_rows(Ref, multistep_step2, Query2), - lists:foreach(fun(Row) -> - [SId, EId, Title, AirDate] = row_values(Row), - io:format("Episode: S~pE~p ~p (aired: ~p)~n", [SId, EId, Title, AirDate]) - end, Rows). - -select_seasons_by_series(Ref) -> - SeriesList = [1, 2], - InClause = string:join([integer_to_list(X) || X <- SeriesList], ", "), - - Query = io_lib:format( - "SELECT CAST(series_id AS Utf8) AS series_id, CAST(season_id AS Utf8) AS season_id, title, CAST(first_aired AS Date) AS first_aired " - "FROM seasons WHERE series_id IN (~s) ORDER BY season_id;", - [InClause] - ), - - Rows = selected_rows(Ref, select_seasons_by_series, Query), - lists:foreach(fun(Row) -> - [SeriesId, SeasonId, Title, FirstAired] = row_values(Row), - io:format("Season: Series=~p, Season=~p, Title=~p, FirstAired=~p~n", - [SeriesId, SeasonId, Title, FirstAired]) - end, Rows). - -drop_tables(Ref) -> - Tables = ["series", "seasons", "episodes"], - - lists:foreach(fun(Table) -> - Query = io_lib:format("DROP TABLE ~s;", [Table]), - case odbc:sql_query(Ref, lists:flatten(Query)) of - {updated, _} -> ok; - {error, _} -> ok - end - end, Tables). - -escape_string(String) -> - EscapedBackslash = string:replace(String, "\\", "\\\\", all), - lists:flatten(string:replace(EscapedBackslash, "\"", "\\\"", all)). - -execute_update(Ref, Query) -> - case odbc:sql_query(Ref, lists:flatten(Query)) of - {updated, _} -> ok; - Error -> throw({query_failed, update, Error}) - end. - -selected_rows(Ref, Step, Query) -> - case odbc:sql_query(Ref, lists:flatten(Query)) of - {selected, _, Rows} -> Rows; - Error -> throw({query_failed, Step, Error}) - end. - -selected_param_rows(Ref, Step, Query, Params) -> - case odbc:param_query(Ref, lists:flatten(Query), Params) of - {selected, _, Rows} -> Rows; - Error -> throw({query_failed, Step, Error}) - end. - -row_values(Row) when is_tuple(Row) -> - tuple_to_list(Row); -row_values(Row) -> - Row. diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index e5fef46a7f0..a74800a52d2 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -48,14 +48,3 @@ install(DIRECTORY include/ add_subdirectory(examples) add_subdirectory(tests) - -install(FILES - odbcinst.ini - DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d - RENAME ydb-odbc.ini -) - -install(FILES - odbc.ini - DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} -) diff --git a/odbc/README.md b/odbc/README.md index e5b4b4f0838..826666f30aa 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -20,49 +20,31 @@ The shared library is produced as `build/odbc/libydb-odbc.so`. ## Install -After configure and build, CMake generates `build/odbc/odbcinst.ini` with the correct path to `libydb-odbc.so`. - -Register the driver with unixODBC: - -```bash -sudo odbcinst -i -d -f build/odbc/odbcinst.ini -``` - -Add a DSN — either copy the sample into the system config: - -```bash -sudo cp odbc/odbc.ini /etc/odbc.ini -# edit Server, Database, etc. -``` - -or point applications at the sample in the repo: - ```bash -export ODBCINI=/absolute/path/to/ydb-cpp-sdk/odbc/odbc.ini +cmake --install build --prefix /usr/local ``` - ## Configuration -1. Make sure the driver is registered: - -```bash -odbcinst -q -d -``` - -You should see an entry named `YDB`. +For `SQLConnect("YDB", ...)`, `isql -v YDB`, or `Driver=YDB`. -2. Check available data sources: +**`odbcinst.ini`** — driver registration. 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`. -```bash -odbcinst -q -s +```ini +[YDB] +Description=YDB ODBC Driver +Driver=/path/to/libydb-odbc.so +Setup=/path/to/libydb-odbc.so ``` -3. Edit `/etc/odbc.ini` (or your `ODBCINI` file) to configure the connection: +**`odbc.ini`** — DSN named `YDB`. In section `[YDB]`: `Driver` is the registered driver name, `Server` is the YDB endpoint, `Database` is the database path. Use `/etc/odbc.ini` or set `ODBCINI` to your file path. + ```ini +[ODBC Data Sources] +YDB=YDB ODBC Driver + [YDB] Driver=YDB -Description=YDB Database Connection Server=localhost:2136 Database=/local ``` diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index a3769588558..7e85e31923a 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -277,30 +277,20 @@ const std::string& TConnection::GetDbmsVersion() { std::optional fetched; const NYdb::TStatus status = client->RetryQuerySync( [&fetched](NQuery::TSession session) -> NYdb::TStatus { - auto iterator = session.StreamExecuteQuery( + auto result = session.ExecuteQuery( "SELECT Version();", NQuery::TTxControl::NoTx(), NYdb::TParamsBuilder().Build()).ExtractValueSync(); - if (!iterator.IsSuccess()) { - return NYdb::TStatus(iterator.GetStatus(), NYdb::NIssue::TIssues(iterator.GetIssues())); + if (!result.IsSuccess()) { + return result; } - while (true) { - auto part = iterator.ReadNext().ExtractValueSync(); - if (part.EOS()) { - break; - } - if (!part.IsSuccess()) { - return NYdb::TStatus(part.GetStatus(), NYdb::NIssue::TIssues(part.GetIssues())); - } - if (!part.HasResultSet()) { - continue; - } - TResultSetParser parser(part.ExtractResultSet()); - if (parser.TryNextRow()) { - fetched = parser.ColumnParser(0).GetUtf8(); - } + if (result.GetResultSets().empty()) { return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); } + TResultSetParser parser(result.GetResultSetParser(0)); + if (parser.TryNextRow()) { + fetched = parser.ColumnParser(0).GetUtf8(); + } return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }); diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 729c6ee0778..8d9f3176aea 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -1,2 +1,30 @@ +set(YDB_ODBC_TEST_CONFIG_DIR "${CMAKE_BINARY_DIR}/odbc") +file(MAKE_DIRECTORY "${YDB_ODBC_TEST_CONFIG_DIR}") + +set(YDB_ODBC_DSN_SERVER "localhost:2136" CACHE STRING + "YDB endpoint in odbc.ini generated for ODBC integration tests") +set(YDB_ODBC_DSN_DATABASE "/local" CACHE STRING + "YDB database path in odbc.ini generated for ODBC integration tests") + +file(GENERATE + OUTPUT "${YDB_ODBC_TEST_CONFIG_DIR}/odbcinst.ini" + CONTENT "[YDB] +Description=YDB ODBC Driver +Driver=$ +Setup=$ +" +) + +file(WRITE "${YDB_ODBC_TEST_CONFIG_DIR}/odbc.ini" +"[ODBC Data Sources] +YDB=YDB ODBC Driver + +[YDB] +Driver=YDB +Description=YDB Database Connection +Server=${YDB_ODBC_DSN_SERVER} +Database=${YDB_ODBC_DSN_DATABASE} +") + add_subdirectory(integration) add_subdirectory(unit) From 60197b1b00ad3232d4d7a695236101684a3ff159 Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sat, 23 May 2026 03:41:20 +0300 Subject: [PATCH 30/55] refactor exec cursor prefetch and test CMake --- odbc/src/connection.cpp | 12 ++--- odbc/src/statement.cpp | 67 ++++++++---------------- odbc/src/utils/cursor.cpp | 74 +++++++++++++++++++-------- odbc/src/utils/cursor.h | 17 ++++-- odbc/tests/integration/CMakeLists.txt | 1 - odbc/tests/unit/CMakeLists.txt | 5 +- odbc/tests/unit/convert_ut.cpp | 2 +- 7 files changed, 96 insertions(+), 82 deletions(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 7e85e31923a..91d632aaa66 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -266,12 +266,9 @@ const std::string& TConnection::GetDbmsVersion() { return *DbmsVersionCache_; } - static const std::string fallback = "unknown"; - DbmsVersionCache_ = fallback; - auto* client = GetClient(); if (!client) { - return *DbmsVersionCache_; + throw TOdbcException("08003", 0, "Connection is not established"); } std::optional fetched; @@ -294,9 +291,12 @@ const std::string& TConnection::GetDbmsVersion() { return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }); - if (status.IsSuccess() && fetched && !fetched->empty()) { - DbmsVersionCache_ = std::move(*fetched); + NStatusHelpers::ThrowOnError(status); + if (!fetched || fetched->empty()) { + throw TOdbcException("HY000", 0, "Failed to retrieve DBMS version"); } + + DbmsVersionCache_ = std::move(*fetched); return *DbmsVersionCache_; } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 04e146f591b..4115dc93c84 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -53,25 +53,6 @@ namespace { NYdb::TStatus StatusFrom(const NYdb::TStatus& ydb_status) { return NYdb::TStatus(ydb_status.GetStatus(), NYdb::NIssue::TIssues(ydb_status.GetIssues())); } - - NYdb::TStatus PrefetchFirstPartStatus(NQuery::TExecuteQueryIterator& iterator, std::optional* prefetchedResultPart){ - prefetchedResultPart->reset(); - while (true) { - auto part = iterator.ReadNext().ExtractValueSync(); - if (part.EOS()) { - break; - } - if (!part.IsSuccess()) { - return StatusFrom(part); - - } - if (part.HasResultSet()) { - prefetchedResultPart->emplace(std::move(part)); - return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); - } - } - return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); - } } TStatement::TStatement(TConnection* conn) @@ -103,49 +84,41 @@ SQLRETURN TStatement::Execute() { return buildRc; } - std::optional iterator; - std::optional prefetchedResultPart; - - if (Conn_->GetAutocommit()){ + if (Conn_->GetAutocommit()) { Conn_->ResetTx(); Conn_->ResetQuerySession(); - const NYdb::NRetry::TRetryOperationSettings retrySettings = - MakeAutocommitRetrySettings(); - - NYdb::TStatus execStatus = client->RetryQuerySync( - [this, ¶ms, &iterator, &prefetchedResultPart](NQuery::TSession session) -> NYdb::TStatus{ - auto retry_iterator = CreateExecuteIterator(session, params); - if (!retry_iterator.IsSuccess()) { - return StatusFrom(retry_iterator); + const NYdb::NRetry::TRetryOperationSettings retrySettings = MakeAutocommitRetrySettings(); + + const NYdb::TStatus execStatus = client->RetryQuerySync( + [this, ¶ms](NQuery::TSession session) -> NYdb::TStatus { + auto retryIterator = CreateExecuteIterator(session, params); + if (!retryIterator.IsSuccess()) { + return StatusFrom(retryIterator); } - std::optional retry_prefetched; - const NYdb::TStatus prefetchStatus = PrefetchFirstPartStatus(retry_iterator, &retry_prefetched); - if (!prefetchStatus.IsSuccess()) { - return prefetchStatus; + TExecCursorCreateResult created = TryCreateExecCursor(this, std::move(retryIterator)); + if (!created.Status.IsSuccess()) { + return created.Status; } - iterator.emplace(std::move(retry_iterator)); - prefetchedResultPart = std::move(retry_prefetched); + Cursor_ = std::move(created.Cursor); return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); - }, retrySettings); + }, + retrySettings); NStatusHelpers::ThrowOnError(execStatus); } else { NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); - iterator.emplace(CreateExecuteIterator(session, params)); - NStatusHelpers::ThrowOnError(*iterator); - NStatusHelpers::ThrowOnError(PrefetchFirstPartStatus(*iterator, &prefetchedResultPart)); - } - - if (prefetchedResultPart) { - Cursor_ = CreateExecCursor(this, std::move(*iterator), std::move(prefetchedResultPart)); - } else { - Cursor_.reset(); + auto iterator = CreateExecuteIterator(session, params); + NStatusHelpers::ThrowOnError(iterator); + TExecCursorCreateResult created = TryCreateExecCursor(this, std::move(iterator)); + NStatusHelpers::ThrowOnError(created.Status); + Cursor_ = std::move(created.Cursor); } return SQL_SUCCESS; } NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() { NYdb::NRetry::TRetryOperationSettings settings; + settings.Idempotent(true); SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { const TDuration deadline = TDuration::Seconds(queryTimeoutSec); diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index aa2268c21e1..533f0b20217 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -8,16 +8,43 @@ namespace NYdb { namespace NOdbc { +namespace { + +NYdb::TStatus StatusFrom(const NYdb::TStatus& ydbStatus) { + return NYdb::TStatus(ydbStatus.GetStatus(), NYdb::NIssue::TIssues(ydbStatus.GetIssues())); +} + +NYdb::TStatus PrefetchFirstResultSet( + NQuery::TExecuteQueryIterator& iterator, + std::optional* resultSet) { + resultSet->reset(); + while (true) { + auto part = iterator.ReadNext().ExtractValueSync(); + if (part.EOS()) { + break; + } + if (!part.IsSuccess()) { + return StatusFrom(part); + } + if (part.HasResultSet()) { + resultSet->emplace(part.ExtractResultSet()); + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); + } + } + return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); +} + +} // namespace + class TExecCursor : public ICursor { public: TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator, - std::optional prefetchedPart) + std::optional firstResultSet) : BindingFiller_(bindingFiller) , Iterator_(std::move(iterator)) - , PrefetchedPart_(std::move(prefetchedPart)) { - if (PrefetchedPart_ && PrefetchedPart_->HasResultSet()) { - FillColumnsMeta(PrefetchedPart_->GetResultSet()); + if (firstResultSet) { + InitResultSet(std::move(*firstResultSet)); } } @@ -30,14 +57,7 @@ class TExecCursor : public ICursor { } ResultSetParser_.reset(); } - NQuery::TExecuteQueryPart part = [&]() { - if (PrefetchedPart_) { - auto p = std::move(*PrefetchedPart_); - PrefetchedPart_.reset(); - return p; - } - return Iterator_.ReadNext().ExtractValueSync(); - }(); + NQuery::TExecuteQueryPart part = Iterator_.ReadNext().ExtractValueSync(); if (part.EOS()) { return false; } @@ -46,10 +66,7 @@ class TExecCursor : public ICursor { return false; } if (part.HasResultSet()) { - TResultSet resultSet = part.ExtractResultSet(); - Columns_.clear(); - FillColumnsMeta(resultSet); - ResultSetParser_ = std::make_unique(resultSet); + InitResultSet(part.ExtractResultSet()); } } return false; @@ -71,6 +88,12 @@ class TExecCursor : public ICursor { } private: + void InitResultSet(TResultSet resultSet) { + Columns_.clear(); + FillColumnsMeta(resultSet); + ResultSetParser_ = std::make_unique(std::move(resultSet)); + } + void FillColumnsMeta(const TResultSet& resultSet) { for (const auto& col : resultSet.GetColumnsMeta()) { const SQLSMALLINT sqlType = GetTypeId(col.Type); @@ -85,7 +108,6 @@ class TExecCursor : public ICursor { IBindingFiller* BindingFiller_; NQuery::TExecuteQueryIterator Iterator_; - std::optional PrefetchedPart_; std::unique_ptr ResultSetParser_; std::vector Columns_; }; @@ -130,10 +152,20 @@ class TVirtualCursor : public ICursor { int64_t Cursor_ = -1; }; -std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, - NQuery::TExecuteQueryIterator iterator, - std::optional prefetchedPart) { - return std::make_unique(bindingFiller, std::move(iterator), std::move(prefetchedPart)); +TExecCursorCreateResult TryCreateExecCursor( + IBindingFiller* bindingFiller, + NQuery::TExecuteQueryIterator iterator) { + std::optional firstResultSet; + const NYdb::TStatus prefetchStatus = PrefetchFirstResultSet(iterator, &firstResultSet); + if (!prefetchStatus.IsSuccess()) { + return {prefetchStatus, nullptr}; + } + if (!firstResultSet) { + return {NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()), nullptr}; + } + return { + NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()), + std::make_unique(bindingFiller, std::move(iterator), std::move(firstResultSet))}; } std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) { diff --git a/odbc/src/utils/cursor.h b/odbc/src/utils/cursor.h index 41f8e03ff16..4fa2682e59b 100644 --- a/odbc/src/utils/cursor.h +++ b/odbc/src/utils/cursor.h @@ -3,13 +3,14 @@ #include "bindings.h" #include +#include #include +#include #include #include #include -#include namespace NYdb { namespace NOdbc { @@ -26,16 +27,22 @@ using TTable = std::vector>; class ICursor { public: - virtual ~ICursor() = default; + virtual ~ICursor() = default; virtual bool Fetch() = 0; virtual SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; virtual const std::vector& GetColumnMeta() const = 0; }; -std::unique_ptr CreateExecCursor(IBindingFiller* bindingFiller, - NYdb::NQuery::TExecuteQueryIterator iterator, - std::optional prefetchedPart = std::nullopt); +struct TExecCursorCreateResult { + NYdb::TStatus Status; + std::unique_ptr Cursor; +}; + +TExecCursorCreateResult TryCreateExecCursor( + IBindingFiller* bindingFiller, + NYdb::NQuery::TExecuteQueryIterator iterator); + std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table); } // namespace NOdbc diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 3e9a4fcf80d..19a7004f16a 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -8,7 +8,6 @@ add_odbc_test(NAME odbc-environment_api_it environment_api_it.cpp ) -# Core API Tests add_odbc_test(NAME odbc-connection_api_it SOURCES connection_api_it.cpp diff --git a/odbc/tests/unit/CMakeLists.txt b/odbc/tests/unit/CMakeLists.txt index d23e837d2f3..006671ce933 100644 --- a/odbc/tests/unit/CMakeLists.txt +++ b/odbc/tests/unit/CMakeLists.txt @@ -1,10 +1,13 @@ add_ydb_test(NAME odbc-convert_ut GTEST SOURCES convert_ut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/convert.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../../src LINK_LIBRARIES yutil + YDB-CPP-SDK::Params api-protos - ydb-odbc LABELS unit ) diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp index 16edf49be21..86b0d3d5be3 100644 --- a/odbc/tests/unit/convert_ut.cpp +++ b/odbc/tests/unit/convert_ut.cpp @@ -1,4 +1,4 @@ -#include +#include "utils/convert.h" #undef BOOL #include From b7e39656229cbdc10996cfa892930404e5e338fa Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sat, 23 May 2026 04:04:25 +0300 Subject: [PATCH 31/55] mv examples odbc --- odbc/examples/erlang_client/Makefile | 50 +++++++++++++++++++ .../examples}/erlang_client/README.md | 0 .../erlang_client/src/sample_data.erl | 0 .../erlang_client/src/ydb_series_client.erl | 0 4 files changed, 50 insertions(+) create mode 100644 odbc/examples/erlang_client/Makefile rename {examples/odbc => odbc/examples}/erlang_client/README.md (100%) rename {examples/odbc => odbc/examples}/erlang_client/src/sample_data.erl (100%) rename {examples/odbc => odbc/examples}/erlang_client/src/ydb_series_client.erl (100%) diff --git a/odbc/examples/erlang_client/Makefile b/odbc/examples/erlang_client/Makefile new file mode 100644 index 00000000000..5f402ef9720 --- /dev/null +++ b/odbc/examples/erlang_client/Makefile @@ -0,0 +1,50 @@ +.PHONY: all compile run run-shell clean distclean check help + +CONN ?= Driver=YDB;Endpoint=localhost:2136;Database=/local; +ERLC ?= erlc +ERL ?= erl + +all: compile + +help: + @echo "YDB Series Example - Erlang ODBC" + @echo "" + @echo "Targets:" + @echo " make compile Compile Erlang modules" + @echo " make run Run the example" + @echo " make run CONN='...' Run with a custom connection string" + @echo " make run-shell Start Erlang shell with compiled modules" + @echo " make check Check Erlang ODBC availability" + @echo " make clean Remove compiled files" + @echo "" + @echo "Examples:" + @echo " make run" + @echo " make run CONN=\"Driver=YDB;Endpoint=myhost:2136;Database=/mydb;\"" + +prepare: + @mkdir -p ebin + +compile: prepare + @echo "Compiling Erlang modules..." + $(ERLC) -o ebin src/*.erl + @echo "Done." + +run: compile + @echo "Running YDB Series Example..." + $(ERL) -pa ebin -noshell -eval 'application:load(odbc), application:start(odbc), ydb_series_client:run("$(CONN)"), halt().' + +run-shell: compile + @echo "Starting Erlang shell with ydb_series_client..." + $(ERL) -pa ebin + +check: + @echo "Checking ODBC support in Erlang..." + @$(ERL) -noshell -eval 'application:load(odbc), io:format("~p~n", [application:start(odbc)]), halt().' + +clean: + @rm -rf ebin/*.beam + @rm -rf *.beam + @rm -rf erl_crash.dump + +distclean: clean + @rm -rf ebin diff --git a/examples/odbc/erlang_client/README.md b/odbc/examples/erlang_client/README.md similarity index 100% rename from examples/odbc/erlang_client/README.md rename to odbc/examples/erlang_client/README.md diff --git a/examples/odbc/erlang_client/src/sample_data.erl b/odbc/examples/erlang_client/src/sample_data.erl similarity index 100% rename from examples/odbc/erlang_client/src/sample_data.erl rename to odbc/examples/erlang_client/src/sample_data.erl diff --git a/examples/odbc/erlang_client/src/ydb_series_client.erl b/odbc/examples/erlang_client/src/ydb_series_client.erl similarity index 100% rename from examples/odbc/erlang_client/src/ydb_series_client.erl rename to odbc/examples/erlang_client/src/ydb_series_client.erl From 99582c8cdd05bb89334386cac76c940107a24dfe Mon Sep 17 00:00:00 2001 From: Ylonies Date: Sun, 24 May 2026 23:57:33 +0300 Subject: [PATCH 32/55] odbcinst.ini on build and install --- odbc/CMakeLists.txt | 21 +++++++++++++++++++++ odbc/README.md | 6 +++++- odbc/odbcinst.ini.in | 4 ++++ odbc/tests/CMakeLists.txt | 9 --------- 4 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 odbc/odbcinst.ini.in diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index a74800a52d2..d63ae83b7aa 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -38,6 +38,27 @@ set_target_properties(ydb-odbc PROPERTIES include(GNUInstallDirs) +file(GENERATE + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/odbcinst.ini" + CONTENT "[YDB] +Description=YDB ODBC Driver +Driver=$ +Setup=$ +" +) + +set(YDB_ODBC_DRIVER_PATH + "${CMAKE_INSTALL_FULL_LIBDIR}/libydb-odbc${CMAKE_SHARED_LIBRARY_SUFFIX}") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/odbcinst.ini.in" + "${CMAKE_CURRENT_BINARY_DIR}/ydb-odbc-odbcinst.ini" + @ONLY +) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ydb-odbc-odbcinst.ini" + DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/odbcinst.d" + RENAME ydb-odbc.ini +) + install(TARGETS ydb-odbc LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) diff --git a/odbc/README.md b/odbc/README.md index 826666f30aa..807c5de68cc 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -24,11 +24,15 @@ The shared library is produced as `build/odbc/libydb-odbc.so`. cmake --install build --prefix /usr/local ``` +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). + ## Configuration For `SQLConnect("YDB", ...)`, `isql -v YDB`, or `Driver=YDB`. -**`odbcinst.ini`** — driver registration. 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 (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`. ```ini [YDB] diff --git a/odbc/odbcinst.ini.in b/odbc/odbcinst.ini.in new file mode 100644 index 00000000000..8543d9adf73 --- /dev/null +++ b/odbc/odbcinst.ini.in @@ -0,0 +1,4 @@ +[YDB] +Description=YDB ODBC Driver +Driver=@YDB_ODBC_DRIVER_PATH@ +Setup=@YDB_ODBC_DRIVER_PATH@ diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 8d9f3176aea..8abcd081832 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -6,15 +6,6 @@ set(YDB_ODBC_DSN_SERVER "localhost:2136" CACHE STRING set(YDB_ODBC_DSN_DATABASE "/local" CACHE STRING "YDB database path in odbc.ini generated for ODBC integration tests") -file(GENERATE - OUTPUT "${YDB_ODBC_TEST_CONFIG_DIR}/odbcinst.ini" - CONTENT "[YDB] -Description=YDB ODBC Driver -Driver=$ -Setup=$ -" -) - file(WRITE "${YDB_ODBC_TEST_CONFIG_DIR}/odbc.ini" "[ODBC Data Sources] YDB=YDB ODBC Driver From 74c0da76e5e5689b37ecdf91629b37b8a9549bb8 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Mon, 8 Jun 2026 12:38:47 +0300 Subject: [PATCH 33/55] fix PIC for .so builds --- .github/actions/prepare_vm/action.yaml | 26 +++++++++++++++++++------- CMakeLists.txt | 4 ++++ README.md | 21 ++++++++++++++------- odbc/README.md | 12 ++++++++---- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/.github/actions/prepare_vm/action.yaml b/.github/actions/prepare_vm/action.yaml index 2e6f36d43ea..bddcf6a89ae 100644 --- a/.github/actions/prepare_vm/action.yaml +++ b/.github/actions/prepare_vm/action.yaml @@ -19,7 +19,8 @@ runs: sudo apt-get -y update sudo apt-get -y install git gdb ninja-build libidn11-dev ragel yasm libc-ares-dev libre2-dev \ rapidjson-dev zlib1g-dev libxxhash-dev libzstd-dev libsnappy-dev libgtest-dev libgmock-dev \ - libbz2-dev liblz4-dev libdouble-conversion-dev libssl-dev libstdc++-13-dev gcc-13 g++-13 + libbz2-dev liblz4-dev libdouble-conversion-dev libssl-dev libstdc++-13-dev gcc-13 g++-13 \ + unixodbc unixodbc-dev sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 10000 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 10000 @@ -28,6 +29,8 @@ runs: if: ${{ inputs.mode == 'build' }} shell: bash run: | + # Static deps are linked into the shared ODBC driver; they must be built with -fPIC. + export PIC="-DCMAKE_POSITION_INDEPENDENT_CODE=ON" # Install ccache (V=4.8.1; curl -L https://github.com/ccache/ccache/releases/download/v${V}/ccache-${V}-linux-x86_64.tar.xz | \ sudo tar -xJ -C /usr/local/bin/ --strip-components=1 --no-same-owner ccache-${V}-linux-x86_64/ccache) @@ -48,7 +51,7 @@ runs: tar -xvzf abseil-cpp-20230802.0.tar.gz cd abseil-cpp-20230802.0 mkdir build && cd build - cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_BUILD_TYPE=Release -DABSL_PROPAGATE_CXX_STD=ON .. + cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_BUILD_TYPE=Release -DABSL_PROPAGATE_CXX_STD=ON ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/absl cd ../../ @@ -59,7 +62,7 @@ runs: cd protobuf-25.0 mkdir build && cd build cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_PREFIX_PATH="${HOME}/ydb_deps/absl" -DCMAKE_BUILD_TYPE=Release \ - -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_INSTALL=ON -Dprotobuf_ABSL_PROVIDER=package .. + -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_INSTALL=ON -Dprotobuf_ABSL_PROVIDER=package ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/protobuf cd ../../ @@ -68,7 +71,7 @@ runs: wget -O grpc-1.60.2.tar.gz https://github.com/grpc/grpc/archive/refs/tags/v1.60.2.tar.gz tar -xvzf grpc-1.60.2.tar.gz && cd grpc-1.60.2 mkdir build && cd build - cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_PREFIX_PATH="${HOME}/ydb_deps/absl;${HOME}/ydb_deps/protobuf" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 \ + cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_PREFIX_PATH="${HOME}/ydb_deps/absl;${HOME}/ydb_deps/protobuf" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 ${PIC} \ -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DgRPC_BUILD_CSHARP_EXT=OFF \ -DgRPC_ZLIB_PROVIDER=package -DgRPC_CARES_PROVIDER=package -DgRPC_RE2_PROVIDER=package \ -DgRPC_SSL_PROVIDER=package -DgRPC_PROTOBUF_PROVIDER=package -DgRPC_ABSL_PROVIDER=package \ @@ -78,11 +81,20 @@ runs: cmake --install . --config Release --prefix ~/ydb_deps/grpc cd ../../ + # Install base64 + wget -O base64-0.5.2.tar.gz https://github.com/aklomp/base64/archive/refs/tags/v0.5.2.tar.gz + tar -xvzf base64-0.5.2.tar.gz && cd base64-0.5.2 + mkdir build && cd build + cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_BUILD_TYPE=Release ${PIC} .. + cmake --build . --config Release + cmake --install . --config Release --prefix ~/ydb_deps/base64 + cd ../../ + # Install brotli wget -O brotli-1.1.0.tar.gz https://github.com/google/brotli/archive/refs/tags/v1.1.0.tar.gz tar -xvzf brotli-1.1.0.tar.gz && cd brotli-1.1.0 mkdir build && cd build - cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_BUILD_TYPE=Release .. + cmake -G Ninja ${ENABLE_CCACHE} -DCMAKE_BUILD_TYPE=Release ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/brotli cd ../../ @@ -90,5 +102,5 @@ runs: # Clean up ccache -s sudo rm -rf llvm.sh abseil-cpp-20230802.0.tar.gz protobuf-25.0.tar.gz grpc-1.60.2.tar.gz \ - brotli-1.1.0.tar.gz abseil-cpp-20230802.0 \ - protobuf-25.0 grpc-1.60.2 brotli-1.1.0 + base64-0.5.2.tar.gz brotli-1.1.0.tar.gz abseil-cpp-20230802.0 \ + protobuf-25.0 grpc-1.60.2 base64-0.5.2 brotli-1.1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cbd9471474..3ee545202cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,10 @@ option(YDB_SDK_ENABLE_OTEL_METRICS "Build OpenTelemetry metrics plugin" Off) option(YDB_SDK_ENABLE_OTEL_TRACE "Build OpenTelemetry trace plugin" Off) option(YDB_CPP_SDK_SLO_USE_INSTALLED_SDK "Build only SLO workloads against an installed ydb-cpp-sdk package" Off) option(YDB_SDK_ODBC "Build YDB ODBC driver" Off) +if (YDB_SDK_ODBC) + # ODBC driver is a shared library; static archives linked into it must be PIC. + set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE) +endif() set(YDB_SDK_GOOGLE_COMMON_PROTOS_TARGET "" CACHE STRING "Name of cmake target preparing google common proto library") option(YDB_SDK_USE_RAPID_JSON "Search for rapid json library in system" ON) diff --git a/README.md b/README.md index 41d1790d193..4f787127900 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,18 @@ match the dependency set published with that gRPC release: These pins are shared by regular CI builds, SLO workload images, and the development container. +When building with `YDB_SDK_ODBC=ON` (included in the `release-test-*` presets), the ODBC +driver is a shared library. Static dependencies installed under `~/ydb_deps` must be built +with position-independent code (`-DCMAKE_POSITION_INDEPENDENT_CODE=ON`). + ```bash sudo apt-get -y update sudo apt-get -y install git gdb ninja-build libidn11-dev ragel yasm libc-ares-dev libre2-dev \ rapidjson-dev zlib1g-dev libxxhash-dev libzstd-dev libsnappy-dev libgtest-dev libgmock-dev \ - libbz2-dev liblz4-dev libdouble-conversion-dev libssl-dev libstdc++-13-dev gcc-13 g++-13 + libbz2-dev liblz4-dev libdouble-conversion-dev libssl-dev libstdc++-13-dev gcc-13 g++-13 \ + unixodbc unixodbc-dev + +PIC="-DCMAKE_POSITION_INDEPENDENT_CODE=ON" wget https://apt.llvm.org/llvm.sh chmod u+x llvm.sh @@ -72,7 +79,7 @@ wget -O abseil-cpp-20230802.0.tar.gz https://github.com/abseil/abseil-cpp/archiv tar -xvzf abseil-cpp-20230802.0.tar.gz cd abseil-cpp-20230802.0 mkdir build && cd build -cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DABSL_PROPAGATE_CXX_STD=ON .. +cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DABSL_PROPAGATE_CXX_STD=ON ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/absl cd ../../ @@ -83,7 +90,7 @@ tar -xvzf protobuf-25.0.tar.gz cd protobuf-25.0 mkdir build && cd build cmake -G Ninja -DCMAKE_PREFIX_PATH="$HOME/ydb_deps/absl" -DCMAKE_BUILD_TYPE=Release \ - -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_INSTALL=ON -Dprotobuf_ABSL_PROVIDER=package .. + -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_INSTALL=ON -Dprotobuf_ABSL_PROVIDER=package ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/protobuf cd ../../ @@ -92,7 +99,7 @@ cd ../../ wget -O grpc-1.60.2.tar.gz https://github.com/grpc/grpc/archive/refs/tags/v1.60.2.tar.gz tar -xvzf grpc-1.60.2.tar.gz && cd grpc-1.60.2 mkdir build && cd build -cmake -G Ninja -DCMAKE_PREFIX_PATH="$HOME/ydb_deps/absl;$HOME/ydb_deps/protobuf" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 \ +cmake -G Ninja -DCMAKE_PREFIX_PATH="${HOME}/ydb_deps/absl;${HOME}/ydb_deps/protobuf" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 ${PIC} \ -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DgRPC_BUILD_CSHARP_EXT=OFF \ -DgRPC_ZLIB_PROVIDER=package -DgRPC_CARES_PROVIDER=package -DgRPC_RE2_PROVIDER=package \ -DgRPC_SSL_PROVIDER=package -DgRPC_PROTOBUF_PROVIDER=package -DgRPC_ABSL_PROVIDER=package \ @@ -106,7 +113,7 @@ cd ../../ wget -O base64-0.5.2.tar.gz https://github.com/aklomp/base64/archive/refs/tags/v0.5.2.tar.gz tar -xvzf base64-0.5.2.tar.gz && cd base64-0.5.2 mkdir build && cd build -cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .. +cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/base64 cd ../../ @@ -115,7 +122,7 @@ cd ../../ wget -O brotli-1.1.0.tar.gz https://github.com/google/brotli/archive/refs/tags/v1.1.0.tar.gz tar -xvzf brotli-1.1.0.tar.gz && cd brotli-1.1.0 mkdir build && cd build -cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \ +cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${PIC} \ -DCMAKE_INSTALL_PREFIX="$HOME/ydb_deps/brotli" .. cmake --build . --config Release cmake --install . --config Release @@ -125,7 +132,7 @@ cd ../../ wget -O jwt-cpp-0.7.0.tar.gz https://github.com/Thalhammer/jwt-cpp/archive/refs/tags/v0.7.0.tar.gz tar -xvzf jwt-cpp-0.7.0.tar.gz && cd jwt-cpp-0.7.0 mkdir build && cd build -cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .. +cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${PIC} .. cmake --build . --config Release cmake --install . --config Release --prefix ~/ydb_deps/jwt-cpp cd ../../ diff --git a/odbc/README.md b/odbc/README.md index 807c5de68cc..d109e1c4af1 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -6,14 +6,18 @@ ODBC driver for YDB. - CMake 3.10 or higher - C/C++ compiler with C11 and C++20 support -- YDB C++ SDK -- unixODBC (for Linux/macOS) +- YDB C++ SDK (build with `YDB_SDK_ODBC=ON`) +- unixODBC development packages (`unixodbc`, `unixodbc-dev` on Debian/Ubuntu) + +Static dependencies under `~/ydb_deps` must be built with +`-DCMAKE_POSITION_INDEPENDENT_CODE=ON` when linking the shared ODBC driver. See the +main [README](../README.md) dependency install section. ## Build ```bash -cmake -DYDB_SDK_ODBC=1 --preset release-test-clang -cmake --build --preset default +cmake --preset release-test-clang +cmake --build build --target ydb-odbc -j$(nproc) ``` The shared library is produced as `build/odbc/libydb-odbc.so`. From 8c456b4868c7cb69d018def4b0ba9dec9259db4c Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 10 Jun 2026 14:24:34 +0300 Subject: [PATCH 34/55] fix odbc driver binding behaviour --- cmake/testing.cmake | 10 +- odbc/CMakeLists.txt | 2 + odbc/README.md | 3 +- odbc/src/statement.cpp | 9 +- odbc/src/utils/escape.cpp | 51 +--------- odbc/src/utils/param_rewrite.cpp | 99 ++++++++++++++++++ odbc/src/utils/param_rewrite.h | 22 ++++ odbc/src/utils/sql_type_map.cpp | 106 ++++++++++++++++++++ odbc/src/utils/sql_type_map.h | 14 +++ odbc/tests/integration/statement_api_it.cpp | 29 ++++++ odbc/tests/integration/test_utils.h | 7 ++ odbc/tests/unit/CMakeLists.txt | 14 +++ odbc/tests/unit/param_rewrite_ut.cpp | 41 ++++++++ 13 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 odbc/src/utils/param_rewrite.cpp create mode 100644 odbc/src/utils/param_rewrite.h create mode 100644 odbc/src/utils/sql_type_map.cpp create mode 100644 odbc/src/utils/sql_type_map.h create mode 100644 odbc/tests/unit/param_rewrite_ut.cpp diff --git a/cmake/testing.cmake b/cmake/testing.cmake index 8a69fd184d8..fa406b2ff13 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -78,10 +78,9 @@ function(add_ydb_test) endif() if (YDB_TEST_GTEST) - set(env_vars "") + set(test_environment "YDB_TEST_ROOT=sdk_tests") foreach(env_var ${YDB_TEST_ENV}) - list(APPEND env_vars "ENVIRONMENT") - list(APPEND env_vars ${env_var}) + string(APPEND test_environment ";${env_var}") endforeach() gtest_discover_tests(${YDB_TEST_NAME} @@ -89,8 +88,7 @@ function(add_ydb_test) WORKING_DIRECTORY ${YDB_TEST_WORKING_DIRECTORY} PROPERTIES LABELS ${YDB_TEST_LABELS} - ENVIRONMENT "YDB_TEST_ROOT=sdk_tests" - ${env_vars} + ENVIRONMENT "${test_environment}" ) target_link_libraries(${YDB_TEST_NAME} PRIVATE @@ -149,6 +147,8 @@ if (YDB_SDK_ODBC) target_compile_definitions(${ODBC_TEST_NAME} PRIVATE ODBC_DRIVER_PATH="$" + ODBC_TEST_ODBCINI="${CMAKE_BINARY_DIR}/odbc/odbc.ini" + ODBC_TEST_ODBCSYSINI="${CMAKE_BINARY_DIR}/odbc" ) add_dependencies(${ODBC_TEST_NAME} ydb-odbc) diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index d63ae83b7aa..d8b37da64f8 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(ydb-odbc SHARED src/utils/attr.cpp src/utils/escape.cpp + src/utils/sql_type_map.cpp + src/utils/param_rewrite.cpp src/utils/cursor.cpp src/utils/types.cpp src/utils/util.cpp diff --git a/odbc/README.md b/odbc/README.md index d109e1c4af1..13ad517fd6d 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -99,7 +99,8 @@ SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT ## Parameters -Use names $p1, $p2, ... for parameter names +`?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` +from `SQLBindParameter` types. YDB-native `$pN` syntax also works. ## License diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 4115dc93c84..ba255fd43be 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -4,6 +4,7 @@ #include "utils/types.h" #include "utils/error_manager.h" #include "utils/escape.h" +#include "utils/param_rewrite.h" #include "utils/sql_like.h" #include @@ -128,10 +129,14 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() } NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ - const std::string sqlText = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON + const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ : RewriteOdbcEscapes(PreparedQuery_); - const std::string queryText = Conn_->WrapQueryForCurrentCatalog(sqlText); + const TParamRewriteResult rewritten = RewriteOdbcQuestionMarks(sqlAfterEscapes, BoundParams_); + if (!rewritten.Success) { + throw TOdbcException(rewritten.SqlState, 0, rewritten.Message); + } + const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { diff --git a/odbc/src/utils/escape.cpp b/odbc/src/utils/escape.cpp index 5a9c643eb7a..f6ab7dcf9b6 100644 --- a/odbc/src/utils/escape.cpp +++ b/odbc/src/utils/escape.cpp @@ -1,10 +1,10 @@ #include "escape.h" +#include "sql_type_map.h" #include #include #include #include -#include namespace NYdb::NOdbc { namespace { @@ -97,55 +97,6 @@ std::string NormalizeOdbcTimestampLiteral(const std::string& raw) { return normalized; } -std::string ToUpperAscii(std::string_view sv) { - std::string upper; - upper.resize(sv.size()); - std::transform(sv.begin(), sv.end(), upper.begin(), [](unsigned char byte) { - return static_cast(std::toupper(byte)); - }); - return upper; -} - -std::string MapSqlTypeToken(std::string_view sqlType) { - static const std::unordered_map kMap = { - {"CHAR", "Utf8"}, - {"VARCHAR", "Utf8"}, - {"LONGVARCHAR", "Utf8"}, - {"WCHAR", "Utf8"}, - {"WVARCHAR", "Utf8"}, - {"WLONGVARCHAR", "Utf8"}, - {"BIT", "Bool"}, - {"TINYINT", "Int8"}, - {"SMALLINT", "Int16"}, - {"INTEGER", "Int32"}, - {"BIGINT", "Int64"}, - {"REAL", "Float"}, - {"FLOAT", "Double"}, - {"DOUBLE", "Double"}, - {"DECIMAL", "Decimal(22, 9)"}, - {"NUMERIC", "Decimal(22, 9)"}, - {"BINARY", "String"}, - {"VARBINARY", "String"}, - {"LONGVARBINARY", "String"}, - {"DATE", "Date"}, - {"TIME", "Time"}, - {"TIMESTAMP", "Datetime"}, - {"TYPE_DATE", "Date"}, - {"TYPE_TIME", "Time"}, - {"TYPE_TIMESTAMP", "Datetime"}, - }; - std::string key = ToUpperAscii(sqlType); - const std::string kSql = "SQL_"; - if (key.size() > kSql.size() && key.compare(0, kSql.size(), kSql) == 0) { - key.erase(0, kSql.size()); - } - const auto mapped = kMap.find(key); - if (mapped != kMap.end()) { - return mapped->second; - } - return key; -} - std::string RewriteOdbcEscapesImpl(std::string_view sql); diff --git a/odbc/src/utils/param_rewrite.cpp b/odbc/src/utils/param_rewrite.cpp new file mode 100644 index 00000000000..d556094abcc --- /dev/null +++ b/odbc/src/utils/param_rewrite.cpp @@ -0,0 +1,99 @@ +#include "param_rewrite.h" +#include "sql_type_map.h" + +#include +#include +#include + +namespace NYdb::NOdbc { + +namespace { + +bool IsParamMark(std::string_view sql, size_t i) { + if (sql[i] != '?') { + return false; + } + if (i > 0) { + const char prev = sql[i - 1]; + if (std::isalnum(static_cast(prev)) || prev == '_' || prev == ')') { + return false; + } + } + return true; +} + +SQLSMALLINT FindParamType(const std::vector& params, SQLUSMALLINT index) { + for (const auto& param : params) { + if (param.ParamNumber == index) { + return param.ParameterType; + } + } + return 0; +} + +} // namespace + +TParamRewriteResult RewriteOdbcQuestionMarks( + std::string_view sql, + const std::vector& boundParams) { + std::string body; + body.reserve(sql.size()); + size_t paramCount = 0; + bool inQuote = false; + size_t braceDepth = 0; + + for (size_t i = 0; i < sql.size(); ++i) { + const char ch = sql[i]; + if (inQuote) { + body.push_back(ch); + if (ch == '\'' && i + 1 < sql.size() && sql[i + 1] == '\'') { + body.push_back('\''); + ++i; + } else if (ch == '\'') { + inQuote = false; + } + continue; + } + if (ch == '\'') { + inQuote = true; + body.push_back(ch); + continue; + } + if (ch == '{') { + ++braceDepth; + body.push_back(ch); + continue; + } + if (ch == '}' && braceDepth > 0) { + --braceDepth; + body.push_back(ch); + continue; + } + if (braceDepth == 0 && IsParamMark(sql, i)) { + body += "$p"; + body += std::to_string(++paramCount); + continue; + } + body.push_back(ch); + } + + if (paramCount == 0) { + return {.Sql = std::string(sql)}; + } + if (paramCount != boundParams.size()) { + return {.Success = false, .SqlState = "07002", .Message = "COUNT field incorrect"}; + } + + std::string declares; + for (size_t idx = 1; idx <= paramCount; ++idx) { + const SQLSMALLINT type = FindParamType(boundParams, static_cast(idx)); + if (!type) { + return {.Success = false, .SqlState = "07002", .Message = "COUNT field incorrect"}; + } + declares += "DECLARE $p" + std::to_string(idx) + " AS " + FormatYqlParamDeclareType(type) + ";\n"; + } + + return {.Sql = declares + body}; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/param_rewrite.h b/odbc/src/utils/param_rewrite.h new file mode 100644 index 00000000000..84748ad0869 --- /dev/null +++ b/odbc/src/utils/param_rewrite.h @@ -0,0 +1,22 @@ +#pragma once + +#include "bindings.h" + +#include +#include +#include + +namespace NYdb::NOdbc { + +struct TParamRewriteResult { + std::string Sql; + bool Success = true; + std::string SqlState; + std::string Message; +}; + +TParamRewriteResult RewriteOdbcQuestionMarks( + std::string_view sql, + const std::vector& boundParams); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/sql_type_map.cpp b/odbc/src/utils/sql_type_map.cpp new file mode 100644 index 00000000000..2685c6cbe23 --- /dev/null +++ b/odbc/src/utils/sql_type_map.cpp @@ -0,0 +1,106 @@ +#include "sql_type_map.h" + +#include +#include +#include +#include +#include + +namespace NYdb::NOdbc { + +namespace { + +std::string ToUpperAscii(std::string_view sv) { + std::string upper; + upper.resize(sv.size()); + std::transform(sv.begin(), sv.end(), upper.begin(), [](unsigned char byte) { + return static_cast(std::toupper(byte)); + }); + return upper; +} + +const std::unordered_map& SqlTypeTokenToYqlMap() { + static const std::unordered_map kMap = { + {"CHAR", "Utf8"}, + {"VARCHAR", "Utf8"}, + {"LONGVARCHAR", "Utf8"}, + {"WCHAR", "Utf8"}, + {"WVARCHAR", "Utf8"}, + {"WLONGVARCHAR", "Utf8"}, + {"BIT", "Bool"}, + {"TINYINT", "Int8"}, + {"SMALLINT", "Int16"}, + {"INTEGER", "Int32"}, + {"BIGINT", "Int64"}, + {"REAL", "Float"}, + {"FLOAT", "Double"}, + {"DOUBLE", "Double"}, + {"DECIMAL", "Decimal(22, 9)"}, + {"NUMERIC", "Decimal(22, 9)"}, + {"BINARY", "String"}, + {"VARBINARY", "String"}, + {"LONGVARBINARY", "String"}, + {"DATE", "Date"}, + {"TIME", "Time"}, + {"TIMESTAMP", "Datetime"}, + {"TYPE_DATE", "Date"}, + {"TYPE_TIME", "Time"}, + {"TYPE_TIMESTAMP", "Datetime"}, + }; + return kMap; +} + +const std::unordered_map& OdbcSqlTypeTokens() { + static const std::unordered_map kMap = { + {SQL_CHAR, "CHAR"}, + {SQL_VARCHAR, "VARCHAR"}, + {SQL_LONGVARCHAR, "LONGVARCHAR"}, + {SQL_WCHAR, "WCHAR"}, + {SQL_WVARCHAR, "WVARCHAR"}, + {SQL_WLONGVARCHAR, "WLONGVARCHAR"}, + {SQL_BIT, "BIT"}, + {SQL_TINYINT, "TINYINT"}, + {SQL_SMALLINT, "SMALLINT"}, + {SQL_INTEGER, "INTEGER"}, + {SQL_BIGINT, "BIGINT"}, + {SQL_REAL, "REAL"}, + {SQL_FLOAT, "FLOAT"}, + {SQL_DOUBLE, "DOUBLE"}, + {SQL_DECIMAL, "DECIMAL"}, + {SQL_NUMERIC, "NUMERIC"}, + {SQL_BINARY, "BINARY"}, + {SQL_VARBINARY, "VARBINARY"}, + {SQL_LONGVARBINARY, "LONGVARBINARY"}, + {SQL_TYPE_DATE, "TYPE_DATE"}, + {SQL_TYPE_TIME, "TYPE_TIME"}, + {SQL_TYPE_TIMESTAMP, "TYPE_TIMESTAMP"}, + }; + return kMap; +} + +} // namespace + +std::string MapSqlTypeToken(std::string_view sqlType) { + std::string key = ToUpperAscii(sqlType); + const std::string kSql = "SQL_"; + if (key.size() > kSql.size() && key.compare(0, kSql.size(), kSql) == 0) { + key.erase(0, kSql.size()); + } + const auto& map = SqlTypeTokenToYqlMap(); + const auto mapped = map.find(key); + if (mapped != map.end()) { + return mapped->second; + } + return key; +} + +std::string FormatYqlParamDeclareType(SQLSMALLINT sqlType) { + const auto& tokens = OdbcSqlTypeTokens(); + const auto tokenIt = tokens.find(sqlType); + const std::string yql = tokenIt != tokens.end() + ? MapSqlTypeToken(tokenIt->second) + : MapSqlTypeToken(std::to_string(sqlType)); + return yql + '?'; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/sql_type_map.h b/odbc/src/utils/sql_type_map.h new file mode 100644 index 00000000000..56549c39208 --- /dev/null +++ b/odbc/src/utils/sql_type_map.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +#include +#include + +namespace NYdb::NOdbc { + +std::string MapSqlTypeToken(std::string_view sqlType); +std::string FormatYqlParamDeclareType(SQLSMALLINT sqlType); + +} // namespace NYdb::NOdbc diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index 1413e40229a..869196f1237 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -84,6 +84,35 @@ TEST(StatementApi, ExecDirectInvalidTable) { SQLFreeHandle(SQL_HANDLE_ENV, env); } +TEST(StatementApi, PrepareAndExecuteWithQuestionMarks) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT ? + ? AS result", SQL_NTS), stmt, SQL_HANDLE_STMT); + + SQLINTEGER p1 = 10, p2 = 20; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &p1, 0, nullptr), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &p2, 0, nullptr), stmt, SQL_HANDLE_STMT); + + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLINTEGER result = 0; + SQLLEN resultInd = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &result, 0, &resultInd), SQL_SUCCESS); + ASSERT_EQ(result, 30); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + TEST(StatementApi, PrepareAndExecute) { SQLHENV env; SQLHDBC dbc; diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h index 362a836991b..a14cbac5f3a 100644 --- a/odbc/tests/integration/test_utils.h +++ b/odbc/tests/integration/test_utils.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -30,6 +31,12 @@ inline bool SqlStatePrefix(const std::string& diag, const char* state5) { } inline void AllocEnv(SQLHENV* env) { + static bool configured = false; + if (!configured) { + setenv("ODBCINI", ODBC_TEST_ODBCINI, 1); + setenv("ODBCSYSINI", ODBC_TEST_ODBCSYSINI, 1); + configured = true; + } ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, env), SQL_SUCCESS); ASSERT_EQ(SQLSetEnvAttr(*env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0), SQL_SUCCESS); } diff --git a/odbc/tests/unit/CMakeLists.txt b/odbc/tests/unit/CMakeLists.txt index 006671ce933..faeb5033333 100644 --- a/odbc/tests/unit/CMakeLists.txt +++ b/odbc/tests/unit/CMakeLists.txt @@ -16,6 +16,20 @@ add_ydb_test(NAME odbc-escape_ut GTEST SOURCES escape_ut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/escape.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/sql_type_map.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + LINK_LIBRARIES + yutil + LABELS + unit +) + +add_ydb_test(NAME odbc-param_rewrite_ut GTEST + SOURCES + param_rewrite_ut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/param_rewrite.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/sql_type_map.cpp INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../../src LINK_LIBRARIES diff --git a/odbc/tests/unit/param_rewrite_ut.cpp b/odbc/tests/unit/param_rewrite_ut.cpp new file mode 100644 index 00000000000..db04ef263ba --- /dev/null +++ b/odbc/tests/unit/param_rewrite_ut.cpp @@ -0,0 +1,41 @@ +#include "utils/bindings.h" +#include "utils/param_rewrite.h" + +#include + +using NYdb::NOdbc::RewriteOdbcQuestionMarks; +using NYdb::NOdbc::TBoundParam; + +namespace { + +TBoundParam IntParam(SQLUSMALLINT n) { + static SQLINTEGER value = 0; + return {n, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &value, 0, nullptr}; +} + +} // namespace + +TEST(OdbcParamRewrite, RewritesQuestionMarks) { + const std::vector params = {IntParam(1), IntParam(2)}; + const auto result = RewriteOdbcQuestionMarks("SELECT ? + ? AS result", params); + ASSERT_TRUE(result.Success); + EXPECT_EQ(result.Sql, + "DECLARE $p1 AS Int32?;\n" + "DECLARE $p2 AS Int32?;\n" + "SELECT $p1 + $p2 AS result"); +} + +TEST(OdbcParamRewrite, SkipsLiteralAndYqlOptionalSyntax) { + const std::vector params = {IntParam(1)}; + EXPECT_EQ(RewriteOdbcQuestionMarks("SELECT '?', ?", params).Sql, + "DECLARE $p1 AS Int32?;\nSELECT '?', $p1"); + EXPECT_EQ(RewriteOdbcQuestionMarks("DECLARE $p1 AS Int32?;\nSELECT $p1", params).Sql, + "DECLARE $p1 AS Int32?;\nSELECT $p1"); + EXPECT_EQ(RewriteOdbcQuestionMarks("SELECT $p1 + 10", params).Sql, "SELECT $p1 + 10"); +} + +TEST(OdbcParamRewrite, RejectsMismatchedBindCount) { + const auto result = RewriteOdbcQuestionMarks("SELECT ? + ?", {IntParam(1)}); + ASSERT_FALSE(result.Success); + EXPECT_EQ(result.SqlState, "07002"); +} From a1dce6cb70b633ddef9b76dfdac77dfb657c1528 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 10 Jun 2026 16:43:42 +0300 Subject: [PATCH 35/55] fix build --- odbc/src/utils/bindings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h index 2480f5367af..bd0b8836b74 100644 --- a/odbc/src/utils/bindings.h +++ b/odbc/src/utils/bindings.h @@ -3,7 +3,7 @@ #include #include -#include +#include namespace NYdb { namespace NOdbc { From 2da873e7cc1a53f4449476e7dc3dac3420ed6bf5 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Thu, 11 Jun 2026 11:55:42 +0300 Subject: [PATCH 36/55] fix the bind param feature --- cmake/testing.cmake | 4 +- odbc/src/utils/param_rewrite.cpp | 55 +++++++++++++++++++--------- odbc/tests/unit/param_rewrite_ut.cpp | 13 ++++++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/cmake/testing.cmake b/cmake/testing.cmake index fa406b2ff13..afc4d936958 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -79,8 +79,8 @@ function(add_ydb_test) if (YDB_TEST_GTEST) set(test_environment "YDB_TEST_ROOT=sdk_tests") - foreach(env_var ${YDB_TEST_ENV}) - string(APPEND test_environment ";${env_var}") + foreach(env_var IN LISTS YDB_TEST_ENV) + list(APPEND test_environment "${env_var}") endforeach() gtest_discover_tests(${YDB_TEST_NAME} diff --git a/odbc/src/utils/param_rewrite.cpp b/odbc/src/utils/param_rewrite.cpp index d556094abcc..d40a3aedabf 100644 --- a/odbc/src/utils/param_rewrite.cpp +++ b/odbc/src/utils/param_rewrite.cpp @@ -2,6 +2,7 @@ #include "sql_type_map.h" #include +#include #include #include @@ -22,13 +23,16 @@ bool IsParamMark(std::string_view sql, size_t i) { return true; } -SQLSMALLINT FindParamType(const std::vector& params, SQLUSMALLINT index) { - for (const auto& param : params) { - if (param.ParamNumber == index) { - return param.ParameterType; - } +bool TryParseDollarParam(std::string_view sql, size_t i, SQLUSMALLINT& index) { + if (sql.size() < i + 3 || sql[i] != '$' || sql[i + 1] != 'p' || !std::isdigit(static_cast(sql[i + 2]))) { + return false; + } + unsigned n = 0; + for (size_t j = i + 2; j < sql.size() && std::isdigit(static_cast(sql[j])); ++j) { + n = n * 10 + static_cast(sql[j] - '0'); } - return 0; + index = static_cast(n); + return true; } } // namespace @@ -38,9 +42,10 @@ TParamRewriteResult RewriteOdbcQuestionMarks( const std::vector& boundParams) { std::string body; body.reserve(sql.size()); - size_t paramCount = 0; + size_t questionMarkCount = 0; bool inQuote = false; size_t braceDepth = 0; + std::set paramIndices; for (size_t i = 0; i < sql.size(); ++i) { const char ch = sql[i]; @@ -69,30 +74,44 @@ TParamRewriteResult RewriteOdbcQuestionMarks( body.push_back(ch); continue; } - if (braceDepth == 0 && IsParamMark(sql, i)) { - body += "$p"; - body += std::to_string(++paramCount); - continue; + if (braceDepth == 0) { + if (IsParamMark(sql, i)) { + const auto index = static_cast(++questionMarkCount); + paramIndices.insert(index); + body += "$p"; + body += std::to_string(index); + continue; + } + SQLUSMALLINT index = 0; + if (TryParseDollarParam(sql, i, index)) { + paramIndices.insert(index); + } } body.push_back(ch); } - if (paramCount == 0) { + if (paramIndices.empty()) { return {.Sql = std::string(sql)}; } - if (paramCount != boundParams.size()) { + if (questionMarkCount > 0 && questionMarkCount != boundParams.size()) { return {.Success = false, .SqlState = "07002", .Message = "COUNT field incorrect"}; } std::string declares; - for (size_t idx = 1; idx <= paramCount; ++idx) { - const SQLSMALLINT type = FindParamType(boundParams, static_cast(idx)); - if (!type) { + for (const SQLUSMALLINT index : paramIndices) { + const std::string declare = "DECLARE $p" + std::to_string(index) + " AS"; + if (sql.find(declare) != std::string_view::npos) { + continue; + } + const auto bound = std::ranges::find(boundParams, index, &TBoundParam::ParamNumber); + if (bound == boundParams.end()) { return {.Success = false, .SqlState = "07002", .Message = "COUNT field incorrect"}; } - declares += "DECLARE $p" + std::to_string(idx) + " AS " + FormatYqlParamDeclareType(type) + ";\n"; + declares += declare + " " + FormatYqlParamDeclareType(bound->ParameterType) + ";\n"; + } + if (declares.empty()) { + return {.Sql = body}; } - return {.Sql = declares + body}; } diff --git a/odbc/tests/unit/param_rewrite_ut.cpp b/odbc/tests/unit/param_rewrite_ut.cpp index db04ef263ba..14ba9d8880e 100644 --- a/odbc/tests/unit/param_rewrite_ut.cpp +++ b/odbc/tests/unit/param_rewrite_ut.cpp @@ -31,7 +31,18 @@ TEST(OdbcParamRewrite, SkipsLiteralAndYqlOptionalSyntax) { "DECLARE $p1 AS Int32?;\nSELECT '?', $p1"); EXPECT_EQ(RewriteOdbcQuestionMarks("DECLARE $p1 AS Int32?;\nSELECT $p1", params).Sql, "DECLARE $p1 AS Int32?;\nSELECT $p1"); - EXPECT_EQ(RewriteOdbcQuestionMarks("SELECT $p1 + 10", params).Sql, "SELECT $p1 + 10"); + EXPECT_EQ(RewriteOdbcQuestionMarks("SELECT $p1 + 10", params).Sql, + "DECLARE $p1 AS Int32?;\nSELECT $p1 + 10"); +} + +TEST(OdbcParamRewrite, PrependsDeclareForNativeDollarParams) { + const std::vector params = {IntParam(1), IntParam(2)}; + const auto result = RewriteOdbcQuestionMarks("SELECT $p1 + $p2 AS result", params); + ASSERT_TRUE(result.Success); + EXPECT_EQ(result.Sql, + "DECLARE $p1 AS Int32?;\n" + "DECLARE $p2 AS Int32?;\n" + "SELECT $p1 + $p2 AS result"); } TEST(OdbcParamRewrite, RejectsMismatchedBindCount) { From a7b4e4f94307a38e7f498054a1f08bc7121918d7 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Thu, 11 Jun 2026 13:48:06 +0300 Subject: [PATCH 37/55] fix cmake pr regression --- cmake/testing.cmake | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmake/testing.cmake b/cmake/testing.cmake index afc4d936958..5f1df627085 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -78,9 +78,10 @@ function(add_ydb_test) endif() if (YDB_TEST_GTEST) - set(test_environment "YDB_TEST_ROOT=sdk_tests") + set(env_vars "") foreach(env_var IN LISTS YDB_TEST_ENV) - list(APPEND test_environment "${env_var}") + list(APPEND env_vars "ENVIRONMENT") + list(APPEND env_vars "${env_var}") endforeach() gtest_discover_tests(${YDB_TEST_NAME} @@ -88,7 +89,8 @@ function(add_ydb_test) WORKING_DIRECTORY ${YDB_TEST_WORKING_DIRECTORY} PROPERTIES LABELS ${YDB_TEST_LABELS} - ENVIRONMENT "${test_environment}" + ENVIRONMENT "YDB_TEST_ROOT=sdk_tests" + ${env_vars} ) target_link_libraries(${YDB_TEST_NAME} PRIVATE From 45e0e054e46c56f48f86fa8dfaa5830afbd888fd Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 17 Jun 2026 14:16:42 +0300 Subject: [PATCH 38/55] remove useless driver pool --- odbc/src/connection.cpp | 93 +++++++++++------------------------------ odbc/src/connection.h | 25 +++++++---- odbc/src/statement.cpp | 12 +++++- 3 files changed, 53 insertions(+), 77 deletions(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 91d632aaa66..974ce254f11 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -18,60 +17,6 @@ namespace NYdb { namespace NOdbc { -namespace { - -struct TDriverKey { - std::string Endpoint; - std::string Database; - - bool operator==(const TDriverKey& other) const noexcept { - return Endpoint == other.Endpoint && Database == other.Database; - } -}; - -struct TDriverKeyHash { - size_t operator()(const TDriverKey& key) const noexcept { - return std::hash{}(key.Endpoint) ^ (std::hash{}(key.Database) << 1U); - } -}; - -struct TDriverPool { - std::unordered_map, TDriverKeyHash> DriversByKey; - size_t InsertionsSinceCleanup = 0; -}; - -void CleanupExpiredDrivers(TDriverPool& pool) { - for (auto mapIt = pool.DriversByKey.begin(); mapIt != pool.DriversByKey.end();) { - if (mapIt->second.expired()) { - mapIt = pool.DriversByKey.erase(mapIt); - } else { - ++mapIt; - } - } -} - -std::shared_ptr AcquireSharedDriver(const std::string& endpoint, const std::string& database) { - static TDriverPool pool; - TDriverKey key{endpoint, database}; - auto it = pool.DriversByKey.find(key); - if (it != pool.DriversByKey.end()) { - if (std::shared_ptr existing = it->second.lock()) { - return existing; - } - } - auto driver = std::make_shared( - NYdb::TDriverConfig().SetEndpoint(endpoint).SetDatabase(database)); - pool.DriversByKey[std::move(key)] = driver; - ++pool.InsertionsSinceCleanup; - if (pool.InsertionsSinceCleanup >= 32) { - CleanupExpiredDrivers(pool); - pool.InsertionsSinceCleanup = 0; - } - return driver; -} - -} // namespace - SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { std::map params; size_t pos = 0; @@ -137,22 +82,40 @@ SQLRETURN TConnection::Disconnect() { Tx_.reset(); DbmsVersionCache_.reset(); DataSourceName_.clear(); - YdbSchemeClient_.reset(); - YdbTableClient_.reset(); - YdbClient_.reset(); - YdbDriver_.reset(); + Ydb_.reset(); return SQL_SUCCESS; } NQuery::TSession& TConnection::GetOrCreateQuerySession() { if (!QuerySession_) { - auto sessionResult = YdbClient_->GetSession().ExtractValueSync(); + auto sessionResult = Ydb_->QueryClient.GetSession().ExtractValueSync(); NStatusHelpers::ThrowOnError(sessionResult); QuerySession_.emplace(std::move(sessionResult.GetSession())); } return *QuerySession_; } +std::optional TConnection::GetClient() { + if (!Ydb_) { + return std::nullopt; + } + return Ydb_->QueryClient; +} + +std::optional TConnection::GetTableClient() { + if (!Ydb_) { + return std::nullopt; + } + return Ydb_->TableClient; +} + +std::optional TConnection::GetSchemeClient() { + if (!Ydb_) { + return std::nullopt; + } + return Ydb_->SchemeClient; +} + std::unique_ptr TConnection::CreateStatement() { return std::make_unique(this); } @@ -266,7 +229,7 @@ const std::string& TConnection::GetDbmsVersion() { return *DbmsVersionCache_; } - auto* client = GetClient(); + auto client = GetClient(); if (!client) { throw TOdbcException("08003", 0, "Connection is not established"); } @@ -304,13 +267,7 @@ void TConnection::RecreateYdbClients() { QuerySession_.reset(); Tx_.reset(); DbmsVersionCache_.reset(); - YdbSchemeClient_.reset(); - YdbTableClient_.reset(); - YdbClient_.reset(); - YdbDriver_ = AcquireSharedDriver(Endpoint_, Database_); - YdbClient_ = std::make_unique(*YdbDriver_); - YdbSchemeClient_ = std::make_unique(*YdbDriver_); - YdbTableClient_ = std::make_unique(*YdbDriver_); + Ydb_.emplace(Endpoint_, Database_); } void TConnection::RebindToDatabase(const std::string& newDatabase) { diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 0bf9cc3d78c..1822dc3713c 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -24,10 +24,21 @@ class TStatement; class TConnection : public TErrorManager { private: - std::shared_ptr YdbDriver_; - std::unique_ptr YdbClient_; - std::unique_ptr YdbTableClient_; - std::unique_ptr YdbSchemeClient_; + struct TYdbState { + TDriver Driver; + NQuery::TQueryClient QueryClient; + NScheme::TSchemeClient SchemeClient; + NTable::TTableClient TableClient; + + TYdbState(const std::string& endpoint, const std::string& database) + : Driver(TDriverConfig().SetEndpoint(endpoint).SetDatabase(database)) + , QueryClient(Driver) + , SchemeClient(Driver) + , TableClient(Driver) + {} + }; + + std::optional Ydb_; std::optional Tx_; std::optional QuerySession_; @@ -54,10 +65,10 @@ class TConnection : public TErrorManager { std::unique_ptr CreateStatement(); void RemoveStatement(TStatement* stmt); - NYdb::NQuery::TQueryClient* GetClient() { return YdbClient_.get(); } + std::optional GetClient(); NQuery::TSession& GetOrCreateQuerySession(); - NYdb::NTable::TTableClient* GetTableClient() { return YdbTableClient_.get(); } - NScheme::TSchemeClient* GetSchemeClient() { return YdbSchemeClient_.get(); } + std::optional GetTableClient(); + std::optional GetSchemeClient(); SQLRETURN SetAutocommit(bool value); bool GetAutocommit() const; diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index ba255fd43be..006b2d6b7e8 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -75,7 +75,7 @@ SQLRETURN TStatement::Execute() { StreamFetchError_ = false; RowsFetched_ = 0; Cursor_.reset(); - auto* client = Conn_->GetClient(); + auto client = Conn_->GetClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } @@ -326,7 +326,12 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, continue; } - auto status = Conn_->GetTableClient()->RetryOperationSync([this, path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { + auto tableClient = Conn_->GetTableClient(); + if (!tableClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } + + auto status = tableClient->RetryOperationSync([this, path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { auto result = session.DescribeTable(path).ExtractValueSync(); NStatusHelpers::ThrowOnError(result); @@ -432,6 +437,9 @@ std::vector TStatement::GetPatternEntries(const std::stri SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries) { auto schemeClient = Conn_->GetSchemeClient(); + if (!schemeClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } auto listDirectoryResult = schemeClient->ListDirectory(path + "/").ExtractValueSync(); NStatusHelpers::ThrowOnError(listDirectoryResult); From 6157276af64dc89904be5bf23ce6bfac3aa0fbd8 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 17 Jun 2026 15:54:24 +0300 Subject: [PATCH 39/55] driver::Stop(true):) --- odbc/src/connection.cpp | 17 ++++++++++++----- odbc/src/connection.h | 8 ++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 974ce254f11..c093ce64662 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -17,6 +17,16 @@ namespace NYdb { namespace NOdbc { +TConnection::~TConnection() { + DestroyYdbState(); +} + +void TConnection::DestroyYdbState() { + QuerySession_.reset(); + Tx_.reset(); + Ydb_.reset(); +} + SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { std::map params; size_t pos = 0; @@ -78,11 +88,9 @@ SQLRETURN TConnection::Connect(const std::string& serverName, } SQLRETURN TConnection::Disconnect() { - QuerySession_.reset(); - Tx_.reset(); + DestroyYdbState(); DbmsVersionCache_.reset(); DataSourceName_.clear(); - Ydb_.reset(); return SQL_SUCCESS; } @@ -264,8 +272,7 @@ const std::string& TConnection::GetDbmsVersion() { } void TConnection::RecreateYdbClients() { - QuerySession_.reset(); - Tx_.reset(); + DestroyYdbState(); DbmsVersionCache_.reset(); Ydb_.emplace(Endpoint_, Database_); } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 1822dc3713c..14448217940 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -25,6 +25,7 @@ class TStatement; class TConnection : public TErrorManager { private: struct TYdbState { + // Declared first: constructed before clients, destroyed after them. TDriver Driver; NQuery::TQueryClient QueryClient; NScheme::TSchemeClient SchemeClient; @@ -36,6 +37,10 @@ class TConnection : public TErrorManager { , SchemeClient(Driver) , TableClient(Driver) {} + + ~TYdbState() { + Driver.Stop(true); + } }; std::optional Ydb_; @@ -52,9 +57,12 @@ class TConnection : public TErrorManager { TConnectionAttributes Attributes_; mutable std::optional DbmsVersionCache_; + void DestroyYdbState(); void RecreateYdbClients(); void RebindToDatabase(const std::string& newDatabase); public: + ~TConnection(); + SQLRETURN Connect(const std::string& serverName, const std::string& userName, const std::string& auth); From 9903036be7d0ba34c04db1a50cefa5ddc15c4da2 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 17 Jun 2026 18:02:28 +0300 Subject: [PATCH 40/55] fully core-odbc compliant driver --- odbc/CMakeLists.txt | 4 + odbc/src/connection.cpp | 55 +- odbc/src/connection.h | 7 +- odbc/src/descriptor.cpp | 370 +++++++++++++ odbc/src/descriptor.h | 70 +++ odbc/src/environment.cpp | 43 +- odbc/src/environment.h | 3 +- odbc/src/metadata.cpp | 179 ++++-- odbc/src/metadata.h | 52 +- odbc/src/odbc_driver.cpp | 234 +++++++- odbc/src/statement.cpp | 454 ++++++++-------- odbc/src/statement.h | 56 +- odbc/src/statement_metadata.cpp | 542 +++++++++++++++++++ odbc/src/utils/bindings.h | 5 + odbc/src/utils/convert.cpp | 115 ++-- odbc/src/utils/convert.h | 1 + odbc/src/utils/cursor.cpp | 10 +- odbc/src/utils/diag.h | 38 +- odbc/src/utils/error_manager.h | 20 + odbc/src/utils/param_rewrite.cpp | 47 ++ odbc/src/utils/param_rewrite.h | 2 + odbc/src/utils/status_util.cpp | 11 + odbc/src/utils/status_util.h | 9 + odbc/src/utils/type_info_rows.cpp | 77 +++ odbc/src/utils/type_info_rows.h | 11 + odbc/src/utils/util.cpp | 86 +++ odbc/src/utils/util.h | 7 + odbc/tests/integration/CMakeLists.txt | 10 + odbc/tests/integration/core_api_it.cpp | 393 ++++++++++++++ odbc/tests/integration/descriptor_api_it.cpp | 91 ++++ odbc/tests/integration/metadata_api_it.cpp | 56 +- odbc/tests/integration/statement_api_it.cpp | 74 +++ odbc/tests/unit/CMakeLists.txt | 13 + odbc/tests/unit/conn_string_ut.cpp | 22 + odbc/tests/unit/param_rewrite_ut.cpp | 8 + 35 files changed, 2786 insertions(+), 389 deletions(-) create mode 100644 odbc/src/descriptor.cpp create mode 100644 odbc/src/descriptor.h create mode 100644 odbc/src/statement_metadata.cpp create mode 100644 odbc/src/utils/status_util.cpp create mode 100644 odbc/src/utils/status_util.h create mode 100644 odbc/src/utils/type_info_rows.cpp create mode 100644 odbc/src/utils/type_info_rows.h create mode 100644 odbc/tests/integration/core_api_it.cpp create mode 100644 odbc/tests/integration/descriptor_api_it.cpp create mode 100644 odbc/tests/unit/conn_string_ut.cpp diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index d8b37da64f8..16afdd5e708 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -3,9 +3,11 @@ add_library(ydb-odbc SHARED src/utils/escape.cpp src/utils/sql_type_map.cpp src/utils/param_rewrite.cpp + src/utils/type_info_rows.cpp src/utils/cursor.cpp src/utils/types.cpp src/utils/util.cpp + src/utils/status_util.cpp src/utils/convert.cpp src/utils/error_manager.cpp src/odbc_driver.cpp @@ -13,8 +15,10 @@ add_library(ydb-odbc SHARED src/connection.cpp src/statement_attr.cpp src/statement.cpp + src/statement_metadata.cpp src/environment.cpp src/metadata.cpp + src/descriptor.cpp ) target_include_directories(ydb-odbc diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index c093ce64662..3a39385923e 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -1,6 +1,6 @@ #include "connection.h" #include "statement.h" -#include "utils/error_manager.h" +#include "utils/util.h" #include #include @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -28,26 +29,10 @@ void TConnection::DestroyYdbState() { } SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { - std::map params; - size_t pos = 0; - while (pos < connectionString.size()) { - size_t eq = connectionString.find('=', pos); - if (eq == std::string::npos) { - break; - } - - size_t sc = connectionString.find(';', eq); - std::string key = connectionString.substr(pos, eq-pos); - std::string val = connectionString.substr(eq+1, (sc == std::string::npos ? std::string::npos : sc-eq-1)); - params[key] = val; - if (sc == std::string::npos) { - break; - } - pos = sc+1; - } - Endpoint_ = params.contains("Server") ? params["Server"] : params["Endpoint"]; - Database_ = params["Database"]; - DataSourceName_ = params.contains("DSN") ? params["DSN"] : ""; + const std::map params = ParseConnectionString(connectionString); + Endpoint_ = params.contains("Server") ? params.at("Server") : params.contains("Endpoint") ? params.at("Endpoint") : ""; + Database_ = params.contains("Database") ? params.at("Database") : ""; + DataSourceName_ = params.contains("DSN") ? params.at("DSN") : ""; if (Endpoint_.empty() || Database_.empty()) { throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in connection string"); @@ -128,11 +113,6 @@ std::unique_ptr TConnection::CreateStatement() { return std::make_unique(this); } -void TConnection::RemoveStatement(TStatement* stmt) { - Statements_.erase(std::remove_if(Statements_.begin(), Statements_.end(), - [stmt](const std::unique_ptr& s) { return s.get() == stmt; }), Statements_.end()); -} - SQLRETURN TConnection::SetAutocommit(bool value) { Attributes_.SetAutocommit(value); if (Attributes_.GetAutocommit() && Tx_) { @@ -301,5 +281,28 @@ std::string TConnection::WrapQueryForCurrentCatalog(const std::string& sql) cons } return "PRAGMA TablePathPrefix = \"" + escapedPrefix + "\";\n" + sql; } + +SQLRETURN TConnection::NativeSql(const std::string& inSql, SQLCHAR* outSql, SQLINTEGER outMax, SQLINTEGER* outLen) { + const SQLINTEGER fullLen = static_cast(inSql.size()); + if (outLen) { + *outLen = fullLen; + } + if (!outSql) { + return outMax == 0 ? SQL_SUCCESS : AddError("HY090", 0, "Invalid string or buffer length"); + } + if (outMax <= 0) { + return fullLen == 0 ? SQL_SUCCESS : AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + const SQLINTEGER copyLen = std::min(fullLen, outMax - 1); + if (copyLen > 0) { + std::memcpy(outSql, inSql.data(), static_cast(copyLen)); + } + outSql[copyLen] = '\0'; + if (copyLen < fullLen) { + return AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + return SQL_SUCCESS; +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/connection.h b/odbc/src/connection.h index 14448217940..c39cdb2e0a5 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -47,12 +47,10 @@ class TConnection : public TErrorManager { std::optional Tx_; std::optional QuerySession_; - std::vector> Statements_; std::string Endpoint_; std::string Database_; std::string DataSourceName_; - std::string AuthToken_; - TEnvironment* ParentEnv_; + TEnvironment* ParentEnv_ = nullptr; TConnectionAttributes Attributes_; mutable std::optional DbmsVersionCache_; @@ -71,7 +69,6 @@ class TConnection : public TErrorManager { SQLRETURN Disconnect(); std::unique_ptr CreateStatement(); - void RemoveStatement(TStatement* stmt); std::optional GetClient(); NQuery::TSession& GetOrCreateQuerySession(); @@ -101,6 +98,8 @@ class TConnection : public TErrorManager { void SetEnvironment(TEnvironment* env); TEnvironment* GetEnvironment(); + + SQLRETURN NativeSql(const std::string& inSql, SQLCHAR* outSql, SQLINTEGER outMax, SQLINTEGER* outLen); }; } // namespace NOdbc diff --git a/odbc/src/descriptor.cpp b/odbc/src/descriptor.cpp new file mode 100644 index 00000000000..4586524ab9b --- /dev/null +++ b/odbc/src/descriptor.cpp @@ -0,0 +1,370 @@ +#include "descriptor.h" +#include "statement.h" +#include "utils/param_rewrite.h" + +#include +#include + +namespace NYdb { +namespace NOdbc { + +TDescriptor::TDescriptor(EDescType type, TStatement* stmt) + : Type_(type) + , Stmt_(stmt) {} + +TDescriptor* TDescriptor::FromHandle(SQLHDESC handle) { + if (!handle) { + throw TOdbcException("HY000", 0, "Invalid handle", SQL_INVALID_HANDLE); + } + return static_cast(handle); +} + +SQLSMALLINT TDescriptor::GetRecordCount() const { + if (Type_ == EDescType::Explicit) { + return static_cast(ExplicitRecs_.size()); + } + if (!Stmt_) { + return 0; + } + switch (Type_) { + case EDescType::ImpRow: { + const auto& cols = Stmt_->GetColumnMeta(); + return static_cast(cols.size()); + } + case EDescType::AppRow: { + SQLSMALLINT maxRec = 0; + for (const auto& col : Stmt_->BoundColumns_) { + maxRec = std::max(maxRec, static_cast(col.ColumnNumber)); + } + return maxRec; + } + case EDescType::ImpParam: + return Stmt_->IsPrepared_ ? Stmt_->GetParamCount() : 0; + case EDescType::AppParam: { + SQLSMALLINT maxRec = 0; + for (const auto& param : Stmt_->BoundParams_) { + maxRec = std::max(maxRec, static_cast(param.ParamNumber)); + } + return maxRec; + } + default: + return 0; + } +} + +bool TDescriptor::GetExplicitRecord(SQLSMALLINT recNumber, TExplicitDescRec& out) const { + if (recNumber < 1 || static_cast(recNumber) > ExplicitRecs_.size()) { + return false; + } + out = ExplicitRecs_[static_cast(recNumber - 1)]; + return true; +} + +TExplicitDescRec& TDescriptor::GetOrCreateExplicitRecord(SQLSMALLINT recNumber) { + if (recNumber < 1) { + throw TOdbcException("07009", 0, "Invalid descriptor index"); + } + const size_t idx = static_cast(recNumber - 1); + if (ExplicitRecs_.size() <= idx) { + ExplicitRecs_.resize(idx + 1); + } + return ExplicitRecs_[idx]; +} + +SQLRETURN TDescriptor::GetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + if (!value && fieldIdentifier != SQL_DESC_ROWS_PROCESSED_PTR) { + return AddError("HY009", 0, "Invalid use of null pointer"); + } + switch (fieldIdentifier) { + case SQL_DESC_ALLOC_TYPE: + *reinterpret_cast(value) = + Type_ == EDescType::Explicit ? SQL_DESC_ALLOC_USER : SQL_DESC_ALLOC_AUTO; + return SQL_SUCCESS; + case SQL_DESC_COUNT: + *reinterpret_cast(value) = GetRecordCount(); + return SQL_SUCCESS; + case SQL_DESC_ARRAY_SIZE: + *reinterpret_cast(value) = 1; + return SQL_SUCCESS; + case SQL_DESC_TYPE: + case SQL_DESC_CONCISE_TYPE: + case SQL_DESC_LENGTH: + case SQL_DESC_PRECISION: + case SQL_DESC_SCALE: + case SQL_DESC_NULLABLE: + case SQL_DESC_NAME: { + SQLCHAR name[256] = {}; + SQLSMALLINT nameLen = 0; + SQLSMALLINT type = 0; + SQLSMALLINT subType = 0; + SQLLEN length = 0; + SQLSMALLINT precision = 0; + SQLSMALLINT scale = 0; + SQLSMALLINT nullable = 0; + const SQLRETURN rc = GetDescRec(recNumber, name, sizeof(name), &nameLen, &type, &subType, + &length, &precision, &scale, &nullable); + if (rc != SQL_SUCCESS) { + return rc; + } + if (fieldIdentifier == SQL_DESC_NAME) { + if (stringLengthPtr) { + *stringLengthPtr = nameLen; + } + if (bufferLength > 0) { + const SQLINTEGER copyLen = std::min(nameLen, bufferLength - 1); + std::memcpy(value, name, static_cast(copyLen)); + reinterpret_cast(value)[copyLen] = '\0'; + } + return SQL_SUCCESS; + } + if (fieldIdentifier == SQL_DESC_TYPE || fieldIdentifier == SQL_DESC_CONCISE_TYPE) { + *reinterpret_cast(value) = type; + } else if (fieldIdentifier == SQL_DESC_LENGTH) { + *reinterpret_cast(value) = length; + } else if (fieldIdentifier == SQL_DESC_PRECISION) { + *reinterpret_cast(value) = precision; + } else if (fieldIdentifier == SQL_DESC_SCALE) { + *reinterpret_cast(value) = scale; + } else if (fieldIdentifier == SQL_DESC_NULLABLE) { + *reinterpret_cast(value) = nullable; + } + return SQL_SUCCESS; + } + default: + return AddError("HYC00", 0, "Optional feature not implemented"); + } +} + +SQLRETURN TDescriptor::GetDescRec(SQLSMALLINT recNumber, SQLCHAR* name, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr, SQLSMALLINT* typePtr, SQLSMALLINT* subTypePtr, + SQLLEN* lengthPtr, SQLSMALLINT* precisionPtr, SQLSMALLINT* scalePtr, + SQLSMALLINT* nullablePtr) { + if (recNumber < 1) { + return AddError("07009", 0, "Invalid descriptor index"); + } + if (Type_ == EDescType::Explicit) { + TExplicitDescRec rec; + if (!GetExplicitRecord(recNumber, rec)) { + return AddError("07009", 0, "Invalid descriptor index"); + } + if (stringLengthPtr) { + *stringLengthPtr = static_cast(rec.Name.size()); + } + if (name && bufferLength > 0) { + const auto copyLen = std::min(rec.Name.size(), static_cast(bufferLength - 1)); + std::memcpy(name, rec.Name.data(), copyLen); + name[copyLen] = '\0'; + } + if (typePtr) { + *typePtr = rec.Type; + } + if (subTypePtr) { + *subTypePtr = rec.SubType; + } + if (lengthPtr) { + *lengthPtr = rec.Length; + } + if (precisionPtr) { + *precisionPtr = rec.Precision; + } + if (scalePtr) { + *scalePtr = rec.Scale; + } + if (nullablePtr) { + *nullablePtr = rec.Nullable; + } + return SQL_SUCCESS; + } + if (!Stmt_) { + return AddError("HY000", 0, "Invalid descriptor"); + } + if (Type_ == EDescType::ImpRow) { + const auto& cols = Stmt_->GetColumnMeta(); + if (static_cast(recNumber) > cols.size()) { + return AddError("07009", 0, "Invalid descriptor index"); + } + const auto& col = cols[static_cast(recNumber - 1)]; + if (stringLengthPtr) { + *stringLengthPtr = static_cast(col.Name.size()); + } + if (name && bufferLength > 0) { + const auto copyLen = std::min(col.Name.size(), static_cast(bufferLength - 1)); + std::memcpy(name, col.Name.data(), copyLen); + name[copyLen] = '\0'; + } + if (typePtr) { + *typePtr = col.SqlType; + } + if (subTypePtr) { + *subTypePtr = 0; + } + if (lengthPtr) { + *lengthPtr = static_cast(col.Size); + } + if (precisionPtr) { + *precisionPtr = static_cast(col.Size); + } + if (scalePtr) { + *scalePtr = col.DecimalDigits; + } + if (nullablePtr) { + *nullablePtr = col.Nullable; + } + return SQL_SUCCESS; + } + if (Type_ == EDescType::AppRow) { + const auto it = std::find_if(Stmt_->BoundColumns_.begin(), Stmt_->BoundColumns_.end(), + [recNumber](const TBoundColumn& col) { return col.ColumnNumber == static_cast(recNumber); }); + if (it == Stmt_->BoundColumns_.end()) { + return AddError("07009", 0, "Invalid descriptor index"); + } + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + if (typePtr) { + *typePtr = it->TargetType; + } + if (subTypePtr) { + *subTypePtr = 0; + } + if (lengthPtr) { + *lengthPtr = it->BufferLength; + } + if (precisionPtr) { + *precisionPtr = 0; + } + if (scalePtr) { + *scalePtr = 0; + } + if (nullablePtr) { + *nullablePtr = SQL_NULLABLE; + } + return SQL_SUCCESS; + } + if (Type_ == EDescType::AppParam || Type_ == EDescType::ImpParam) { + const auto it = std::find_if(Stmt_->BoundParams_.begin(), Stmt_->BoundParams_.end(), + [recNumber](const TBoundParam& p) { return p.ParamNumber == static_cast(recNumber); }); + if (it != Stmt_->BoundParams_.end()) { + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + if (typePtr) { + *typePtr = Type_ == EDescType::AppParam ? it->ValueType : it->ParameterType; + } + if (subTypePtr) { + *subTypePtr = 0; + } + if (lengthPtr) { + *lengthPtr = static_cast(it->ColumnSize); + } + if (precisionPtr) { + *precisionPtr = static_cast(it->ColumnSize); + } + if (scalePtr) { + *scalePtr = it->DecimalDigits; + } + if (nullablePtr) { + *nullablePtr = SQL_NULLABLE; + } + return SQL_SUCCESS; + } + if (Type_ == EDescType::ImpParam && Stmt_->IsPrepared_ + && recNumber <= CountOdbcParams(Stmt_->PreparedQuery_)) { + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + if (typePtr) { + *typePtr = SQL_UNKNOWN_TYPE; + } + if (subTypePtr) { + *subTypePtr = 0; + } + if (lengthPtr) { + *lengthPtr = 0; + } + if (precisionPtr) { + *precisionPtr = 0; + } + if (scalePtr) { + *scalePtr = 0; + } + if (nullablePtr) { + *nullablePtr = SQL_NULLABLE_UNKNOWN; + } + return SQL_SUCCESS; + } + return AddError("07009", 0, "Invalid descriptor index"); + } + return AddError("HYC00", 0, "Optional feature not implemented"); +} + +SQLRETURN TDescriptor::SetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength) { + (void)bufferLength; + if (Type_ != EDescType::Explicit) { + return AddError("HY017", 0, "Invalid use of an automatically allocated descriptor handle"); + } + auto& rec = GetOrCreateExplicitRecord(recNumber); + switch (fieldIdentifier) { + case SQL_DESC_TYPE: + case SQL_DESC_CONCISE_TYPE: + rec.Type = *reinterpret_cast(value); + return SQL_SUCCESS; + case SQL_DESC_LENGTH: + rec.Length = *reinterpret_cast(value); + return SQL_SUCCESS; + case SQL_DESC_PRECISION: + rec.Precision = *reinterpret_cast(value); + return SQL_SUCCESS; + case SQL_DESC_SCALE: + rec.Scale = *reinterpret_cast(value); + return SQL_SUCCESS; + case SQL_DESC_NULLABLE: + rec.Nullable = *reinterpret_cast(value); + return SQL_SUCCESS; + default: + return AddError("HYC00", 0, "Optional feature not implemented"); + } +} + +SQLRETURN TDescriptor::SetDescRec(SQLSMALLINT recNumber, SQLSMALLINT type, SQLSMALLINT subType, SQLLEN length, + SQLSMALLINT precision, SQLSMALLINT scale, SQLPOINTER dataPtr, + SQLLEN* stringLengthPtr, SQLLEN* indicatorPtr) { + if (Type_ == EDescType::Explicit) { + auto& rec = GetOrCreateExplicitRecord(recNumber); + rec.Type = type; + rec.SubType = subType; + rec.Length = length; + rec.Precision = precision; + rec.Scale = scale; + rec.DataPtr = dataPtr; + rec.Indicator = indicatorPtr ? *indicatorPtr : 0; + return SQL_SUCCESS; + } + if (!Stmt_) { + return AddError("HY000", 0, "Invalid descriptor"); + } + if (Type_ == EDescType::AppRow) { + return Stmt_->BindCol(static_cast(recNumber), type, dataPtr, length, indicatorPtr); + } + if (Type_ == EDescType::AppParam) { + return Stmt_->BindParameter(static_cast(recNumber), SQL_PARAM_INPUT, type, subType, + static_cast(length), scale, dataPtr, length, indicatorPtr); + } + return AddError("HY017", 0, "Invalid use of an automatically allocated descriptor handle"); +} + +SQLRETURN TDescriptor::CopyDesc(TDescriptor* target) { + if (!target) { + return AddError("HY009", 0, "Invalid use of null pointer"); + } + if (Type_ != EDescType::Explicit || target->Type_ != EDescType::Explicit) { + return AddError("HYC00", 0, "Optional feature not implemented"); + } + target->ExplicitRecs_ = ExplicitRecs_; + return SQL_SUCCESS; +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/descriptor.h b/odbc/src/descriptor.h new file mode 100644 index 00000000000..7e1abe3d822 --- /dev/null +++ b/odbc/src/descriptor.h @@ -0,0 +1,70 @@ +#pragma once + +#include "utils/error_manager.h" + +#include +#include + +#include +#include + +namespace NYdb { +namespace NOdbc { + +class TStatement; + +enum class EDescType { + AppRow, + AppParam, + ImpRow, + ImpParam, + Explicit, +}; + +struct TExplicitDescRec { + std::string Name; + SQLSMALLINT Type = 0; + SQLSMALLINT SubType = 0; + SQLLEN Length = 0; + SQLSMALLINT Precision = 0; + SQLSMALLINT Scale = 0; + SQLSMALLINT Nullable = SQL_NULLABLE; + SQLPOINTER DataPtr = nullptr; + SQLLEN Indicator = 0; + SQLLEN OctetLength = 0; +}; + +class TDescriptor : public TErrorManager { +public: + TDescriptor(EDescType type, TStatement* stmt = nullptr); + + EDescType GetDescType() const noexcept { return Type_; } + TStatement* GetStatement() const noexcept { return Stmt_; } + + SQLRETURN GetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); + SQLRETURN GetDescRec(SQLSMALLINT recNumber, SQLCHAR* name, SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr, SQLSMALLINT* typePtr, SQLSMALLINT* subTypePtr, + SQLLEN* lengthPtr, SQLSMALLINT* precisionPtr, SQLSMALLINT* scalePtr, + SQLSMALLINT* nullablePtr); + SQLRETURN SetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, + SQLINTEGER bufferLength); + SQLRETURN SetDescRec(SQLSMALLINT recNumber, SQLSMALLINT type, SQLSMALLINT subType, SQLLEN length, + SQLSMALLINT precision, SQLSMALLINT scale, SQLPOINTER dataPtr, + SQLLEN* stringLengthPtr, SQLLEN* indicatorPtr); + SQLRETURN CopyDesc(TDescriptor* target); + + static TDescriptor* FromHandle(SQLHDESC handle); + +private: + SQLSMALLINT GetRecordCount() const; + bool GetExplicitRecord(SQLSMALLINT recNumber, TExplicitDescRec& out) const; + TExplicitDescRec& GetOrCreateExplicitRecord(SQLSMALLINT recNumber); + + EDescType Type_; + TStatement* Stmt_; + std::vector ExplicitRecs_; +}; + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index 8df0949faa8..5947823df44 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -30,22 +30,53 @@ SQLRETURN TEnvironment::SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQL } } +SQLRETURN TEnvironment::GetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + if (!value) { + return AddError("HY009", 0, "Invalid use of null pointer"); + } + if (stringLengthPtr) { + *stringLengthPtr = 0; + } + switch (attribute) { + case SQL_ATTR_ODBC_VERSION: + if (bufferLength < static_cast(sizeof(SQLINTEGER))) { + return AddError("HY090", 0, "Invalid string or buffer length"); + } + *reinterpret_cast(value) = OdbcVersion_; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); + } + return SQL_SUCCESS; + case SQL_ATTR_OUTPUT_NTS: + if (bufferLength < static_cast(sizeof(SQLINTEGER))) { + return AddError("HY090", 0, "Invalid string or buffer length"); + } + *reinterpret_cast(value) = SQL_TRUE; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLINTEGER); + } + return SQL_SUCCESS; + default: + return AddError("HYC00", 0, "Optional feature not implemented"); + } +} + void TEnvironment::RegisterConnection(TConnection* conn){ if (conn == nullptr){ throw std::invalid_argument("null connection"); } - connections_.insert(conn); + Connections_.insert(conn); } void TEnvironment::UnregisterConnection(TConnection* conn){ if (conn == nullptr){ throw std::invalid_argument("null connection"); } - connections_.erase(conn); + Connections_.erase(conn); } std::vector TEnvironment::GetConnectionsSnapshot() const { - return std::vector(connections_.begin(), connections_.end()); + return std::vector(Connections_.begin(), Connections_.end()); } SQLRETURN TEnvironment::EndTran(SQLSMALLINT completionType){ @@ -55,7 +86,7 @@ SQLRETURN TEnvironment::EndTran(SQLSMALLINT completionType){ bool hasFailures = false; int failedCount = 0; - for (auto* conn : connections_) { + for (auto* conn : Connections_) { if (!conn || !conn->GetTx()) { continue; } @@ -76,7 +107,9 @@ SQLRETURN TEnvironment::EndTran(SQLSMALLINT completionType){ } } if (hasFailures) { - AddError("01000", 0, "SQLEndTran(SQL_HANDLE_ENV): some connections failed", SQL_SUCCESS_WITH_INFO); + AddError("01000", 0, + "SQLEndTran(SQL_HANDLE_ENV): " + std::to_string(failedCount) + " connection(s) failed", + SQL_SUCCESS_WITH_INFO); return SQL_SUCCESS_WITH_INFO; } return SQL_SUCCESS; diff --git a/odbc/src/environment.h b/odbc/src/environment.h index 70a785f45d7..5dc6021ce36 100644 --- a/odbc/src/environment.h +++ b/odbc/src/environment.h @@ -15,13 +15,14 @@ class TConnection; class TEnvironment : public TErrorManager { private: SQLINTEGER OdbcVersion_; - std::unordered_set connections_; + std::unordered_set Connections_; public: TEnvironment(); ~TEnvironment(); SQLRETURN SetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength); + SQLRETURN GetAttribute(SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); void RegisterConnection(TConnection*); void UnregisterConnection(TConnection*); diff --git a/odbc/src/metadata.cpp b/odbc/src/metadata.cpp index b8857b245d9..2a648da1248 100644 --- a/odbc/src/metadata.cpp +++ b/odbc/src/metadata.cpp @@ -1,5 +1,7 @@ #include "metadata.h" +#include "utils/diag.h" + #include #include @@ -12,30 +14,7 @@ SQLRETURN WriteInfoString( SQLPOINTER infoValuePtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { - if (!infoValuePtr) { - return conn->AddError("HY009", 0, "Invalid use of null pointer"); - } - if (bufferLength < 0) { - return conn->AddError("HY090", 0, "Invalid string or buffer length"); - } - const SQLSMALLINT fullLen = static_cast(std::strlen(value)); - if (stringLengthPtr) { - *stringLengthPtr = fullLen; - } - if (bufferLength == 0) { - return fullLen == 0 ? SQL_SUCCESS : conn->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); - } - - auto* out = reinterpret_cast(infoValuePtr); - const SQLSMALLINT copyLen = static_cast(std::min(fullLen, bufferLength - 1)); - if (copyLen > 0) { - std::memcpy(out, value, static_cast(copyLen)); - } - out[copyLen] = '\0'; - if (copyLen < fullLen) { - return conn->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); - } - return SQL_SUCCESS; + return Diag::WriteOdbcString(*conn, value, infoValuePtr, bufferLength, stringLengthPtr); } template @@ -60,10 +39,14 @@ bool IsSupportedFunction(SQLUSMALLINT functionId) { case SQL_API_SQLALLOCHANDLE: case SQL_API_SQLBINDCOL: case SQL_API_SQLBINDPARAMETER: + case SQL_API_SQLCANCEL: case SQL_API_SQLCLOSECURSOR: + case SQL_API_SQLCOLATTRIBUTE: case SQL_API_SQLCOLUMNS: case SQL_API_SQLCONNECT: + case SQL_API_SQLCOPYDESC: case SQL_API_SQLDESCRIBECOL: + case SQL_API_SQLDESCRIBEPARAM: case SQL_API_SQLDISCONNECT: case SQL_API_SQLDRIVERCONNECT: case SQL_API_SQLENDTRAN: @@ -71,22 +54,38 @@ bool IsSupportedFunction(SQLUSMALLINT functionId) { case SQL_API_SQLEXECUTE: case SQL_API_SQLFETCH: case SQL_API_SQLFETCHSCROLL: + case SQL_API_SQLFOREIGNKEYS: case SQL_API_SQLFREEHANDLE: case SQL_API_SQLFREESTMT: + case SQL_API_SQLGETCURSORNAME: case SQL_API_SQLGETDATA: + case SQL_API_SQLGETDESCFIELD: + case SQL_API_SQLGETDESCREC: case SQL_API_SQLGETDIAGFIELD: case SQL_API_SQLGETDIAGREC: case SQL_API_SQLGETFUNCTIONS: case SQL_API_SQLGETCONNECTATTR: + case SQL_API_SQLGETENVATTR: case SQL_API_SQLGETINFO: case SQL_API_SQLGETSTMTATTR: + case SQL_API_SQLGETTYPEINFO: case SQL_API_SQLMORERESULTS: + case SQL_API_SQLNATIVESQL: + case SQL_API_SQLNUMPARAMS: case SQL_API_SQLNUMRESULTCOLS: + case SQL_API_SQLPARAMDATA: case SQL_API_SQLPREPARE: + case SQL_API_SQLPRIMARYKEYS: + case SQL_API_SQLPUTDATA: case SQL_API_SQLROWCOUNT: case SQL_API_SQLSETCONNECTATTR: + case SQL_API_SQLSETCURSORNAME: + case SQL_API_SQLSETDESCFIELD: + case SQL_API_SQLSETDESCREC: case SQL_API_SQLSETENVATTR: case SQL_API_SQLSETSTMTATTR: + case SQL_API_SQLSPECIALCOLUMNS: + case SQL_API_SQLSTATISTICS: case SQL_API_SQLTABLES: return true; default: @@ -96,7 +95,7 @@ bool IsSupportedFunction(SQLUSMALLINT functionId) { } // namespace -SQLRETURN TMetadata::GetInfo( +SQLRETURN NMetadata::GetInfo( TConnection* conn, SQLUSMALLINT infoType, SQLPOINTER infoValuePtr, @@ -110,6 +109,46 @@ SQLRETURN TMetadata::GetInfo( return WriteInfoString(conn, "unknown", infoValuePtr, bufferLength, stringLengthPtr); case SQL_DRIVER_ODBC_VER: return WriteInfoString(conn, "03.00", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_ODBC_INTERFACE_CONFORMANCE: + return WriteInfoScalar(conn, SQL_OIC_CORE, infoValuePtr, stringLengthPtr); + case SQL_ODBC_API_CONFORMANCE: + return WriteInfoScalar(conn, SQL_OAC_LEVEL1, infoValuePtr, stringLengthPtr); + case SQL_ODBC_SAG_CLI_CONFORMANCE: + return WriteInfoScalar(conn, SQL_OSCC_NOT_COMPLIANT, infoValuePtr, stringLengthPtr); + case SQL_ODBC_SQL_CONFORMANCE: + return WriteInfoScalar(conn, SQL_OSC_MINIMUM, infoValuePtr, stringLengthPtr); + case SQL_MAX_TABLE_NAME_LEN: + case SQL_MAX_COLUMN_NAME_LEN: + case SQL_MAX_CATALOG_NAME_LEN: + case SQL_MAX_IDENTIFIER_LEN: + return WriteInfoScalar(conn, 255, infoValuePtr, stringLengthPtr); + case SQL_MAX_SCHEMA_NAME_LEN: + case SQL_MAX_PROCEDURE_NAME_LEN: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_MAX_USER_NAME_LEN: + return WriteInfoScalar(conn, 128, infoValuePtr, stringLengthPtr); + case SQL_MAX_DRIVER_CONNECTIONS: + case SQL_MAX_CONCURRENT_ACTIVITIES: + case SQL_MAX_STATEMENT_LEN: + case SQL_MAX_BINARY_LITERAL_LEN: + case SQL_MAX_CHAR_LITERAL_LEN: + case SQL_MAX_COLUMNS_IN_GROUP_BY: + case SQL_MAX_COLUMNS_IN_ORDER_BY: + case SQL_MAX_COLUMNS_IN_INDEX: + case SQL_MAX_COLUMNS_IN_SELECT: + case SQL_MAX_COLUMNS_IN_TABLE: + return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_SEARCH_PATTERN_ESCAPE: + return WriteInfoString(conn, "\\", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_KEYWORDS: + case SQL_SPECIAL_CHARACTERS: + return WriteInfoString(conn, "", infoValuePtr, bufferLength, stringLengthPtr); + case SQL_CONCAT_NULL_BEHAVIOR: + return WriteInfoScalar(conn, SQL_CB_NULL, infoValuePtr, stringLengthPtr); + case SQL_NULL_COLLATION: + return WriteInfoScalar(conn, SQL_NC_HIGH, infoValuePtr, stringLengthPtr); + case SQL_MAX_CURSOR_NAME_LEN: + return WriteInfoScalar(conn, 128, infoValuePtr, stringLengthPtr); // DBMS Information case SQL_DBMS_NAME: @@ -208,7 +247,7 @@ SQLRETURN TMetadata::GetInfo( } -SQLRETURN TMetadata::GetFunctions(SQLUSMALLINT functionId, SQLUSMALLINT* supportedPtr) { +SQLRETURN NMetadata::GetFunctions(SQLUSMALLINT functionId, SQLUSMALLINT* supportedPtr) { if (!supportedPtr) { return SQL_ERROR; } @@ -237,7 +276,7 @@ SQLRETURN TMetadata::GetFunctions(SQLUSMALLINT functionId, SQLUSMALLINT* support return SQL_SUCCESS; } -SQLRETURN TMetadata::DescribeCol( +SQLRETURN NMetadata::DescribeCol( TStatement* stmt, SQLUSMALLINT columnNumber, SQLCHAR* columnName, @@ -253,13 +292,9 @@ SQLRETURN TMetadata::DescribeCol( } const auto& column = columns[columnNumber - 1]; - if (nameLengthPtr) { - *nameLengthPtr = static_cast(column.Name.size()); - } - if (columnName && bufferLength > 0) { - const auto copyLength = std::min(column.Name.size(), static_cast(bufferLength - 1)); - std::memcpy(columnName, column.Name.data(), copyLength); - columnName[copyLength] = '\0'; + const SQLRETURN nameRc = Diag::WriteOdbcString(*stmt, column.Name, columnName, bufferLength, nameLengthPtr); + if (nameRc != SQL_SUCCESS) { + return nameRc; } if (dataTypePtr) { *dataTypePtr = column.SqlType; @@ -276,4 +311,80 @@ SQLRETURN TMetadata::DescribeCol( return SQL_SUCCESS; } +SQLRETURN NMetadata::ColAttribute( + TStatement* stmt, + SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, + SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthAttributePtr, + SQLLEN* numericAttributePtr) { + SQLCHAR name[256] = {}; + SQLSMALLINT nameLength = 0; + SQLSMALLINT dataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + + const SQLRETURN describeRc = DescribeCol( + stmt, columnNumber, name, sizeof(name), &nameLength, &dataType, &columnSize, &decimalDigits, &nullable); + if (describeRc != SQL_SUCCESS) { + return describeRc; + } + + const auto setNumericAttr = [&](SQLLEN value) -> SQLRETURN { + if (!numericAttributePtr) { + return stmt->AddError("HY009", 0, "Invalid use of null pointer"); + } + *numericAttributePtr = value; + return SQL_SUCCESS; + }; + + switch (fieldIdentifier) { + case SQL_DESC_NAME: + case SQL_COLUMN_NAME: { + if (!characterAttributePtr && bufferLength != 0) { + return stmt->AddError("HY090", 0, "Invalid string or buffer length"); + } + const SQLSMALLINT fullLen = nameLength; + if (stringLengthAttributePtr) { + *stringLengthAttributePtr = fullLen; + } + if (bufferLength == 0) { + return fullLen == 0 ? SQL_SUCCESS + : stmt->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + auto* out = reinterpret_cast(characterAttributePtr); + const SQLSMALLINT copyLen = static_cast(std::min(fullLen, bufferLength - 1)); + if (copyLen > 0) { + std::memcpy(out, name, static_cast(copyLen)); + } + if (out) { + out[copyLen] = '\0'; + } + if (copyLen < fullLen) { + return stmt->AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + } + return SQL_SUCCESS; + } + case SQL_DESC_TYPE: + case SQL_COLUMN_TYPE: + return setNumericAttr(dataType); + case SQL_DESC_LENGTH: + case SQL_COLUMN_LENGTH: + return setNumericAttr(static_cast(columnSize)); + case SQL_DESC_PRECISION: + case SQL_COLUMN_PRECISION: + return setNumericAttr(static_cast(columnSize)); + case SQL_DESC_SCALE: + case SQL_COLUMN_SCALE: + return setNumericAttr(decimalDigits); + case SQL_DESC_NULLABLE: + case SQL_COLUMN_NULLABLE: + return setNumericAttr(nullable); + default: + return stmt->AddError("HYC00", 0, "Optional feature not implemented"); + } +} + } // namespace NYdb::NOdbc diff --git a/odbc/src/metadata.h b/odbc/src/metadata.h index 7374e45e53a..59b83896c4b 100644 --- a/odbc/src/metadata.h +++ b/odbc/src/metadata.h @@ -4,30 +4,38 @@ #include "statement.h" namespace NYdb::NOdbc { +namespace NMetadata { -class TMetadata { -public: - static SQLRETURN GetInfo( - TConnection* conn, - SQLUSMALLINT infoType, - SQLPOINTER infoValuePtr, - SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr); +SQLRETURN GetInfo( + TConnection* conn, + SQLUSMALLINT infoType, + SQLPOINTER infoValuePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthPtr); - static SQLRETURN GetFunctions( - SQLUSMALLINT functionId, - SQLUSMALLINT* supportedPtr); +SQLRETURN GetFunctions( + SQLUSMALLINT functionId, + SQLUSMALLINT* supportedPtr); - static SQLRETURN DescribeCol( - TStatement* stmt, - SQLUSMALLINT columnNumber, - SQLCHAR* columnName, - SQLSMALLINT bufferLength, - SQLSMALLINT* nameLengthPtr, - SQLSMALLINT* dataTypePtr, - SQLULEN* columnSizePtr, - SQLSMALLINT* decimalDigitsPtr, - SQLSMALLINT* nullablePtr); -}; +SQLRETURN DescribeCol( + TStatement* stmt, + SQLUSMALLINT columnNumber, + SQLCHAR* columnName, + SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr); +SQLRETURN ColAttribute( + TStatement* stmt, + SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, + SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthAttributePtr, + SQLLEN* numericAttributePtr); + +} // namespace NMetadata } // namespace NYdb::NOdbc diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 3d9a2019c3a..e609d7b7130 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -2,6 +2,7 @@ #include "connection.h" #include "statement.h" #include "metadata.h" +#include "descriptor.h" #include "utils/util.h" #include "utils/error_manager.h" @@ -62,6 +63,17 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, return SQL_SUCCESS; }); } + case SQL_HANDLE_DESC: { + return NYdb::NOdbc::HandleOdbcExceptions( + inputHandle, + [&]() { + auto* const desc = new NYdb::NOdbc::TDescriptor(NYdb::NOdbc::EDescType::Explicit); + *outputHandle = desc; + desc->SetLastReturnCode(SQL_SUCCESS); + return SQL_SUCCESS; + }, + NYdb::NOdbc::ENullInputHandlePolicy::Allow); + } default: return SQL_ERROR; } @@ -70,13 +82,13 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { switch (handleType) { case SQL_HANDLE_ENV: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* env) { + return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* env) { delete env; return SQL_SUCCESS; }); } case SQL_HANDLE_DBC: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* conn) { + return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* conn) { auto* env = conn->GetEnvironment(); if (env != nullptr){ env->UnregisterConnection(conn); @@ -86,14 +98,17 @@ SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { }); } case SQL_HANDLE_STMT: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [](auto* stmt) { - if (stmt->GetConnection()) { - stmt->GetConnection()->RemoveStatement(stmt); - } + return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* stmt) { delete stmt; return SQL_SUCCESS; }); } + case SQL_HANDLE_DESC: { + return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* desc) { + delete desc; + return SQL_SUCCESS; + }); + } default: return SQL_ERROR; } @@ -113,6 +128,20 @@ SQLRETURN SQL_API SQLSetEnvAttr(SQLHENV environmentHandle, }); } +SQLRETURN SQL_API SQLGetEnvAttr(SQLHENV environmentHandle, + SQLINTEGER attribute, + SQLPOINTER value, + SQLINTEGER bufferLength, + SQLINTEGER* stringLengthPtr) { + auto env = static_cast(environmentHandle); + if (!env) { + return SQL_INVALID_HANDLE; + } + return NYdb::NOdbc::HandleOdbcExceptions(env, [&]() { + return env->GetAttribute(attribute, value, bufferLength, stringLengthPtr); + }); +} + SQLRETURN SQL_API SQLDriverConnect(SQLHDBC connectionHandle, SQLHWND /*WindowHandle*/, SQLCHAR* inConnectionString, @@ -353,12 +382,13 @@ SQLRETURN SQL_API SQLCloseCursor(SQLHSTMT statementHandle) { } SQLRETURN SQL_API SQLFreeStmt(SQLHSTMT statementHandle, SQLUSMALLINT option) { + if (option == SQL_DROP) { + return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); + } return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) -> SQLRETURN { switch (option) { case SQL_CLOSE: return stmt->Close(true); - case SQL_DROP: - return SQLFreeHandle(SQL_HANDLE_STMT, statementHandle); case SQL_UNBIND: stmt->UnbindColumns(); return SQL_SUCCESS; @@ -405,7 +435,7 @@ SQLRETURN SQL_API SQLDescribeCol( SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr) { return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { - return NYdb::NOdbc::TMetadata::DescribeCol( + return NYdb::NOdbc::NMetadata::DescribeCol( stmt, columnNumber, columnName, @@ -425,7 +455,7 @@ SQLRETURN SQL_API SQLMoreResults(SQLHSTMT) { SQLRETURN SQL_API SQLGetFunctions(SQLHDBC connectionHandle, SQLUSMALLINT functionId, SQLUSMALLINT* supportedPtr) { return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto*) { - return NYdb::NOdbc::TMetadata::GetFunctions(functionId, supportedPtr); + return NYdb::NOdbc::NMetadata::GetFunctions(functionId, supportedPtr); }); } @@ -452,7 +482,189 @@ SQLRETURN SQL_API SQLGetInfo(SQLHDBC connectionHandle, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { - return NYdb::NOdbc::TMetadata::GetInfo(conn, infoType, infoValuePtr, bufferLength, stringLengthPtr); + return NYdb::NOdbc::NMetadata::GetInfo(conn, infoType, infoValuePtr, bufferLength, stringLengthPtr); + }); +} + +SQLRETURN SQL_API SQLGetTypeInfo(SQLHSTMT statementHandle, SQLSMALLINT dataType) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->GetTypeInfo(dataType); + }); +} + +SQLRETURN SQL_API SQLStatistics(SQLHSTMT statementHandle, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3, + SQLUSMALLINT unique, SQLUSMALLINT reserved) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Statistics( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + unique, + reserved); + }); +} + +SQLRETURN SQL_API SQLSpecialColumns(SQLHSTMT statementHandle, + SQLUSMALLINT identifierType, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3, + SQLUSMALLINT scope, + SQLUSMALLINT nullable) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + (void)nullable; + return stmt->SpecialColumns( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3), + identifierType, + scope); + }); +} + +SQLRETURN SQL_API SQLColAttribute(SQLHSTMT statementHandle, + SQLUSMALLINT columnNumber, + SQLUSMALLINT fieldIdentifier, + SQLPOINTER characterAttributePtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* stringLengthAttributePtr, + SQLLEN* numericAttributePtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return NYdb::NOdbc::NMetadata::ColAttribute( + stmt, columnNumber, fieldIdentifier, characterAttributePtr, bufferLength, + stringLengthAttributePtr, numericAttributePtr); + }); +} + +SQLRETURN SQL_API SQLNumParams(SQLHSTMT statementHandle, SQLSMALLINT* paramCountPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->NumParams(paramCountPtr); + }); +} + +SQLRETURN SQL_API SQLDescribeParam(SQLHSTMT statementHandle, SQLUSMALLINT paramNumber, SQLSMALLINT* dataTypePtr, + SQLULEN* paramSizePtr, SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->DescribeParam(paramNumber, dataTypePtr, paramSizePtr, decimalDigitsPtr, nullablePtr); + }); +} + +SQLRETURN SQL_API SQLParamData(SQLHSTMT statementHandle, SQLPOINTER* valuePtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->ParamData(valuePtr); + }); +} + +SQLRETURN SQL_API SQLPutData(SQLHSTMT statementHandle, SQLPOINTER data, SQLLEN strLenOrInd) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->PutData(data, strLenOrInd); + }); +} + +SQLRETURN SQL_API SQLCancel(SQLHSTMT statementHandle) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->Cancel(); + }); +} + +SQLRETURN SQL_API SQLNativeSql(SQLHDBC connectionHandle, + SQLCHAR* inNativeSql, + SQLINTEGER textLength1, + SQLCHAR* outNativeSql, + SQLINTEGER bufferLength, + SQLINTEGER* outLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(connectionHandle, [&](auto* conn) { + const std::string inSql = textLength1 == SQL_NTS + ? reinterpret_cast(inNativeSql) + : NYdb::NOdbc::GetString(inNativeSql, static_cast(textLength1)); + return conn->NativeSql(inSql, outNativeSql, bufferLength, outLengthPtr); + }); +} + +SQLRETURN SQL_API SQLSetCursorName(SQLHSTMT statementHandle, SQLCHAR* cursorName, SQLSMALLINT nameLength) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->SetCursorName(NYdb::NOdbc::GetString(cursorName, nameLength)); + }); +} + +SQLRETURN SQL_API SQLGetCursorName(SQLHSTMT statementHandle, + SQLCHAR* cursorName, + SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->GetCursorName(cursorName, bufferLength, nameLengthPtr); + }); +} + +SQLRETURN SQL_API SQLPrimaryKeys(SQLHSTMT statementHandle, + SQLCHAR* catalogName, SQLSMALLINT nameLength1, + SQLCHAR* schemaName, SQLSMALLINT nameLength2, + SQLCHAR* tableName, SQLSMALLINT nameLength3) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->PrimaryKeys( + NYdb::NOdbc::GetString(catalogName, nameLength1), + NYdb::NOdbc::GetString(schemaName, nameLength2), + NYdb::NOdbc::GetString(tableName, nameLength3)); + }); +} + +SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT statementHandle, + SQLCHAR* pkCatalogName, SQLSMALLINT nameLength1, + SQLCHAR* pkSchemaName, SQLSMALLINT nameLength2, + SQLCHAR* pkTableName, SQLSMALLINT nameLength3, + SQLCHAR* fkCatalogName, SQLSMALLINT nameLength4, + SQLCHAR* fkSchemaName, SQLSMALLINT nameLength5, + SQLCHAR* fkTableName, SQLSMALLINT nameLength6) { + return NYdb::NOdbc::HandleOdbcExceptions(statementHandle, [&](auto* stmt) { + return stmt->ForeignKeys( + NYdb::NOdbc::GetString(pkCatalogName, nameLength1), + NYdb::NOdbc::GetString(pkSchemaName, nameLength2), + NYdb::NOdbc::GetString(pkTableName, nameLength3), + NYdb::NOdbc::GetString(fkCatalogName, nameLength4), + NYdb::NOdbc::GetString(fkSchemaName, nameLength5), + NYdb::NOdbc::GetString(fkTableName, nameLength6)); + }); +} + +SQLRETURN SQL_API SQLGetDescField(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, + SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(descriptorHandle, [&](auto* desc) { + return desc->GetDescField(recNumber, fieldIdentifier, value, bufferLength, stringLengthPtr); + }); +} + +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) { + return NYdb::NOdbc::HandleOdbcExceptions(descriptorHandle, [&](auto* desc) { + return desc->GetDescRec(recNumber, name, bufferLength, stringLengthPtr, typePtr, subTypePtr, + lengthPtr, precisionPtr, scalePtr, nullablePtr); + }); +} + +SQLRETURN SQL_API SQLSetDescField(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, + SQLPOINTER value, SQLINTEGER bufferLength) { + return NYdb::NOdbc::HandleOdbcExceptions(descriptorHandle, [&](auto* desc) { + return desc->SetDescField(recNumber, fieldIdentifier, value, bufferLength); + }); +} + +SQLRETURN SQL_API SQLSetDescRec(SQLHDESC descriptorHandle, SQLSMALLINT recNumber, SQLSMALLINT type, + SQLSMALLINT subType, SQLLEN length, SQLSMALLINT precision, SQLSMALLINT scale, + SQLPOINTER dataPtr, SQLLEN* stringLengthPtr, SQLLEN* indicatorPtr) { + return NYdb::NOdbc::HandleOdbcExceptions(descriptorHandle, [&](auto* desc) { + return desc->SetDescRec(recNumber, type, subType, length, precision, scale, dataPtr, + stringLengthPtr, indicatorPtr); + }); +} + +SQLRETURN SQL_API SQLCopyDesc(SQLHDESC sourceDesc, SQLHDESC targetDesc) { + return NYdb::NOdbc::HandleOdbcExceptions(sourceDesc, [&](auto* src) { + return src->CopyDesc(NYdb::NOdbc::TDescriptor::FromHandle(targetDesc)); }); } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 006b2d6b7e8..399bbddbb91 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -2,10 +2,13 @@ #include "utils/convert.h" #include "utils/types.h" -#include "utils/error_manager.h" +#include "utils/diag.h" #include "utils/escape.h" #include "utils/param_rewrite.h" #include "utils/sql_like.h" +#include "utils/type_info_rows.h" +#include "utils/util.h" +#include "utils/status_util.h" #include #include @@ -17,47 +20,53 @@ #include #include #include +#include namespace NYdb { namespace NOdbc { namespace { - bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen) { - if (sLen < prefixLen) { - return false; - } - for (size_t i = 0; i < prefixLen; ++i) { - if (std::tolower(static_cast(s[i])) != - std::tolower(static_cast(prefix[i]))) { - return false; - } - } - return true; - } - bool IsDdlQuery(const std::string& queryText) { - size_t pos = 0; - while (pos < queryText.size() && std::isspace(static_cast(queryText[pos]))) { - ++pos; - } - if (queryText.size() - pos < 6) { - return false; + size_t i = 0; + while (i < queryText.size()) { + if (std::isspace(static_cast(queryText[i]))) { + ++i; + } else if (queryText[i] == '-' && i + 1 < queryText.size() && queryText[i + 1] == '-') { + while (i < queryText.size() && queryText[i] != '\n') { + ++i; + } + } else if (queryText[i] == '/' && i + 1 < queryText.size() && queryText[i + 1] == '*') { + i += 2; + while (i + 1 < queryText.size() && !(queryText[i] == '*' && queryText[i + 1] == '/')) { + ++i; + } + if (i + 1 < queryText.size()) { + i += 2; + } else { + i = queryText.size(); + } + } else { + break; + } } - const char* start = queryText.c_str() + pos; - const size_t remaining = queryText.size() - pos; + const char* start = queryText.c_str() + i; + const size_t remaining = queryText.size() - i; return StartsWithPrefix(start, remaining, "CREATE", 6) || StartsWithPrefix(start, remaining, "DROP", 4) || - StartsWithPrefix(start, remaining, "ALTER", 5); + StartsWithPrefix(start, remaining, "ALTER", 5) || + StartsWithPrefix(start, remaining, "GRANT", 5) || + StartsWithPrefix(start, remaining, "REVOKE", 6); } - NYdb::TStatus StatusFrom(const NYdb::TStatus& ydb_status) { - return NYdb::TStatus(ydb_status.GetStatus(), NYdb::NIssue::TIssues(ydb_status.GetIssues())); - } } TStatement::TStatement(TConnection* conn) - : Conn_(conn) {} + : Conn_(conn) + , AppRowDesc_(std::make_unique(EDescType::AppRow, this)) + , AppParamDesc_(std::make_unique(EDescType::AppParam, this)) + , ImpRowDesc_(std::make_unique(EDescType::ImpRow, this)) + , ImpParamDesc_(std::make_unique(EDescType::ImpParam, this)) {} SQLRETURN TStatement::Prepare(const std::string& statementText) { StreamFetchError_ = false; @@ -65,6 +74,7 @@ SQLRETURN TStatement::Prepare(const std::string& statementText) { Cursor_.reset(); PreparedQuery_ = statementText; IsPrepared_ = true; + ParamCount_ = CountOdbcParams(PreparedQuery_); return SQL_SUCCESS; } @@ -72,6 +82,18 @@ SQLRETURN TStatement::Execute() { if (!IsPrepared_ || PreparedQuery_.empty()) { throw TOdbcException("HY007", 0, "No prepared statement"); } + const SQLUSMALLINT next = FindNextNeedDataParam(); + if (next != 0) { + NeedDataParam_ = next; + InAtExec_ = true; + return SQL_NEED_DATA; + } + InAtExec_ = false; + NeedDataParam_ = 0; + return ExecuteInternal(); +} + +SQLRETURN TStatement::ExecuteInternal() { StreamFetchError_ = false; RowsFetched_ = 0; Cursor_.reset(); @@ -114,9 +136,21 @@ SQLRETURN TStatement::Execute() { NStatusHelpers::ThrowOnError(created.Status); Cursor_ = std::move(created.Cursor); } + RowCount_ = Cursor_ ? -1 : 0; + InAtExec_ = false; + NeedDataParam_ = 0; return SQL_SUCCESS; } +SQLUSMALLINT TStatement::FindNextNeedDataParam() const { + for (const auto& param : BoundParams_) { + if (param.AtExec && !param.AtExecComplete) { + return param.ParamNumber; + } + } + return 0; +} + NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() { NYdb::NRetry::TRetryOperationSettings settings; settings.Idempotent(true); @@ -128,7 +162,7 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() return settings; } -NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params){ +NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params) { const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ : RewriteOdbcEscapes(PreparedQuery_); @@ -136,6 +170,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession if (!rewritten.Success) { throw TOdbcException(rewritten.SqlState, 0, rewritten.Message); } + const bool isDdl = IsDdlQuery(rewritten.Sql); const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); @@ -147,8 +182,6 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession // TS_SNAPSHOT_RW doesn't support explicit BeginTx() - we use NoTx() instead // DDL must use NoTx() per YDB documentation const bool isSnapshotRw = (txSettings.GetMode() == NQuery::TTxSettings::TS_SNAPSHOT_RW); - - const bool isDdl = IsDdlQuery(queryText); if (isSnapshotRw || isDdl) { return session.StreamExecuteQuery( @@ -179,7 +212,6 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession SQLRETURN TStatement::Fetch() { if (!Cursor_) { - Cursor_.reset(); return SQL_NO_DATA; } const SQLULEN maxRows = Attributes_.GetMaxRows(); @@ -191,7 +223,10 @@ SQLRETURN TStatement::Fetch() { return StreamFetchError_ ? SQL_ERROR : SQL_NO_DATA; } ++RowsFetched_; - return SQL_SUCCESS; + if (LastFetchRc_ != SQL_SUCCESS) { + return LastFetchRc_; + } + return GetLastReturnCode() == SQL_SUCCESS_WITH_INFO ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; } void TStatement::OnStreamPartError(const TStatus& status) { @@ -205,15 +240,31 @@ SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, if (!Cursor_) { return SQL_NO_DATA; } - return Cursor_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + const SQLRETURN rc = Cursor_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + if (const char* sqlState = ConsumeLastConvertSqlState()) { + AddError(sqlState, 0, std::strcmp(sqlState, "22003") == 0 ? "Numeric value out of range" : "Conversion error"); + } + return rc; } void TStatement::FillBoundColumns() { if (!Cursor_) { return; } + LastFetchRc_ = SQL_SUCCESS; for (const auto& col : BoundColumns_) { - Cursor_->GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + const SQLRETURN rc = Cursor_->GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + if (rc == SQL_SUCCESS_WITH_INFO) { + AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); + if (LastFetchRc_ == SQL_SUCCESS) { + LastFetchRc_ = SQL_SUCCESS_WITH_INFO; + } + } else if (rc != SQL_SUCCESS && LastFetchRc_ == SQL_SUCCESS) { + if (const char* sqlState = ConsumeLastConvertSqlState()) { + AddError(sqlState, 0, std::strcmp(sqlState, "22003") == 0 ? "Numeric value out of range" : "Conversion error"); + } + LastFetchRc_ = rc; + } } } @@ -252,13 +303,16 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, throw TOdbcException("HYC00", 0, "Only input parameters are supported"); } + const bool atExec = strLenOrIndPtr && *strLenOrIndPtr == SQL_DATA_AT_EXEC; + BoundParams_.erase(std::remove_if(BoundParams_.begin(), BoundParams_.end(), [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }), BoundParams_.end()); - if (!parameterValuePtr) { + if (!parameterValuePtr && !atExec) { return SQL_SUCCESS; } - BoundParams_.push_back({paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, parameterValuePtr, bufferLength, strLenOrIndPtr}); + BoundParams_.push_back({paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, + parameterValuePtr, bufferLength, strLenOrIndPtr, atExec, false, {}}); return SQL_SUCCESS; } @@ -267,6 +321,21 @@ SQLRETURN TStatement::BuildParams(NYdb::TParams& out) { NYdb::TParamsBuilder paramsBuilder; for (const auto& param : BoundParams_) { const std::string paramName = "$p" + std::to_string(param.ParamNumber); + if (param.AtExec) { + if (!param.AtExecComplete || param.AtExecChunk.empty()) { + return AddError("HY000", 0, "Missing data-at-execution parameter value"); + } + SQLLEN nts = SQL_NTS; + TBoundParam tmp = param; + tmp.ParameterValuePtr = const_cast(param.AtExecChunk.data()); + tmp.StrLenOrIndPtr = &nts; + const SQLRETURN convRc = ConvertParam(tmp, paramsBuilder.AddParam(paramName)); + if (convRc != SQL_SUCCESS) { + return AddError("07006", 0, "Unsupported or invalid ODBC parameter type for parameter " + + std::to_string(param.ParamNumber)); + } + continue; + } const SQLRETURN convRc = ConvertParam(param, paramsBuilder.AddParam(paramName)); if (convRc != SQL_SUCCESS) { return AddError( @@ -281,236 +350,124 @@ SQLRETURN TStatement::BuildParams(NYdb::TParams& out) { return SQL_SUCCESS; } -SQLRETURN TStatement::Columns(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, - const std::string& columnName) { - ClearErrors(); - RowsFetched_ = 0; - Cursor_.reset(); - std::vector columns = { - {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, - {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, - {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, - {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, - {"DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, - {"TYPE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, - {"COLUMN_SIZE", SQL_INTEGER, 0, SQL_NULLABLE}, - {"BUFFER_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, - {"DECIMAL_DIGITS", SQL_INTEGER, 0, SQL_NULLABLE}, - {"NUM_PREC_RADIX", SQL_INTEGER, 0, SQL_NULLABLE}, - {"NULLABLE", SQL_INTEGER, 0, SQL_NO_NULLS}, - {"REMARKS", SQL_VARCHAR, 762, SQL_NULLABLE}, - {"COLUMN_DEF", SQL_VARCHAR, 254, SQL_NULLABLE}, - {"SQL_DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, - {"SQL_DATETIME_SUB", SQL_INTEGER, 0, SQL_NULLABLE}, - {"CHAR_OCTET_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, - {"ORDINAL_POSITION", SQL_INTEGER, 0, SQL_NO_NULLS}, - {"IS_NULLABLE", SQL_VARCHAR, 254, SQL_NO_NULLS} - }; - - auto entries = GetPatternEntries(tableName); - - TTable table; - table.reserve(entries.size()); - - if (entries.empty()) { - Cursor_ = CreateVirtualCursor(this, columns, table); - return SQL_SUCCESS; +SQLRETURN TStatement::NumParams(SQLSMALLINT* paramCount) { + if (!paramCount) { + throw TOdbcException("HY000", 0, "Invalid parameter"); } - - for (const auto& entry : entries) { - if (entry.Type != NScheme::ESchemeEntryType::Table && - entry.Type != NScheme::ESchemeEntryType::ColumnTable) { - continue; - } - - auto tableClient = Conn_->GetTableClient(); - if (!tableClient) { - throw TOdbcException("HY000", 0, "No client connection"); - } - - auto status = tableClient->RetryOperationSync([this, path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { - auto result = session.DescribeTable(path).ExtractValueSync(); - NStatusHelpers::ThrowOnError(result); - - auto columns = result.GetTableDescription().GetTableColumns(); - - auto columnMatches = [&](const NTable::TTableColumn& column) { - if (columnName.empty()) { - return true; - } - if (Attributes_.GetMetadataId() == SQL_TRUE) { - return column.Name == columnName; - } - return SqlLikeMatch(column.Name, columnName); - }; - - bool foundColumn = false; - for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) { - const auto& column = columns[columnIndex]; - if (!columnMatches(column)) { - continue; - } - foundColumn = true; - - table.push_back({ - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().Utf8(path).Build(), - TValueBuilder().Utf8(column.Name).Build(), - TValueBuilder().Int16(GetTypeId(column.Type)).Build(), - TValueBuilder().Utf8(column.Type.ToString()).Build(), - TValueBuilder().OptionalInt32(std::nullopt).Build(), - TValueBuilder().OptionalInt32(std::nullopt).Build(), - TValueBuilder().OptionalInt16(GetDecimalDigits(column.Type)).Build(), - TValueBuilder().OptionalInt16(GetRadix(column.Type)).Build(), - TValueBuilder().Int16(column.NotNull && *column.NotNull ? SQL_NO_NULLS : SQL_NULLABLE).Build(), - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().Int16(GetTypeId(column.Type)).Build(), - TValueBuilder().OptionalInt16(std::nullopt).Build(), - TValueBuilder().OptionalInt32(8).Build(), - TValueBuilder().OptionalInt32(columnIndex + 1).Build(), - TValueBuilder().Utf8(column.NotNull && *column.NotNull ? "NO" : "YES").Build(), - }); - } - if (!foundColumn) { - throw TOdbcException("42S22", 0, "Column not found", SQL_ERROR); - } - return TStatus(EStatus::SUCCESS, {}); - }); - - NStatusHelpers::ThrowOnError(status); + if (!IsPrepared_) { + throw TOdbcException("HY010", 0, "Function sequence error"); } - - Cursor_ = CreateVirtualCursor(this, columns, table); + *paramCount = ParamCount_; return SQL_SUCCESS; } -SQLRETURN TStatement::Tables(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, - const std::string& tableType) { +void TStatement::ResetForMetadata() { ClearErrors(); RowsFetched_ = 0; Cursor_.reset(); +} - std::vector columns = { - {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, - {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, - {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, - {"TABLE_TYPE", SQL_VARCHAR, 128, SQL_NO_NULLS}, - {"REMARKS", SQL_VARCHAR, 254, SQL_NULLABLE} - }; - - auto entries = GetPatternEntries(tableName); - - TTable table; - table.reserve(entries.size()); - - for (const auto& entry : entries) { - auto tableType = GetTableType(entry.Type); - if (!tableType) { - continue; - } - - table.push_back({ - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - TValueBuilder().Utf8(entry.Name).Build(), - TValueBuilder().Utf8(*tableType).Build(), - TValueBuilder().OptionalUtf8(std::nullopt).Build(), - }); +SQLRETURN TStatement::DescribeParam(SQLUSMALLINT paramNumber, SQLSMALLINT* dataTypePtr, SQLULEN* paramSizePtr, + SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr) { + if (!IsPrepared_) { + throw TOdbcException("HY010", 0, "Function sequence error"); + } + if (paramNumber < 1 || paramNumber > ParamCount_) { + throw TOdbcException("07009", 0, "Invalid descriptor index"); + } + const auto it = std::find_if(BoundParams_.begin(), BoundParams_.end(), + [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }); + const SQLSMALLINT dataType = it != BoundParams_.end() ? it->ParameterType : SQL_UNKNOWN_TYPE; + const SQLULEN paramSize = it != BoundParams_.end() ? it->ColumnSize : 0; + const SQLSMALLINT decimalDigits = it != BoundParams_.end() ? it->DecimalDigits : 0; + const SQLSMALLINT nullable = it != BoundParams_.end() ? SQL_NULLABLE : SQL_NULLABLE_UNKNOWN; + if (dataTypePtr) { + *dataTypePtr = dataType; + } + if (paramSizePtr) { + *paramSizePtr = paramSize; + } + if (decimalDigitsPtr) { + *decimalDigitsPtr = decimalDigits; + } + if (nullablePtr) { + *nullablePtr = nullable; } - - Cursor_ = CreateVirtualCursor(this, columns, table); return SQL_SUCCESS; } -std::vector TStatement::GetPatternEntries(const std::string& pattern) { - std::vector entries; - VisitEntry("", pattern, entries); - return entries; +SQLRETURN TStatement::ParamData(SQLPOINTER* valuePtr) { + if (!valuePtr) { + throw TOdbcException("HY009", 0, "Invalid use of null pointer"); + } + if (!InAtExec_) { + return SQL_NO_DATA; + } + const SQLUSMALLINT next = FindNextNeedDataParam(); + if (next != 0) { + NeedDataParam_ = next; + *valuePtr = reinterpret_cast(static_cast(next)); + return SQL_NEED_DATA; + } + InAtExec_ = false; + NeedDataParam_ = 0; + return ExecuteInternal(); } -SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries) { - auto schemeClient = Conn_->GetSchemeClient(); - if (!schemeClient) { - throw TOdbcException("HY000", 0, "No client connection"); +SQLRETURN TStatement::PutData(SQLPOINTER data, SQLLEN strLenOrInd) { + if (!InAtExec_ || NeedDataParam_ == 0) { + throw TOdbcException("HY010", 0, "Function sequence error"); } - auto listDirectoryResult = schemeClient->ListDirectory(path + "/").ExtractValueSync(); - NStatusHelpers::ThrowOnError(listDirectoryResult); - - for (const auto& entry : listDirectoryResult.GetChildren()) { - std::string fullPath = path + "/" + entry.Name; - if (entry.Type == NScheme::ESchemeEntryType::Directory || - entry.Type == NScheme::ESchemeEntryType::SubDomain) { - VisitEntry(fullPath, pattern, resultEntries); - } else if (IsPatternMatch(fullPath, pattern)) { - NScheme::TSchemeEntry entryCopy = entry; - entryCopy.Name = fullPath; - resultEntries.push_back(entryCopy); + for (auto& param : BoundParams_) { + if (param.ParamNumber != NeedDataParam_) { + continue; } + SQLLEN chunkLen = strLenOrInd; + if (chunkLen == SQL_NTS) { + if (!data) { + throw TOdbcException("HY009", 0, "Invalid use of null pointer"); + } + chunkLen = static_cast(std::strlen(static_cast(data))); + } + if (chunkLen > 0 && data) { + const char* bytes = static_cast(data); + param.AtExecChunk.append(bytes, static_cast(chunkLen)); + } + if (strLenOrInd == 0 || strLenOrInd == SQL_NTS) { + param.AtExecComplete = true; + NeedDataParam_ = 0; + } + break; } return SQL_SUCCESS; } -bool TStatement::IsPatternMatch(const std::string& path, const std::string& pattern) { - if (pattern.empty()) { - return true; +SQLRETURN TStatement::Cancel() { + if (!Cursor_ && !InAtExec_) { + return SQL_SUCCESS; } - if (Attributes_.GetMetadataId() == SQL_TRUE) { - return path == pattern; + Cursor_.reset(); + InAtExec_ = false; + NeedDataParam_ = 0; + for (auto& param : BoundParams_) { + param.AtExecComplete = false; + param.AtExecChunk.clear(); } - return SqlLikeMatch(path, pattern); + RowsFetched_ = 0; + return SQL_SUCCESS; } -std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { - switch (type) { - case NScheme::ESchemeEntryType::Table: - return "TABLE"; - case NScheme::ESchemeEntryType::View: - return "VIEW"; - case NScheme::ESchemeEntryType::ColumnStore: - return "COLUMN_STORE"; - case NScheme::ESchemeEntryType::ColumnTable: - return "COLUMN_TABLE"; - case NScheme::ESchemeEntryType::Sequence: - return "SEQUENCE"; - case NScheme::ESchemeEntryType::Replication: - return "REPLICATION"; - case NScheme::ESchemeEntryType::Topic: - return "TOPIC"; - case NScheme::ESchemeEntryType::ExternalTable: - return "EXTERNAL_TABLE"; - case NScheme::ESchemeEntryType::ExternalDataSource: - return "EXTERNAL_DATA_SOURCE"; - case NScheme::ESchemeEntryType::ResourcePool: - return "RESOURCE_POOL"; - case NScheme::ESchemeEntryType::PqGroup: - return "PQ_GROUP"; - case NScheme::ESchemeEntryType::RtmrVolume: - return "RTMR_VOLUME"; - case NScheme::ESchemeEntryType::BlockStoreVolume: - return "BLOCK_STORE_VOLUME"; - case NScheme::ESchemeEntryType::CoordinationNode: - return "COORDINATION_NODE"; - case NScheme::ESchemeEntryType::Unknown: - return "UNKNOWN"; - case NScheme::ESchemeEntryType::SysView: - return "SYSTEM VIEW"; - case NScheme::ESchemeEntryType::Transfer: - return "TRANSFER"; - case NScheme::ESchemeEntryType::Directory: - case NScheme::ESchemeEntryType::SubDomain: - return std::nullopt; - default: - return std::nullopt; - } +SQLRETURN TStatement::SetCursorName(const std::string& name) { + CursorName_ = name; + return SQL_SUCCESS; } +SQLRETURN TStatement::GetCursorName(SQLCHAR* name, SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr) { + return Diag::WriteOdbcString(*this, CursorName_, name, bufferLength, nameLengthPtr); +} + + SQLRETURN TStatement::Close(bool force) { if (!force && !Cursor_) { throw TOdbcException("24000", 0, "Invalid handle"); @@ -535,7 +492,7 @@ SQLRETURN TStatement::RowCount(SQLLEN* rowCount) { throw TOdbcException("HY000", 0, "Invalid parameter"); } - *rowCount = -1; + *rowCount = RowCount_; return SQL_SUCCESS; } @@ -561,6 +518,25 @@ SQLRETURN TStatement::SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER } SQLRETURN TStatement::GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { + if (!value) { + return AddError("HY009", 0, "Invalid use of null pointer"); + } + switch (attr) { + case SQL_ATTR_APP_ROW_DESC: + *reinterpret_cast(value) = AppRowDesc_.get(); + return SQL_SUCCESS; + case SQL_ATTR_APP_PARAM_DESC: + *reinterpret_cast(value) = AppParamDesc_.get(); + return SQL_SUCCESS; + case SQL_ATTR_IMP_ROW_DESC: + *reinterpret_cast(value) = ImpRowDesc_.get(); + return SQL_SUCCESS; + case SQL_ATTR_IMP_PARAM_DESC: + *reinterpret_cast(value) = ImpParamDesc_.get(); + return SQL_SUCCESS; + default: + break; + } return Attributes_.GetStmtAttr(attr, value, bufferLength, stringLengthPtr, *this); } diff --git a/odbc/src/statement.h b/odbc/src/statement.h index e74ce58b84e..7a49ea48b02 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -2,6 +2,7 @@ #include "connection.h" #include "statement_attr.h" +#include "descriptor.h" #include "utils/error_manager.h" #include "utils/bindings.h" #include "utils/cursor.h" @@ -20,11 +21,13 @@ namespace NYdb { namespace NOdbc { class TStatement : public TErrorManager, public IBindingFiller { + friend class TDescriptor; public: TStatement(TConnection* conn); SQLRETURN Prepare(const std::string& statementText); SQLRETURN Execute(); + SQLRETURN ExecuteInternal(); SQLRETURN Fetch(); SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, @@ -50,6 +53,40 @@ class TStatement : public TErrorManager, public IBindingFiller { const std::string& tableName, const std::string& tableType); + SQLRETURN GetTypeInfo(SQLSMALLINT dataType); + SQLRETURN Statistics(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + SQLUSMALLINT unique, + SQLUSMALLINT accuracy); + SQLRETURN SpecialColumns(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + SQLUSMALLINT identifierType, + SQLUSMALLINT scope); + SQLRETURN PrimaryKeys(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName); + SQLRETURN ForeignKeys(const std::string& pkCatalogName, + const std::string& pkSchemaName, + const std::string& pkTableName, + const std::string& fkCatalogName, + const std::string& fkSchemaName, + const std::string& fkTableName); + SQLRETURN NumParams(SQLSMALLINT* paramCount); + SQLRETURN DescribeParam(SQLUSMALLINT paramNumber, SQLSMALLINT* dataTypePtr, SQLULEN* paramSizePtr, + SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr); + SQLRETURN ParamData(SQLPOINTER* valuePtr); + SQLRETURN PutData(SQLPOINTER data, SQLLEN strLenOrInd); + SQLRETURN Cancel(); + SQLRETURN SetCursorName(const std::string& name); + SQLRETURN GetCursorName(SQLCHAR* name, SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr); + + TDescriptor& GetAppRowDesc() { return *AppRowDesc_; } + TDescriptor& GetAppParamDesc() { return *AppParamDesc_; } + TDescriptor& GetImpRowDesc() { return *ImpRowDesc_; } + TDescriptor& GetImpParamDesc() { return *ImpParamDesc_; } + SQLRETURN RowCount(SQLLEN* rowCount); SQLRETURN NumResultCols(SQLSMALLINT* colCount); const std::vector& GetColumnMeta() const; @@ -59,6 +96,8 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) override; + SQLSMALLINT GetParamCount() const { return ParamCount_; } + TConnection* GetConnection() { return Conn_; } @@ -68,15 +107,30 @@ class TStatement : public TErrorManager, public IBindingFiller { std::unique_ptr Cursor_; std::string PreparedQuery_; bool IsPrepared_ = false; + SQLSMALLINT ParamCount_ = 0; std::vector BoundColumns_; std::vector BoundParams_; bool StreamFetchError_ = false; SQLULEN RowsFetched_ = 0; + SQLLEN RowCount_ = -1; TStatementAttributes Attributes_; + std::string CursorName_; + std::unique_ptr AppRowDesc_; + std::unique_ptr AppParamDesc_; + std::unique_ptr ImpRowDesc_; + std::unique_ptr ImpParamDesc_; + SQLUSMALLINT NeedDataParam_ = 0; + bool InAtExec_ = false; + SQLRETURN LastFetchRc_ = SQL_SUCCESS; SQLRETURN BuildParams(NYdb::TParams& out); - + + void ResetForMetadata(); + + SQLUSMALLINT FindNextNeedDataParam() const; + std::string GetTraversalRoot(const std::string& pattern) const; + NQuery::TExecuteQueryIterator CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp new file mode 100644 index 00000000000..1a7744e31a0 --- /dev/null +++ b/odbc/src/statement_metadata.cpp @@ -0,0 +1,542 @@ +#include "statement.h" + +#include "utils/types.h" +#include "utils/sql_like.h" +#include "utils/type_info_rows.h" +#include "utils/cursor.h" +#include "utils/util.h" + +#include + +#include +#include +#include +#include + +namespace NYdb { +namespace NOdbc { + +namespace { + +bool MatchesTableTypeFilter(const std::string& filter, const std::string& entryType) { + if (filter.empty()) { + return true; + } + size_t start = 0; + while (start <= filter.size()) { + const size_t comma = filter.find(',', start); + std::string token = filter.substr(start, comma == std::string::npos ? std::string::npos : comma - start); + while (!token.empty() && std::isspace(static_cast(token.front()))) { + token.erase(token.begin()); + } + while (!token.empty() && std::isspace(static_cast(token.back()))) { + token.pop_back(); + } + if (token.size() >= 2 && token.front() == '\'' && token.back() == '\'') { + token = token.substr(1, token.size() - 2); + } + if (!token.empty() && token.size() == entryType.size() && + StartsWithPrefix(entryType.c_str(), entryType.size(), token.c_str(), token.size())) { + return true; + } + if (comma == std::string::npos) { + break; + } + start = comma + 1; + } + return false; +} + +namespace NColumnsRow { +constexpr int kTableCat = 0; +constexpr int kTableSchem = 1; +constexpr int kTableName = 2; +constexpr int kColumnName = 3; +constexpr int kDataType = 4; +constexpr int kTypeName = 5; +constexpr int kColumnSize = 6; +constexpr int kBufferLength = 7; +constexpr int kDecimalDigits = 8; +constexpr int kNumPrecRadix = 9; +constexpr int kNullable = 10; +constexpr int kRemarks = 11; +constexpr int kColumnDef = 12; +constexpr int kSqlDataType = 13; +constexpr int kSqlDatetimeSub = 14; +constexpr int kCharOctetLength = 15; +constexpr int kOrdinalPosition = 16; +constexpr int kIsNullable = 17; +} // namespace NColumnsRow + +} // namespace + +SQLRETURN TStatement::Columns(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& columnName) { + ResetForMetadata(); + + std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"TYPE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_SIZE", SQL_INTEGER, 0, SQL_NULLABLE}, + {"BUFFER_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, + {"DECIMAL_DIGITS", SQL_INTEGER, 0, SQL_NULLABLE}, + {"NUM_PREC_RADIX", SQL_INTEGER, 0, SQL_NULLABLE}, + {"NULLABLE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"REMARKS", SQL_VARCHAR, 762, SQL_NULLABLE}, + {"COLUMN_DEF", SQL_VARCHAR, 254, SQL_NULLABLE}, + {"SQL_DATA_TYPE", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"SQL_DATETIME_SUB", SQL_INTEGER, 0, SQL_NULLABLE}, + {"CHAR_OCTET_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, + {"ORDINAL_POSITION", SQL_INTEGER, 0, SQL_NO_NULLS}, + {"IS_NULLABLE", SQL_VARCHAR, 254, SQL_NO_NULLS} + }; + + auto entries = GetPatternEntries(tableName); + + TTable table; + table.reserve(entries.size()); + + if (entries.empty()) { + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; + } + + for (const auto& entry : entries) { + if (entry.Type != NScheme::ESchemeEntryType::Table && + entry.Type != NScheme::ESchemeEntryType::ColumnTable) { + continue; + } + + auto tableClient = Conn_->GetTableClient(); + if (!tableClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } + + auto status = tableClient->RetryOperationSync([this, path = entry.Name, &table, &columnName](NTable::TSession session) -> TStatus { + auto result = session.DescribeTable(path).ExtractValueSync(); + NStatusHelpers::ThrowOnError(result); + + auto columns = result.GetTableDescription().GetTableColumns(); + + auto columnMatches = [&](const NTable::TTableColumn& column) { + if (columnName.empty()) { + return true; + } + if (Attributes_.GetMetadataId() == SQL_TRUE) { + return column.Name == columnName; + } + return SqlLikeMatch(column.Name, columnName); + }; + + bool foundColumn = false; + for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) { + const auto& column = columns[columnIndex]; + if (!columnMatches(column)) { + continue; + } + foundColumn = true; + + const auto sqlType = GetTypeId(column.Type); + const auto colSize = GetColumnSize(sqlType); + const auto decDigits = GetDecimalDigits(column.Type); + const auto radix = GetRadix(column.Type); + const std::optional colSizeOpt = colSize > 0 ? std::optional(static_cast(colSize)) : std::nullopt; + + table.push_back({ + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Utf8(path).Build(), + TValueBuilder().Utf8(column.Name).Build(), + TValueBuilder().Int16(sqlType).Build(), + TValueBuilder().Utf8(column.Type.ToString()).Build(), + TValueBuilder().OptionalInt32(colSizeOpt).Build(), + TValueBuilder().OptionalInt32(colSizeOpt).Build(), + TValueBuilder().OptionalInt16(decDigits).Build(), + TValueBuilder().OptionalInt16(radix).Build(), + TValueBuilder().Int16(column.NotNull && *column.NotNull ? SQL_NO_NULLS : SQL_NULLABLE).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Int16(sqlType).Build(), + TValueBuilder().OptionalInt16(std::nullopt).Build(), + TValueBuilder().OptionalInt32(colSizeOpt).Build(), + TValueBuilder().OptionalInt32(columnIndex + 1).Build(), + TValueBuilder().Utf8(column.NotNull && *column.NotNull ? "NO" : "YES").Build(), + }); + } + if (!foundColumn && !columnName.empty()) { + return TStatus(EStatus::SUCCESS, {}); + } + return TStatus(EStatus::SUCCESS, {}); + }); + + NStatusHelpers::ThrowOnError(status); + } + + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::Tables(const std::string& catalogName, + const std::string& schemaName, + const std::string& tableName, + const std::string& tableType) { + ResetForMetadata(); + + std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"TABLE_TYPE", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"REMARKS", SQL_VARCHAR, 254, SQL_NULLABLE} + }; + + auto entries = GetPatternEntries(tableName); + + TTable table; + table.reserve(entries.size()); + + for (const auto& entry : entries) { + const auto entryType = GetTableType(entry.Type); + if (!entryType || !MatchesTableTypeFilter(tableType, *entryType)) { + continue; + } + + table.push_back({ + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Utf8(entry.Name).Build(), + TValueBuilder().Utf8(*entryType).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + }); + } + + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::GetTypeInfo(SQLSMALLINT dataType) { + ResetForMetadata(); + + static const std::vector columns = { + {"TYPE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"DATA_TYPE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"COLUMN_SIZE", SQL_INTEGER, 0, SQL_NULLABLE}, + {"LITERAL_PREFIX", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"LITERAL_SUFFIX", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"CREATE_PARAMS", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"NULLABLE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"CASE_SENSITIVE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"SEARCHABLE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"UNSIGNED_ATTRIBUTE", SQL_CHAR, 1, SQL_NULLABLE}, + {"FIXED_PREC_SCALE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"AUTO_UNIQUE_VALUE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"LOCAL_TYPE_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"MINIMUM_SCALE", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"MAXIMUM_SCALE", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"SQL_DATA_TYPE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"SQL_DATETIME_SUB", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"NUM_PREC_RADIX", SQL_INTEGER, 0, SQL_NULLABLE}, + {"INTERVAL_PRECISION", SQL_SMALLINT, 0, SQL_NULLABLE}, + }; + + Cursor_ = CreateVirtualCursor(this, columns, BuildTypeInfoRows(dataType)); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::Statistics(const std::string& /*catalogName*/, + const std::string& /*schemaName*/, + const std::string& /*tableName*/, + SQLUSMALLINT /*unique*/, + SQLUSMALLINT /*accuracy*/) { + ResetForMetadata(); + + static const std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"NON_UNIQUE", SQL_CHAR, 1, SQL_NO_NULLS}, + {"INDEX_QUALIFIER", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"INDEX_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TYPE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"ORDINAL_POSITION", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"ASC_OR_DESC", SQL_CHAR, 1, SQL_NULLABLE}, + {"CARDINALITY", SQL_INTEGER, 0, SQL_NULLABLE}, + {"PAGES", SQL_INTEGER, 0, SQL_NULLABLE}, + {"FILTER_CONDITION", SQL_VARCHAR, 128, SQL_NULLABLE}, + }; + + Cursor_ = CreateVirtualCursor(this, columns, TTable{}); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::SpecialColumns(const std::string& /*catalogName*/, + const std::string& /*schemaName*/, + const std::string& tableName, + SQLUSMALLINT identifierType, + SQLUSMALLINT /*scope*/) { + if (identifierType != SQL_BEST_ROWID) { + return AddError("HYC00", 0, "Optional feature not implemented"); + } + + ResetForMetadata(); + + std::vector columns = { + {"SCOPE", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"DATA_TYPE", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"TYPE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_SIZE", SQL_INTEGER, 0, SQL_NULLABLE}, + {"BUFFER_LENGTH", SQL_INTEGER, 0, SQL_NULLABLE}, + {"DECIMAL_DIGITS", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"PSEUDO_COLUMN", SQL_SMALLINT, 0, SQL_NO_NULLS}, + }; + + TTable table; + auto entries = GetPatternEntries(tableName); + if (entries.size() != 1) { + if (entries.empty()) { + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; + } + throw TOdbcException("HY000", 0, "Ambiguous table name"); + } + + auto tableClient = Conn_->GetTableClient(); + if (!tableClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } + + const std::string path = entries.front().Name; + auto status = tableClient->RetryOperationSync([path, &table, &columns](NTable::TSession session) -> TStatus { + auto result = session.DescribeTable(path).ExtractValueSync(); + NStatusHelpers::ThrowOnError(result); + + const auto& pkColumns = result.GetTableDescription().GetPrimaryKeyColumns(); + const auto& tableColumns = result.GetTableDescription().GetTableColumns(); + for (const auto& pkName : pkColumns) { + const auto columnIt = std::ranges::find_if(tableColumns, + [&](const NTable::TTableColumn& column) { return column.Name == pkName; }); + if (columnIt == tableColumns.end()) { + continue; + } + const auto sqlType = GetTypeId(columnIt->Type); + const auto colSize = GetColumnSize(sqlType); + const std::optional colSizeOpt = colSize > 0 ? std::optional(static_cast(colSize)) : std::nullopt; + table.push_back({ + TValueBuilder().OptionalInt16(SQL_SCOPE_SESSION).Build(), + TValueBuilder().Utf8(pkName).Build(), + TValueBuilder().Int16(sqlType).Build(), + TValueBuilder().Utf8(columnIt->Type.ToString()).Build(), + TValueBuilder().OptionalInt32(colSizeOpt).Build(), + TValueBuilder().OptionalInt32(colSizeOpt).Build(), + TValueBuilder().OptionalInt16(GetDecimalDigits(columnIt->Type)).Build(), + TValueBuilder().Int16(SQL_PC_NOT_PSEUDO).Build(), + }); + } + return TStatus(EStatus::SUCCESS, {}); + }); + NStatusHelpers::ThrowOnError(status); + + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::PrimaryKeys(const std::string& /*catalogName*/, + const std::string& /*schemaName*/, + const std::string& tableName) { + ResetForMetadata(); + + std::vector columns = { + {"TABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"TABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"COLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"KEY_SEQ", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"PK_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + }; + + TTable table; + auto entries = GetPatternEntries(tableName); + if (entries.size() != 1) { + if (entries.empty()) { + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; + } + throw TOdbcException("HY000", 0, "Ambiguous table name"); + } + + auto tableClient = Conn_->GetTableClient(); + if (!tableClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } + + const std::string path = entries.front().Name; + auto status = tableClient->RetryOperationSync([path, &table](NTable::TSession session) -> TStatus { + auto result = session.DescribeTable(path).ExtractValueSync(); + NStatusHelpers::ThrowOnError(result); + + const auto& pkColumns = result.GetTableDescription().GetPrimaryKeyColumns(); + SQLSMALLINT keySeq = 1; + for (const auto& pkName : pkColumns) { + table.push_back({ + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + TValueBuilder().Utf8(path).Build(), + TValueBuilder().Utf8(pkName).Build(), + TValueBuilder().Int16(keySeq++).Build(), + TValueBuilder().OptionalUtf8(std::nullopt).Build(), + }); + } + return TStatus(EStatus::SUCCESS, {}); + }); + NStatusHelpers::ThrowOnError(status); + + Cursor_ = CreateVirtualCursor(this, columns, table); + return SQL_SUCCESS; +} + +SQLRETURN TStatement::ForeignKeys(const std::string& /*pkCatalogName*/, + const std::string& /*pkSchemaName*/, + const std::string& /*pkTableName*/, + const std::string& /*fkCatalogName*/, + const std::string& /*fkSchemaName*/, + const std::string& /*fkTableName*/) { + ResetForMetadata(); + + std::vector columns = { + {"PKTABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"PKTABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"PKTABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"PKCOLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"FKTABLE_CAT", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"FKTABLE_SCHEM", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"FKTABLE_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"FKCOLUMN_NAME", SQL_VARCHAR, 128, SQL_NO_NULLS}, + {"KEY_SEQ", SQL_SMALLINT, 0, SQL_NO_NULLS}, + {"UPDATE_RULE", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"DELETE_RULE", SQL_SMALLINT, 0, SQL_NULLABLE}, + {"FK_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"PK_NAME", SQL_VARCHAR, 128, SQL_NULLABLE}, + {"DEFERRABILITY", SQL_SMALLINT, 0, SQL_NULLABLE}, + }; + + Cursor_ = CreateVirtualCursor(this, columns, TTable{}); + return SQL_SUCCESS; +} + +std::string TStatement::GetTraversalRoot(const std::string& pattern) const { + if (pattern.empty()) { + return ""; + } + const auto hasWildcard = [](const std::string& value) { + return value.find('%') != std::string::npos || value.find('_') != std::string::npos; + }; + if (Attributes_.GetMetadataId() == SQL_TRUE && !hasWildcard(pattern)) { + const auto pos = pattern.find_last_of('/'); + return pos == std::string::npos ? "" : pattern.substr(0, pos); + } + size_t wildPos = pattern.size(); + const auto pct = pattern.find('%'); + const auto usc = pattern.find('_'); + if (pct != std::string::npos) { + wildPos = std::min(wildPos, pct); + } + if (usc != std::string::npos) { + wildPos = std::min(wildPos, usc); + } + const std::string prefix = pattern.substr(0, wildPos); + const auto pos = prefix.find_last_of('/'); + return pos == std::string::npos ? "" : prefix.substr(0, pos); +} + +std::vector TStatement::GetPatternEntries(const std::string& pattern) { + std::vector entries; + VisitEntry(GetTraversalRoot(pattern), pattern, entries); + return entries; +} + +SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries) { + auto schemeClient = Conn_->GetSchemeClient(); + if (!schemeClient) { + throw TOdbcException("HY000", 0, "No client connection"); + } + auto listDirectoryResult = schemeClient->ListDirectory(path + "/").ExtractValueSync(); + NStatusHelpers::ThrowOnError(listDirectoryResult); + + for (const auto& entry : listDirectoryResult.GetChildren()) { + std::string fullPath = path + "/" + entry.Name; + if (entry.Type == NScheme::ESchemeEntryType::Directory || + entry.Type == NScheme::ESchemeEntryType::SubDomain) { + VisitEntry(fullPath, pattern, resultEntries); + } else if (IsPatternMatch(fullPath, pattern)) { + NScheme::TSchemeEntry entryCopy = entry; + entryCopy.Name = fullPath; + resultEntries.push_back(entryCopy); + } + } + return SQL_SUCCESS; +} + +bool TStatement::IsPatternMatch(const std::string& path, const std::string& pattern) { + if (pattern.empty()) { + return true; + } + if (Attributes_.GetMetadataId() == SQL_TRUE) { + return path == pattern; + } + return SqlLikeMatch(path, pattern); +} + +std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { + switch (type) { + case NScheme::ESchemeEntryType::Table: + return "TABLE"; + case NScheme::ESchemeEntryType::View: + return "VIEW"; + case NScheme::ESchemeEntryType::ColumnStore: + return "COLUMN_STORE"; + case NScheme::ESchemeEntryType::ColumnTable: + return "COLUMN_TABLE"; + case NScheme::ESchemeEntryType::Sequence: + return "SEQUENCE"; + case NScheme::ESchemeEntryType::Replication: + return "REPLICATION"; + case NScheme::ESchemeEntryType::Topic: + return "TOPIC"; + case NScheme::ESchemeEntryType::ExternalTable: + return "EXTERNAL_TABLE"; + case NScheme::ESchemeEntryType::ExternalDataSource: + return "EXTERNAL_DATA_SOURCE"; + case NScheme::ESchemeEntryType::ResourcePool: + return "RESOURCE_POOL"; + case NScheme::ESchemeEntryType::PqGroup: + return "PQ_GROUP"; + case NScheme::ESchemeEntryType::RtmrVolume: + return "RTMR_VOLUME"; + case NScheme::ESchemeEntryType::BlockStoreVolume: + return "BLOCK_STORE_VOLUME"; + case NScheme::ESchemeEntryType::CoordinationNode: + return "COORDINATION_NODE"; + case NScheme::ESchemeEntryType::Unknown: + return "UNKNOWN"; + case NScheme::ESchemeEntryType::SysView: + return "SYSTEM VIEW"; + case NScheme::ESchemeEntryType::Transfer: + return "TRANSFER"; + case NScheme::ESchemeEntryType::Directory: + case NScheme::ESchemeEntryType::SubDomain: + return std::nullopt; + default: + return std::nullopt; + } +} + +} // namespace NOdbc +} // namespace NYdb diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h index bd0b8836b74..f75cd12289f 100644 --- a/odbc/src/utils/bindings.h +++ b/odbc/src/utils/bindings.h @@ -5,6 +5,8 @@ #include +#include + namespace NYdb { namespace NOdbc { @@ -18,6 +20,9 @@ struct TBoundParam { SQLPOINTER ParameterValuePtr; SQLLEN BufferLength; SQLLEN* StrLenOrIndPtr; + bool AtExec = false; + bool AtExecComplete = false; + std::string AtExecChunk; }; struct TBoundColumn { diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index db7928ce659..c25715c893c 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -4,11 +4,54 @@ #include #include +#include +#include #include +#include namespace NYdb { namespace NOdbc { +namespace { + +thread_local const char* LastConvertSqlState = nullptr; + +bool FitsInt16(int64_t value) { + return value >= INT16_MIN && value <= INT16_MAX; +} + +bool FitsInt32(int64_t value) { + return value >= INT32_MIN && value <= INT32_MAX; +} + +void SetNumericOutOfRange() { + LastConvertSqlState = "22003"; +} + +std::optional GetAsInt64(TValueParser& parser, EPrimitiveType ydbType) { + switch (ydbType) { + case EPrimitiveType::Int8: return parser.GetInt8(); + case EPrimitiveType::Uint8: return parser.GetUint8(); + case EPrimitiveType::Int16: return parser.GetInt16(); + case EPrimitiveType::Uint16: return parser.GetUint16(); + case EPrimitiveType::Int32: return parser.GetInt32(); + case EPrimitiveType::Uint32: return parser.GetUint32(); + case EPrimitiveType::Int64: return parser.GetInt64(); + case EPrimitiveType::Uint64: { + const uint64_t unsignedValue = parser.GetUint64(); + if (unsignedValue > static_cast(INT64_MAX)) { + SetNumericOutOfRange(); + return std::nullopt; + } + return static_cast(unsignedValue); + } + case EPrimitiveType::Bool: return parser.GetBool() ? 1 : 0; + default: return std::nullopt; + } +} + +} // namespace + template struct TSqlTypeTraits; @@ -348,6 +391,7 @@ SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder) { } SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { + LastConvertSqlState = nullptr; if (parser.IsNull()) { if (strLenOrInd) { *strLenOrInd = SQL_NULL_DATA; @@ -372,19 +416,16 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case SQL_C_SHORT: case SQL_C_SSHORT: { - SQLSMALLINT v = 0; - switch (ydbType) { - case EPrimitiveType::Int16: v = parser.GetInt16(); break; - case EPrimitiveType::Uint16: v = static_cast(parser.GetUint16()); break; - case EPrimitiveType::Int8: v = static_cast(parser.GetInt8()); break; - case EPrimitiveType::Uint8: v = static_cast(parser.GetUint8()); break; - case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; - case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; - case EPrimitiveType::Bool: v = parser.GetBool() ? 1 : 0; break; - default: return SQL_ERROR; + const auto raw = GetAsInt64(parser, ydbType); + if (!raw) { + return SQL_ERROR; + } + if (!FitsInt16(*raw)) { + SetNumericOutOfRange(); + return SQL_ERROR; } if (targetValue) { - *reinterpret_cast(targetValue) = v; + *reinterpret_cast(targetValue) = static_cast(*raw); } if (strLenOrInd) { *strLenOrInd = sizeof(SQLSMALLINT); @@ -394,21 +435,16 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER case SQL_C_SLONG: case SQL_C_LONG: { - int32_t v = 0; - switch (ydbType) { - case EPrimitiveType::Int16: v = static_cast(parser.GetInt16()); break; - case EPrimitiveType::Uint16: v = static_cast(parser.GetUint16()); break; - case EPrimitiveType::Int8: v = static_cast(parser.GetInt8()); break; - case EPrimitiveType::Uint8: v = static_cast(parser.GetUint8()); break; - case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; - case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; - case EPrimitiveType::Int64: v = static_cast(parser.GetInt64()); break; - case EPrimitiveType::Uint64: v = static_cast(parser.GetUint64()); break; - case EPrimitiveType::Bool: v = parser.GetBool() ? 1 : 0; break; - default: return SQL_ERROR; + const auto raw = GetAsInt64(parser, ydbType); + if (!raw) { + return SQL_ERROR; + } + if (!FitsInt32(*raw)) { + SetNumericOutOfRange(); + return SQL_ERROR; } if (targetValue) { - *reinterpret_cast(targetValue) = v; + *reinterpret_cast(targetValue) = static_cast(*raw); } if (strLenOrInd) { *strLenOrInd = sizeof(int32_t); @@ -417,16 +453,12 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER } case SQL_C_SBIGINT: { - SQLBIGINT v = 0; - switch (ydbType) { - case EPrimitiveType::Int64: v = parser.GetInt64(); break; - case EPrimitiveType::Uint64: v = static_cast(parser.GetUint64()); break; - case EPrimitiveType::Int32: v = static_cast(parser.GetInt32()); break; - case EPrimitiveType::Uint32: v = static_cast(parser.GetUint32()); break; - default: return SQL_ERROR; + const auto raw = GetAsInt64(parser, ydbType); + if (!raw) { + return SQL_ERROR; } if (targetValue) { - *reinterpret_cast(targetValue) = v; + *reinterpret_cast(targetValue) = static_cast(*raw); } if (strLenOrInd) { *strLenOrInd = sizeof(SQLBIGINT); @@ -517,11 +549,22 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER if (strLenOrInd) { *strLenOrInd = len; } + if (targetValue && bufferLength > 0 && len >= static_cast(bufferLength)) { + return SQL_SUCCESS_WITH_INFO; + } return SQL_SUCCESS; } case SQL_C_BIT: { - char v = parser.GetBool() ? 1 : 0; + const auto raw = GetAsInt64(parser, ydbType); + if (!raw) { + return SQL_ERROR; + } + if (*raw != 0 && *raw != 1) { + SetNumericOutOfRange(); + return SQL_ERROR; + } + const char v = *raw != 0 ? 1 : 0; if (targetValue) { *reinterpret_cast(targetValue) = v; } @@ -535,5 +578,11 @@ SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER } } +const char* ConsumeLastConvertSqlState() { + const char* result = LastConvertSqlState; + LastConvertSqlState = nullptr; + return result; +} + } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h index 8f8195ba1c8..27f14cc3f3a 100644 --- a/odbc/src/utils/convert.h +++ b/odbc/src/utils/convert.h @@ -12,6 +12,7 @@ namespace NOdbc { SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder); SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); +const char* ConsumeLastConvertSqlState(); } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index 533f0b20217..d7e521b6727 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -1,4 +1,5 @@ #include "cursor.h" +#include "status_util.h" #include "convert.h" #include "types.h" @@ -8,12 +9,6 @@ namespace NYdb { namespace NOdbc { -namespace { - -NYdb::TStatus StatusFrom(const NYdb::TStatus& ydbStatus) { - return NYdb::TStatus(ydbStatus.GetStatus(), NYdb::NIssue::TIssues(ydbStatus.GetIssues())); -} - NYdb::TStatus PrefetchFirstResultSet( NQuery::TExecuteQueryIterator& iterator, std::optional* resultSet) { @@ -34,8 +29,6 @@ NYdb::TStatus PrefetchFirstResultSet( return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); } -} // namespace - class TExecCursor : public ICursor { public: TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator, @@ -69,7 +62,6 @@ class TExecCursor : public ICursor { InitResultSet(part.ExtractResultSet()); } } - return false; } SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, diff --git a/odbc/src/utils/diag.h b/odbc/src/utils/diag.h index 5e2db740a07..3b87baae004 100644 --- a/odbc/src/utils/diag.h +++ b/odbc/src/utils/diag.h @@ -4,6 +4,8 @@ #include #include +#include +#include namespace NYdb::NOdbc { namespace Diag { @@ -28,6 +30,38 @@ namespace Diag { return errors.AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); } -} + inline SQLRETURN WriteOdbcString( + TErrorManager& errors, + std::string_view value, + SQLPOINTER outPtr, + SQLSMALLINT bufferLength, + SQLSMALLINT* lengthPtr) { + if (!outPtr) { + return errors.AddError("HY009", 0, "Invalid use of null pointer"); + } + if (bufferLength < 0) { + return errors.AddError("HY090", 0, "Invalid string or buffer length"); + } + const SQLLEN fullLen = static_cast(value.size()); + const SQLSMALLINT reportedLen = static_cast(std::min(fullLen, 32767)); + if (lengthPtr) { + *lengthPtr = reportedLen; + } + if (bufferLength == 0) { + return fullLen == 0 ? SQL_SUCCESS : AddRightTruncated(errors); + } + auto* out = reinterpret_cast(outPtr); + const SQLSMALLINT copyLen = static_cast(std::min(fullLen, static_cast(bufferLength - 1))); + if (copyLen > 0) { + std::memcpy(out, value.data(), static_cast(copyLen)); + } + out[copyLen] = '\0'; + if (copyLen < fullLen) { + return AddRightTruncated(errors); + } + return SQL_SUCCESS; + } + +} // namespace Diag -} // namespace NYdb::NOdbc::Diag +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h index e08083ed1f0..da25cd412ed 100644 --- a/odbc/src/utils/error_manager.h +++ b/odbc/src/utils/error_manager.h @@ -88,6 +88,26 @@ enum class ENullInputHandlePolicy : unsigned char { Allow, }; +template +SQLRETURN HandleOdbcExceptionsConsuming(SQLHANDLE handlePtr, std::function&& func) { + if (!handlePtr) { + return SQL_INVALID_HANDLE; + } + auto handle = static_cast(handlePtr); + + try { + return func(handle); + } catch (const NStatusHelpers::TYdbErrorException& ex) { + return handle->AddError(ex.GetStatus()); + } catch (const TOdbcException& ex) { + return handle->AddError(ex); + } catch (const std::exception& ex) { + return handle->AddError("HY000", 0, ex.what()); + } catch (...) { + return handle->AddError("HY000", 0, "Unknown error"); + } +} + template SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { if (!handlePtr) { diff --git a/odbc/src/utils/param_rewrite.cpp b/odbc/src/utils/param_rewrite.cpp index d40a3aedabf..315e4c96195 100644 --- a/odbc/src/utils/param_rewrite.cpp +++ b/odbc/src/utils/param_rewrite.cpp @@ -1,6 +1,7 @@ #include "param_rewrite.h" #include "sql_type_map.h" +#include #include #include #include @@ -37,6 +38,52 @@ bool TryParseDollarParam(std::string_view sql, size_t i, SQLUSMALLINT& index) { } // namespace +SQLSMALLINT CountOdbcParams(std::string_view sql) { + SQLSMALLINT questionMarkCount = 0; + SQLSMALLINT maxDollarIndex = 0; + bool inQuote = false; + size_t braceDepth = 0; + + for (size_t i = 0; i < sql.size(); ++i) { + const char ch = sql[i]; + if (inQuote) { + if (ch == '\'' && i + 1 < sql.size() && sql[i + 1] == '\'') { + ++i; + } else if (ch == '\'') { + inQuote = false; + } + continue; + } + if (ch == '\'') { + inQuote = true; + continue; + } + if (ch == '{') { + ++braceDepth; + continue; + } + if (ch == '}' && braceDepth > 0) { + --braceDepth; + continue; + } + if (braceDepth == 0) { + if (IsParamMark(sql, i)) { + ++questionMarkCount; + continue; + } + SQLUSMALLINT index = 0; + if (TryParseDollarParam(sql, i, index)) { + maxDollarIndex = std::max(maxDollarIndex, static_cast(index)); + } + } + } + + if (questionMarkCount > 0) { + return questionMarkCount; + } + return maxDollarIndex; +} + TParamRewriteResult RewriteOdbcQuestionMarks( std::string_view sql, const std::vector& boundParams) { diff --git a/odbc/src/utils/param_rewrite.h b/odbc/src/utils/param_rewrite.h index 84748ad0869..f1b41d51218 100644 --- a/odbc/src/utils/param_rewrite.h +++ b/odbc/src/utils/param_rewrite.h @@ -19,4 +19,6 @@ TParamRewriteResult RewriteOdbcQuestionMarks( std::string_view sql, const std::vector& boundParams); +SQLSMALLINT CountOdbcParams(std::string_view sql); + } // namespace NYdb::NOdbc diff --git a/odbc/src/utils/status_util.cpp b/odbc/src/utils/status_util.cpp new file mode 100644 index 00000000000..99620babd88 --- /dev/null +++ b/odbc/src/utils/status_util.cpp @@ -0,0 +1,11 @@ +#include "status_util.h" + +#include + +namespace NYdb::NOdbc { + +NYdb::TStatus StatusFrom(const NYdb::TStatus& ydbStatus) { + return NYdb::TStatus(ydbStatus.GetStatus(), NYdb::NIssue::TIssues(ydbStatus.GetIssues())); +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/status_util.h b/odbc/src/utils/status_util.h new file mode 100644 index 00000000000..43595c69ac1 --- /dev/null +++ b/odbc/src/utils/status_util.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace NYdb::NOdbc { + +NYdb::TStatus StatusFrom(const NYdb::TStatus& ydbStatus); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/type_info_rows.cpp b/odbc/src/utils/type_info_rows.cpp new file mode 100644 index 00000000000..8dd62500f9d --- /dev/null +++ b/odbc/src/utils/type_info_rows.cpp @@ -0,0 +1,77 @@ +#include "type_info_rows.h" + +#include + +#include +#include + +namespace NYdb::NOdbc { +namespace { + +struct TTypeInfoSpec { + const char* TypeName; + SQLSMALLINT DataType; + SQLULEN ColumnSize; + SQLSMALLINT Nullable; +}; + +const TTypeInfoSpec kTypeInfoSpecs[] = { + {"bigint", SQL_BIGINT, 19, SQL_NULLABLE}, + {"integer", SQL_INTEGER, 10, SQL_NULLABLE}, + {"smallint", SQL_SMALLINT, 5, SQL_NULLABLE}, + {"double", SQL_DOUBLE, 53, SQL_NULLABLE}, + {"real", SQL_REAL, 24, SQL_NULLABLE}, + {"varchar", SQL_VARCHAR, 255, SQL_NULLABLE}, + {"char", SQL_CHAR, 255, SQL_NULLABLE}, +}; + +TValue MakeOptionalInt16(SQLSMALLINT value) { + return TValueBuilder().OptionalInt16(value).Build(); +} + +TValue MakeOptionalInt32(SQLINTEGER value) { + return TValueBuilder().OptionalInt32(value).Build(); +} + +TValue MakeNullUtf8() { + return TValueBuilder().OptionalUtf8(std::nullopt).Build(); +} + +std::vector MakeTypeInfoRow(const TTypeInfoSpec& spec) { + return { + TValueBuilder().Utf8(spec.TypeName).Build(), + TValueBuilder().Int16(spec.DataType).Build(), + MakeOptionalInt32(static_cast(spec.ColumnSize)), + MakeNullUtf8(), + MakeNullUtf8(), + MakeNullUtf8(), + MakeOptionalInt16(spec.Nullable), + MakeOptionalInt16(SQL_FALSE), + MakeOptionalInt16(SQL_PRED_SEARCHABLE), + MakeNullUtf8(), + MakeOptionalInt16(SQL_FALSE), + MakeOptionalInt16(SQL_FALSE), + TValueBuilder().OptionalUtf8(spec.TypeName).Build(), + MakeOptionalInt16(0), + MakeOptionalInt16(0), + MakeOptionalInt16(spec.DataType), + MakeOptionalInt16(0), + MakeOptionalInt32(10), + MakeOptionalInt32(0), + }; +} + +} // namespace + +TTable BuildTypeInfoRows(SQLSMALLINT dataType) { + TTable table; + for (const auto& spec : kTypeInfoSpecs) { + if (dataType != SQL_ALL_TYPES && spec.DataType != dataType) { + continue; + } + table.push_back(MakeTypeInfoRow(spec)); + } + return table; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/type_info_rows.h b/odbc/src/utils/type_info_rows.h new file mode 100644 index 00000000000..23c87f6cf0e --- /dev/null +++ b/odbc/src/utils/type_info_rows.h @@ -0,0 +1,11 @@ +#pragma once + +#include "cursor.h" + +#include + +namespace NYdb::NOdbc { + +TTable BuildTypeInfoRows(SQLSMALLINT dataType); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp index 54700d06091..f3d06c81d51 100644 --- a/odbc/src/utils/util.cpp +++ b/odbc/src/utils/util.cpp @@ -1,7 +1,22 @@ #include "util.h" +#include + namespace NYdb::NOdbc { +namespace { + +void TrimInPlace(std::string& value) { + while (!value.empty() && std::isspace(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && std::isspace(static_cast(value.back()))) { + value.pop_back(); + } +} + +} // namespace + std::string GetString(SQLCHAR* str, SQLSMALLINT length) { if (!str) { return {}; @@ -15,4 +30,75 @@ std::string GetString(SQLCHAR* str, SQLSMALLINT length) { return std::string(reinterpret_cast(str), length); } +bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen) { + if (sLen < prefixLen) { + return false; + } + for (size_t i = 0; i < prefixLen; ++i) { + if (std::tolower(static_cast(s[i])) != + std::tolower(static_cast(prefix[i]))) { + return false; + } + } + return true; +} + +std::map ParseConnectionString(const std::string& connectionString) { + std::map params; + size_t pos = 0; + while (pos < connectionString.size()) { + const size_t eq = connectionString.find('=', pos); + if (eq == std::string::npos) { + break; + } + std::string key = connectionString.substr(pos, eq - pos); + TrimInPlace(key); + if (key.empty()) { + break; + } + + size_t valueStart = eq + 1; + size_t valueEnd = connectionString.size(); + if (valueStart < connectionString.size() && connectionString[valueStart] == '{') { + ++valueStart; + size_t braceDepth = 1; + size_t i = valueStart; + while (i < connectionString.size() && braceDepth > 0) { + if (connectionString[i] == '{') { + ++braceDepth; + } else if (connectionString[i] == '}') { + --braceDepth; + if (braceDepth == 0) { + valueEnd = i; + pos = i + 1; + if (pos < connectionString.size() && connectionString[pos] == ';') { + ++pos; + } + break; + } + } + ++i; + } + if (braceDepth != 0) { + valueEnd = connectionString.size(); + pos = connectionString.size(); + } + params[key] = connectionString.substr(valueStart, valueEnd - valueStart); + continue; + } + + const size_t sc = connectionString.find(';', valueStart); + if (sc != std::string::npos) { + valueEnd = sc; + pos = sc + 1; + } else { + pos = connectionString.size(); + } + std::string val = connectionString.substr(valueStart, valueEnd - valueStart); + TrimInPlace(val); + params[key] = val; + } + return params; +} + } // namespace NYdb::NOdbc diff --git a/odbc/src/utils/util.h b/odbc/src/utils/util.h index b17fe2c235f..942b20293eb 100644 --- a/odbc/src/utils/util.h +++ b/odbc/src/utils/util.h @@ -1,12 +1,19 @@ #pragma once +#include + #include #include +#include #include namespace NYdb::NOdbc { std::string GetString(SQLCHAR* str, SQLSMALLINT length); +bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen); + +std::map ParseConnectionString(const std::string& connectionString); + } // namespace NYdb::NOdbc diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 19a7004f16a..1b116da8bd0 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -32,3 +32,13 @@ add_odbc_test(NAME odbc-metadata_api_it SOURCES metadata_api_it.cpp ) + +add_odbc_test(NAME odbc-core_api_it + SOURCES + core_api_it.cpp +) + +add_odbc_test(NAME odbc-descriptor_api_it + SOURCES + descriptor_api_it.cpp +) diff --git a/odbc/tests/integration/core_api_it.cpp b/odbc/tests/integration/core_api_it.cpp new file mode 100644 index 00000000000..d66971490f7 --- /dev/null +++ b/odbc/tests/integration/core_api_it.cpp @@ -0,0 +1,393 @@ +#include "test_utils.h" + +#include + +#ifndef SQL_ODBC_INTERFACE_CONFORMANCE +#define SQL_ODBC_INTERFACE_CONFORMANCE 169 +#endif + +TEST(CoreApi, SQLGetTypeInfoAll) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLGetTypeInfo(stmt, SQL_ALL_TYPES), stmt, SQL_HANDLE_STMT); + char typeName[64] = {}; + SQLLEN indicator = 0; + SQLBindCol(stmt, 1, SQL_C_CHAR, typeName, sizeof(typeName), &indicator); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_TRUE(std::strstr(typeName, "bigint") != nullptr); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLGetTypeInfoFilter) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLGetTypeInfo(stmt, SQL_INTEGER), stmt, SQL_HANDLE_STMT); + SQLINTEGER dataType = 0; + SQLLEN indicator = 0; + SQLBindCol(stmt, 2, SQL_C_LONG, &dataType, 0, &indicator); + int rowCount = 0; + while (SQLFetch(stmt) == SQL_SUCCESS) { + ASSERT_EQ(dataType, SQL_INTEGER); + ++rowCount; + } + ASSERT_GT(rowCount, 0); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLNumParamsQuestionMarks) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT ? + ?", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLSMALLINT paramCount = 0; + CHECK_ODBC_OK(SQLNumParams(stmt, ¶mCount), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(paramCount, 2); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLNumParamsDollarParams) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT $p1", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLSMALLINT paramCount = 0; + CHECK_ODBC_OK(SQLNumParams(stmt, ¶mCount), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(paramCount, 1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLColAttributeName) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS col", SQL_NTS), stmt, SQL_HANDLE_STMT); + char name[64] = {}; + SQLSMALLINT nameLen = 0; + CHECK_ODBC_OK(SQLColAttribute(stmt, 1, SQL_DESC_NAME, name, sizeof(name), &nameLen, nullptr), + stmt, SQL_HANDLE_STMT); + EXPECT_STREQ(name, "col"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLColAttributeType) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS col", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLLEN dataType = 0; + CHECK_ODBC_OK(SQLColAttribute(stmt, 1, SQL_DESC_TYPE, nullptr, 0, nullptr, &dataType), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(dataType, SQL_INTEGER); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLNativeSqlPassthrough) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + char out[64] = {}; + SQLINTEGER outLen = 0; + CHECK_ODBC_OK(SQLNativeSql(dbc, (SQLCHAR*)"SELECT 1", SQL_NTS, (SQLCHAR*)out, sizeof(out), &outLen), + dbc, SQL_HANDLE_DBC); + EXPECT_STREQ(out, "SELECT 1"); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLSetGetCursorName) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLSetCursorName(stmt, (SQLCHAR*)"mycursor", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLCHAR name[64] = {}; + SQLSMALLINT nameLen = 0; + CHECK_ODBC_OK(SQLGetCursorName(stmt, name, sizeof(name), &nameLen), stmt, SQL_HANDLE_STMT); + EXPECT_STREQ(reinterpret_cast(name), "mycursor"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLStatisticsEmpty) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLStatistics(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)"%", SQL_NTS, SQL_INDEX_ALL, SQL_ENSURE), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLSpecialColumnsPrimaryKey) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_special_columns_pk", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_special_columns_pk (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + const char* table = "/local/test_special_columns_pk"; + CHECK_ODBC_OK(SQLSpecialColumns(stmt, SQL_BEST_ROWID, nullptr, 0, nullptr, 0, + (SQLCHAR*)table, SQL_NTS, SQL_SCOPE_SESSION, 0), + stmt, SQL_HANDLE_STMT); + char columnName[64] = {}; + SQLLEN indicator = 0; + SQLBindCol(stmt, 2, SQL_C_CHAR, columnName, sizeof(columnName), &indicator); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_STREQ(columnName, "id"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLGetInfoInterfaceConformance) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLUINTEGER conformance = 0; + SQLSMALLINT outLen = 0; + CHECK_ODBC_OK(SQLGetInfo(dbc, SQL_ODBC_INTERFACE_CONFORMANCE, &conformance, 0, &outLen), + dbc, SQL_HANDLE_DBC); + EXPECT_EQ(conformance, SQL_OIC_CORE); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLForeignKeysEmpty) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLForeignKeys(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)"%", SQL_NTS, + nullptr, 0, nullptr, 0, (SQLCHAR*)"%", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLSMALLINT colCount = 0; + CHECK_ODBC_OK(SQLNumResultCols(stmt, &colCount), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(colCount, 14); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLPrimaryKeys) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_primary_keys", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_primary_keys (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + const char* table = "/local/test_primary_keys"; + CHECK_ODBC_OK(SQLPrimaryKeys(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)table, SQL_NTS), + stmt, SQL_HANDLE_STMT); + char columnName[64] = {}; + SQLLEN indicator = 0; + SQLBindCol(stmt, 4, SQL_C_CHAR, columnName, sizeof(columnName), &indicator); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_STREQ(columnName, "id"); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLDescribeParamUnknown) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT ?", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLSMALLINT dataType = 0; + SQLULEN paramSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + CHECK_ODBC_OK(SQLDescribeParam(stmt, 1, &dataType, ¶mSize, &decimalDigits, &nullable), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(dataType, SQL_UNKNOWN_TYPE); + EXPECT_EQ(nullable, SQL_NULLABLE_UNKNOWN); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLDescribeParamBound) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"SELECT ?", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER value = 42; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &value, 0, nullptr), + stmt, SQL_HANDLE_STMT); + SQLSMALLINT dataType = 0; + SQLULEN paramSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + CHECK_ODBC_OK(SQLDescribeParam(stmt, 1, &dataType, ¶mSize, &decimalDigits, &nullable), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(dataType, SQL_INTEGER); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLParamDataPutData) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_at_exec", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_at_exec (id Int32, val Text, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"UPSERT INTO test_at_exec (id, val) VALUES (1, ?)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLLEN atExec = SQL_DATA_AT_EXEC; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 32, 0, + nullptr, 0, &atExec), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLExecute(stmt), SQL_NEED_DATA); + SQLPOINTER token = nullptr; + ASSERT_EQ(SQLParamData(stmt, &token), SQL_NEED_DATA); + const char part1[] = "hel"; + CHECK_ODBC_OK(SQLPutData(stmt, (SQLPOINTER)part1, sizeof(part1) - 1), stmt, SQL_HANDLE_STMT); + const char part2[] = "lo"; + CHECK_ODBC_OK(SQLPutData(stmt, (SQLPOINTER)part2, sizeof(part2) - 1), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLPutData(stmt, nullptr, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLParamData(stmt, &token), stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLCancel) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT * FROM AS_TABLE(ListMap(ListFromRange(1u, 1000000u), ($x)->(AsStruct($x AS v))))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLCancel(stmt), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLFreeStmtDrop) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_DROP), SQL_SUCCESS); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLParamDataPutDataNts) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_at_exec_nts", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_at_exec_nts (id Int32, val Text, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"UPSERT INTO test_at_exec_nts (id, val) VALUES (2, ?)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLLEN atExec = SQL_DATA_AT_EXEC; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 32, 0, + nullptr, 0, &atExec), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLExecute(stmt), SQL_NEED_DATA); + SQLPOINTER token = nullptr; + ASSERT_EQ(SQLParamData(stmt, &token), SQL_NEED_DATA); + const char payload[] = "nts-value"; + CHECK_ODBC_OK(SQLPutData(stmt, (SQLPOINTER)payload, SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLParamData(stmt, &token), stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(CoreApi, SQLCancelIdle) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLCancel(stmt), stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/descriptor_api_it.cpp b/odbc/tests/integration/descriptor_api_it.cpp new file mode 100644 index 00000000000..2ac4de26736 --- /dev/null +++ b/odbc/tests/integration/descriptor_api_it.cpp @@ -0,0 +1,91 @@ +#include "test_utils.h" + +TEST(DescriptorApi, ImplicitImpRowDesc) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS col", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLHDESC ird = SQL_NULL_HDESC; + CHECK_ODBC_OK(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), + stmt, SQL_HANDLE_STMT); + ASSERT_NE(ird, SQL_NULL_HDESC); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(DescriptorApi, ImpRowDescCount) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1 AS a, 2 AS b", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLHDESC ird = SQL_NULL_HDESC; + CHECK_ODBC_OK(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), + stmt, SQL_HANDLE_STMT); + SQLSMALLINT descCount = 0; + CHECK_ODBC_OK(SQLGetDescField(ird, 0, SQL_DESC_COUNT, &descCount, 0, nullptr), ird, SQL_HANDLE_DESC); + SQLSMALLINT numCols = 0; + CHECK_ODBC_OK(SQLNumResultCols(stmt, &numCols), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(descCount, numCols); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(DescriptorApi, AppRowDescBindCol) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 42 AS v", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER value = 0; + SQLLEN indicator = 0; + CHECK_ODBC_OK(SQLBindCol(stmt, 1, SQL_C_LONG, &value, 0, &indicator), stmt, SQL_HANDLE_STMT); + SQLHDESC ard = SQL_NULL_HDESC; + CHECK_ODBC_OK(SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, sizeof(ard), nullptr), + stmt, SQL_HANDLE_STMT); + SQLSMALLINT type = 0; + SQLSMALLINT subType = 0; + SQLLEN length = 0; + CHECK_ODBC_OK(SQLGetDescRec(ard, 1, nullptr, 0, nullptr, &type, &subType, &length, nullptr, nullptr, nullptr), + ard, SQL_HANDLE_DESC); + EXPECT_EQ(type, SQL_C_LONG); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(DescriptorApi, ExplicitDescAllocCopy) { + SQLHENV env; + SQLHDBC dbc; + AllocEnvAndConnect(&env, &dbc); + SQLHDESC src = SQL_NULL_HDESC; + SQLHDESC dst = SQL_NULL_HDESC; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, dbc, &src), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, dbc, &dst), SQL_SUCCESS); + SQLSMALLINT type = SQL_C_LONG; + SQLLEN length = sizeof(SQLINTEGER); + CHECK_ODBC_OK(SQLSetDescRec(src, 1, type, SQL_INTEGER, length, 0, 0, nullptr, nullptr, nullptr), + src, SQL_HANDLE_DESC); + CHECK_ODBC_OK(SQLCopyDesc(src, dst), src, SQL_HANDLE_DESC); + SQLSMALLINT outType = 0; + SQLSMALLINT outSubType = 0; + SQLLEN outLen = 0; + CHECK_ODBC_OK(SQLGetDescRec(dst, 1, nullptr, 0, nullptr, &outType, &outSubType, &outLen, nullptr, nullptr, nullptr), + dst, SQL_HANDLE_DESC); + EXPECT_EQ(outType, SQL_C_LONG); + EXPECT_EQ(outLen, length); + SQLFreeHandle(SQL_HANDLE_DESC, src); + SQLFreeHandle(SQL_HANDLE_DESC, dst); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index d39710657fd..e93bb97bb4b 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -216,9 +216,14 @@ TEST(MetadataApi, SQLColumnsMetadataId) { ASSERT_EQ(SQLColumns(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)exactTable.c_str(), SQL_NTS, (SQLCHAR*)"val%", SQL_NTS), - SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "42S22")); + SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLColumns(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)exactTable.c_str(), SQL_NTS, + (SQLCHAR*)"nonexistent_col%", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_FALSE, 0), stmt, SQL_HANDLE_STMT); SQLFreeHandle(SQL_HANDLE_STMT, stmt); @@ -226,3 +231,50 @@ TEST(MetadataApi, SQLColumnsMetadataId) { SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); } + +TEST(MetadataApi, SQLTablesFilterByType) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_type_filter", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_type_filter (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"/local/test_type_filter", SQL_NTS, + (SQLCHAR*)"VIEW", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"/local/test_type_filter", SQL_NTS, + (SQLCHAR*)"TABLE", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, DdlWithComment) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_ddl_comment", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"/* ddl */ CREATE TABLE test_ddl_comment (id Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index 869196f1237..efa1bde2ed0 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -386,6 +386,7 @@ TEST(StatementApi, RowCount) { SQLLEN rowCount; CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, -1); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); @@ -607,3 +608,76 @@ TEST(StatementApi, EscapeSequenceTimestamp) { SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); } + +TEST(StatementApi, NumericOutOfRange) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT CAST(3000000000 AS Uint64) AS v", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER value = 0; + SQLLEN indicator = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &value, sizeof(value), &indicator), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "22003")); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, UpsertAutocommitPersist) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS test_upsert_persist", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE test_upsert_persist (id Int32, val Int32, PRIMARY KEY (id))", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPSERT INTO test_upsert_persist (id, val) VALUES (1, 42)", SQL_NTS), + stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT val FROM test_upsert_persist WHERE id = 1", SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER val = 0; + SQLLEN ind = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &val, sizeof(val), &ind), SQL_SUCCESS); + ASSERT_EQ(val, 42); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, SqlCBit) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT true AS b", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + char bitVal = 0; + SQLLEN ind = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_BIT, &bitVal, sizeof(bitVal), &ind), SQL_SUCCESS); + ASSERT_EQ(bitVal, 1); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT false AS b", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_BIT, &bitVal, sizeof(bitVal), &ind), SQL_SUCCESS); + ASSERT_EQ(bitVal, 0); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/unit/CMakeLists.txt b/odbc/tests/unit/CMakeLists.txt index faeb5033333..c2c18c9bada 100644 --- a/odbc/tests/unit/CMakeLists.txt +++ b/odbc/tests/unit/CMakeLists.txt @@ -38,6 +38,19 @@ add_ydb_test(NAME odbc-param_rewrite_ut GTEST unit ) +add_ydb_test(NAME odbc-conn_string_ut GTEST + SOURCES + conn_string_ut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/utils/util.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + LINK_LIBRARIES + yutil + YDB-CPP-SDK::Params + LABELS + unit +) + add_ydb_test(NAME odbc-sql_like_ut GTEST SOURCES sql_like_ut.cpp diff --git a/odbc/tests/unit/conn_string_ut.cpp b/odbc/tests/unit/conn_string_ut.cpp new file mode 100644 index 00000000000..48bb5f7694d --- /dev/null +++ b/odbc/tests/unit/conn_string_ut.cpp @@ -0,0 +1,22 @@ +#include "utils/util.h" + +#include + +TEST(ConnString, ParsesBraceEscapedSemicolons) { + const auto params = NYdb::NOdbc::ParseConnectionString("Database={path;with;semicolons};Server=host"); + ASSERT_EQ(params.at("Database"), "path;with;semicolons"); + ASSERT_EQ(params.at("Server"), "host"); +} + +TEST(ConnString, ParsesSimplePairs) { + const auto params = NYdb::NOdbc::ParseConnectionString("DSN=YDB;Database=/local;Server=grpc://localhost:2136"); + ASSERT_EQ(params.at("DSN"), "YDB"); + ASSERT_EQ(params.at("Database"), "/local"); + ASSERT_EQ(params.at("Server"), "grpc://localhost:2136"); +} + +TEST(ConnString, TrimsWhitespace) { + const auto params = NYdb::NOdbc::ParseConnectionString(" Database = /local ; Server = host "); + ASSERT_EQ(params.at("Database"), "/local"); + ASSERT_EQ(params.at("Server"), "host"); +} diff --git a/odbc/tests/unit/param_rewrite_ut.cpp b/odbc/tests/unit/param_rewrite_ut.cpp index 14ba9d8880e..efb979a4ba9 100644 --- a/odbc/tests/unit/param_rewrite_ut.cpp +++ b/odbc/tests/unit/param_rewrite_ut.cpp @@ -4,6 +4,7 @@ #include using NYdb::NOdbc::RewriteOdbcQuestionMarks; +using NYdb::NOdbc::CountOdbcParams; using NYdb::NOdbc::TBoundParam; namespace { @@ -50,3 +51,10 @@ TEST(OdbcParamRewrite, RejectsMismatchedBindCount) { ASSERT_FALSE(result.Success); EXPECT_EQ(result.SqlState, "07002"); } + +TEST(OdbcParamRewrite, CountOdbcParams) { + EXPECT_EQ(CountOdbcParams("SELECT ? + ?"), 2); + EXPECT_EQ(CountOdbcParams("SELECT $p1"), 1); + EXPECT_EQ(CountOdbcParams("SELECT $p1 + $p2"), 2); + EXPECT_EQ(CountOdbcParams("SELECT 1"), 0); +} From bce83c2088ed8c65924fdfe88f33846bee5880bd Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Thu, 18 Jun 2026 07:53:12 +0300 Subject: [PATCH 41/55] fix the tests --- odbc/tests/integration/descriptor_api_it.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odbc/tests/integration/descriptor_api_it.cpp b/odbc/tests/integration/descriptor_api_it.cpp index 2ac4de26736..02b5c401458 100644 --- a/odbc/tests/integration/descriptor_api_it.cpp +++ b/odbc/tests/integration/descriptor_api_it.cpp @@ -10,7 +10,7 @@ TEST(DescriptorApi, ImplicitImpRowDesc) { SQLHDESC ird = SQL_NULL_HDESC; CHECK_ODBC_OK(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), stmt, SQL_HANDLE_STMT); - ASSERT_NE(ird, SQL_NULL_HDESC); + ASSERT_NE(ird, nullptr); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); From af203b82e70f7e36182d62b5af68761ebc011b55 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Tue, 14 Jul 2026 19:21:08 +0300 Subject: [PATCH 42/55] fix odbc core conformance --- cmake/testing.cmake | 18 +- odbc/README.md | 7 + odbc/examples/erlang_client/Makefile | 2 +- .../erlang_client/src/sample_data.erl | 7 +- .../erlang_client/src/ydb_series_client.erl | 70 +- odbc/src/connection.cpp | 23 +- odbc/src/connection.h | 16 +- odbc/src/connection_attr.cpp | 49 +- odbc/src/connection_attr.h | 2 + odbc/src/descriptor.cpp | 575 +++++---- odbc/src/descriptor.h | 62 +- odbc/src/environment.cpp | 11 +- odbc/src/metadata.cpp | 10 +- odbc/src/odbc_driver.cpp | 65 +- odbc/src/statement.cpp | 593 ++++++++-- odbc/src/statement.h | 47 +- odbc/src/statement_attr.cpp | 12 + odbc/src/statement_attr.h | 1 + odbc/src/statement_metadata.cpp | 20 +- odbc/src/utils/bindings.h | 17 - odbc/src/utils/convert.cpp | 740 ++++-------- odbc/src/utils/convert.h | 4 +- odbc/src/utils/cursor.cpp | 124 +- odbc/src/utils/cursor.h | 16 +- odbc/src/utils/error_manager.cpp | 14 +- odbc/src/utils/error_manager.h | 26 +- odbc/src/utils/sql_type_map.cpp | 122 +- odbc/src/utils/sql_type_map.h | 11 + odbc/src/utils/type_info_rows.cpp | 39 +- odbc/src/utils/types.cpp | 29 +- odbc/tests/CMakeLists.txt | 1 + odbc/tests/conformance/CMakeLists.txt | 8 + odbc/tests/conformance/README.md | 56 + .../tests/conformance/core_conformance_it.cpp | 1036 +++++++++++++++++ odbc/tests/integration/core_api_it.cpp | 6 +- odbc/tests/unit/convert_ut.cpp | 6 +- 36 files changed, 2475 insertions(+), 1370 deletions(-) create mode 100644 odbc/tests/conformance/CMakeLists.txt create mode 100644 odbc/tests/conformance/README.md create mode 100644 odbc/tests/conformance/core_conformance_it.cpp diff --git a/cmake/testing.cmake b/cmake/testing.cmake index 5f1df627085..a45477df1a7 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -83,16 +83,28 @@ function(add_ydb_test) list(APPEND env_vars "ENVIRONMENT") list(APPEND env_vars "${env_var}") endforeach() - gtest_discover_tests(${YDB_TEST_NAME} EXTRA_ARGS ${YDB_TEST_TEST_ARG} WORKING_DIRECTORY ${YDB_TEST_WORKING_DIRECTORY} PROPERTIES - LABELS ${YDB_TEST_LABELS} ENVIRONMENT "YDB_TEST_ROOT=sdk_tests" ${env_vars} ) + # Discovered tests only exist when CTest loads this directory. Assign + # labels from a second include so a semicolon-separated label list stays a + # single property value rather than becoming extra property/value pairs. + if (YDB_TEST_LABELS) + set(test_labels_file + "${CMAKE_CURRENT_BINARY_DIR}/${YDB_TEST_NAME}_labels.cmake") + string(CONCAT test_labels_content + "if(DEFINED ${YDB_TEST_NAME}_TESTS)\n" + " set_tests_properties(\${${YDB_TEST_NAME}_TESTS} PROPERTIES LABELS \"${YDB_TEST_LABELS}\")\n" + "endif()\n") + file(GENERATE OUTPUT "${test_labels_file}" CONTENT "${test_labels_content}") + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES "${test_labels_file}") + endif() + target_link_libraries(${YDB_TEST_NAME} PRIVATE GTest::gtest_main GTest::gmock_main @@ -113,7 +125,7 @@ function(add_ydb_test) cpp-testing-unittest_main ) - set_tests_properties(${YDB_TEST_NAME} PROPERTIES LABELS ${YDB_TEST_LABELS}) + set_tests_properties(${YDB_TEST_NAME} PROPERTIES LABELS "${YDB_TEST_LABELS}") set_tests_properties(${YDB_TEST_NAME} PROPERTIES ENVIRONMENT "YDB_TEST_ROOT=sdk_tests") if (YDB_TEST_ENV) set_tests_properties(${YDB_TEST_NAME} PROPERTIES ENVIRONMENT ${YDB_TEST_ENV}) diff --git a/odbc/README.md b/odbc/README.md index 13ad517fd6d..626eec86ccb 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -102,6 +102,13 @@ SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT `?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` from `SQLBindParameter` types. YDB-native `$pN` syntax also works. +## Core conformance tests + +The Linux/headless ODBC 3.x Core contract audit and the open-source test-suite +evaluation are documented in [tests/conformance](tests/conformance/README.md). +With a local YDB server running, build `odbc-core-conformance_it` and run +`ctest --test-dir build -L core-conformance --output-on-failure`. + ## License Apache License 2.0 diff --git a/odbc/examples/erlang_client/Makefile b/odbc/examples/erlang_client/Makefile index 5f402ef9720..24bafca0f8a 100644 --- a/odbc/examples/erlang_client/Makefile +++ b/odbc/examples/erlang_client/Makefile @@ -31,7 +31,7 @@ compile: prepare run: compile @echo "Running YDB Series Example..." - $(ERL) -pa ebin -noshell -eval 'application:load(odbc), application:start(odbc), ydb_series_client:run("$(CONN)"), halt().' + $(ERL) -pa ebin -noshell -eval 'case ydb_series_client:run("$(CONN)") of ok -> halt(0); _ -> halt(1) end.' run-shell: compile @echo "Starting Erlang shell with ydb_series_client..." diff --git a/odbc/examples/erlang_client/src/sample_data.erl b/odbc/examples/erlang_client/src/sample_data.erl index 5fccdce72ee..f4f0ef654ce 100644 --- a/odbc/examples/erlang_client/src/sample_data.erl +++ b/odbc/examples/erlang_client/src/sample_data.erl @@ -1,5 +1,5 @@ -module(sample_data). --export([series/0, seasons/0, episodes/0, format_date/1]). +-export([series/0, seasons/0, episodes/0]). series() -> [ @@ -57,8 +57,3 @@ episodes() -> days_from_date({Year, Month, Day}) -> calendar:date_to_gregorian_days(Year, Month, Day) - calendar:date_to_gregorian_days(1970, 1, 1). - -format_date(Days) -> - Date = calendar:gregorian_days_to_date(Days + calendar:date_to_gregorian_days(1970, 1, 1)), - {Year, Month, Day} = Date, - io_lib:format("~4..0B-~2..0B-~2..0B", [Year, Month, Day]). diff --git a/odbc/examples/erlang_client/src/ydb_series_client.erl b/odbc/examples/erlang_client/src/ydb_series_client.erl index 7b52b6687b5..ba3fd1cf71d 100644 --- a/odbc/examples/erlang_client/src/ydb_series_client.erl +++ b/odbc/examples/erlang_client/src/ydb_series_client.erl @@ -11,7 +11,7 @@ run(ConnectionString) when is_list(ConnectionString) -> application:load(odbc), application:start(odbc), - case odbc:connect(ConnectionString, [{tuple_format, list}]) of + case odbc:connect(ConnectionString, []) of {ok, Ref} -> Result = run_example(Ref), odbc:disconnect(Ref), @@ -82,38 +82,31 @@ fill_table_data(Ref) -> SeasonsData = sample_data:seasons(), EpisodesData = sample_data:episodes(), - lists:foreach(fun(Row) -> - [Id, Title, Info, Date] = Row, - Query = io_lib:format( - "UPSERT INTO series (series_id, title, series_info, release_date) VALUES (~p, \"~s\", \"~s\", ~p);", - [Id, escape_string(Title), escape_string(Info), Date] - ), - execute_update(Ref, Query) - end, SeriesData), - - lists:foreach(fun(Row) -> - [SeriesId, SeasonId, Title, FirstAired, LastAired] = Row, - Query = io_lib:format( - "UPSERT INTO seasons (series_id, season_id, title, first_aired, last_aired) VALUES (~p, ~p, \"~s\", ~p, ~p);", - [SeriesId, SeasonId, escape_string(Title), FirstAired, LastAired] - ), - execute_update(Ref, Query) - end, SeasonsData), - - lists:foreach(fun(Row) -> - [SeriesId, SeasonId, EpisodeId, Title, AirDate] = Row, - Query = io_lib:format( - "UPSERT INTO episodes (series_id, season_id, episode_id, title, air_date) VALUES (~p, ~p, ~p, \"~s\", ~p);", - [SeriesId, SeasonId, EpisodeId, escape_string(Title), AirDate] - ), - execute_update(Ref, Query) - end, EpisodesData), + batch_update(Ref, + "UPSERT INTO series (series_id, title, series_info, release_date) " + "VALUES (CAST(? AS Uint64), ?, ?, CAST(? AS Uint64))", + [{sql_integer, column(1, SeriesData)}, + {{sql_varchar, 64}, column(2, SeriesData)}, + {{sql_varchar, 256}, column(3, SeriesData)}, + {sql_integer, column(4, SeriesData)}]), + batch_update(Ref, + "UPSERT INTO seasons (series_id, season_id, title, first_aired, last_aired) " + "VALUES (CAST(? AS Uint64), CAST(? AS Uint64), ?, CAST(? AS Uint64), CAST(? AS Uint64))", + [{sql_integer, column(1, SeasonsData)}, {sql_integer, column(2, SeasonsData)}, + {{sql_varchar, 64}, column(3, SeasonsData)}, {sql_integer, column(4, SeasonsData)}, + {sql_integer, column(5, SeasonsData)}]), + batch_update(Ref, + "UPSERT INTO episodes (series_id, season_id, episode_id, title, air_date) " + "VALUES (CAST(? AS Uint64), CAST(? AS Uint64), CAST(? AS Uint64), ?, CAST(? AS Uint64))", + [{sql_integer, column(1, EpisodesData)}, {sql_integer, column(2, EpisodesData)}, + {sql_integer, column(3, EpisodesData)}, {{sql_varchar, 128}, column(4, EpisodesData)}, + {sql_integer, column(5, EpisodesData)}]), io:format("Inserted ~p series, ~p seasons, ~p episodes~n", [length(SeriesData), length(SeasonsData), length(EpisodesData)]). select_simple(Ref) -> - Query = "SELECT CAST(series_id AS Utf8) AS series_id, title, CAST(release_date AS Date) AS release_date FROM series WHERE series_id = 1;", + Query = "SELECT series_id, title, CAST(release_date AS Date) AS release_date FROM series WHERE series_id = 1;", Rows = selected_rows(Ref, select_simple, Query), lists:foreach(fun(Row) -> [Id, Title, ReleaseDate] = row_values(Row), @@ -131,7 +124,7 @@ select_with_params(Ref) -> Query = "SELECT sa.title AS season_title, sr.title AS series_title " "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id " - "WHERE sa.series_id = CAST($p1 AS Uint64) AND sa.season_id = CAST($p2 AS Uint64);", + "WHERE sa.series_id = CAST(? AS Uint64) AND sa.season_id = CAST(? AS Uint64);", Params = [{sql_integer, [SeriesId]}, {sql_integer, [SeasonId]}], Rows = selected_param_rows(Ref, select_with_params, Query, Params), @@ -145,7 +138,7 @@ multistep(Ref) -> SeasonId = 5, Query1 = io_lib:format( - "SELECT CAST(first_aired AS Utf8) AS first_aired FROM seasons WHERE series_id = ~p AND season_id = ~p;", + "SELECT first_aired FROM seasons WHERE series_id = ~p AND season_id = ~p;", [SeriesId, SeasonId] ), @@ -156,7 +149,7 @@ multistep(Ref) -> ToDate = FromDate + 15, Query2 = io_lib:format( - "SELECT CAST(season_id AS Utf8) AS season_id, CAST(episode_id AS Utf8) AS episode_id, title, CAST(air_date AS Utf8) AS air_date FROM episodes " + "SELECT season_id, episode_id, title, air_date FROM episodes " "WHERE series_id = ~p AND air_date >= ~p AND air_date <= ~p;", [SeriesId, FromDate, ToDate] ), @@ -172,7 +165,7 @@ select_seasons_by_series(Ref) -> InClause = string:join([integer_to_list(X) || X <- SeriesList], ", "), Query = io_lib:format( - "SELECT CAST(series_id AS Utf8) AS series_id, CAST(season_id AS Utf8) AS season_id, title, CAST(first_aired AS Date) AS first_aired " + "SELECT series_id, season_id, title, CAST(first_aired AS Date) AS first_aired " "FROM seasons WHERE series_id IN (~s) ORDER BY season_id;", [InClause] ), @@ -195,16 +188,21 @@ drop_tables(Ref) -> end end, Tables). -escape_string(String) -> - EscapedBackslash = string:replace(String, "\\", "\\\\", all), - lists:flatten(string:replace(EscapedBackslash, "\"", "\\\"", all)). - execute_update(Ref, Query) -> case odbc:sql_query(Ref, lists:flatten(Query)) of {updated, _} -> ok; Error -> throw({query_failed, update, Error}) end. +batch_update(Ref, Query, Params) -> + case odbc:param_query(Ref, Query, Params) of + {updated, _} -> ok; + Error -> throw({query_failed, batch_update, Error}) + end. + +column(Number, Rows) -> + [lists:nth(Number, Row) || Row <- Rows]. + selected_rows(Ref, Step, Query) -> case odbc:sql_query(Ref, lists:flatten(Query)) of {selected, _, Rows} -> Rows; diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 3a39385923e..fca372daaa1 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -15,8 +15,7 @@ #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { TConnection::~TConnection() { DestroyYdbState(); @@ -113,14 +112,19 @@ std::unique_ptr TConnection::CreateStatement() { return std::make_unique(this); } +void TConnection::CloseStatementCursors() { + for (TStatement* stmt : Statements_) { + stmt->Close(true); + } +} + SQLRETURN TConnection::SetAutocommit(bool value) { - Attributes_.SetAutocommit(value); - if (Attributes_.GetAutocommit() && Tx_) { + if (value && Tx_) { auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); } - return SQL_SUCCESS; + return Attributes_.SetAutocommit(value); } bool TConnection::GetAutocommit() const { @@ -171,21 +175,23 @@ void TConnection::ResetQuerySession() { SQLRETURN TConnection::CommitTx() { if (!Tx_) { - return AddError("25000", 0, "Invalid transaction state: no active transaction"); + return SQL_SUCCESS; } auto status = Tx_->Commit().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); + CloseStatementCursors(); return SQL_SUCCESS; } SQLRETURN TConnection::RollbackTx() { if (!Tx_) { - return AddError("25000", 0, "Invalid transaction state: no active transaction"); + return SQL_SUCCESS; } auto status = Tx_->Rollback().ExtractValueSync(); NStatusHelpers::ThrowOnError(status); Tx_.reset(); + CloseStatementCursors(); return SQL_SUCCESS; } @@ -304,5 +310,4 @@ SQLRETURN TConnection::NativeSql(const std::string& inSql, SQLCHAR* outSql, SQLI return SQL_SUCCESS; } -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/connection.h b/odbc/src/connection.h index c39cdb2e0a5..ae921ecd1af 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -16,11 +16,12 @@ #include #include #include +#include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { class TStatement; +class TDescriptor; class TConnection : public TErrorManager { private: @@ -54,6 +55,8 @@ class TConnection : public TErrorManager { TConnectionAttributes Attributes_; mutable std::optional DbmsVersionCache_; + std::unordered_set Statements_; + std::unordered_set Descriptors_; void DestroyYdbState(); void RecreateYdbClients(); @@ -69,6 +72,12 @@ class TConnection : public TErrorManager { SQLRETURN Disconnect(); std::unique_ptr CreateStatement(); + void RegisterStatement(TStatement* stmt) { Statements_.insert(stmt); } + void UnregisterStatement(TStatement* stmt) { Statements_.erase(stmt); } + void RegisterDescriptor(TDescriptor* desc) { Descriptors_.insert(desc); } + void UnregisterDescriptor(TDescriptor* desc) { Descriptors_.erase(desc); } + bool HasChildren() const noexcept { return !Statements_.empty() || !Descriptors_.empty(); } + void CloseStatementCursors(); std::optional GetClient(); NQuery::TSession& GetOrCreateQuerySession(); @@ -102,5 +111,4 @@ class TConnection : public TErrorManager { SQLRETURN NativeSql(const std::string& inSql, SQLCHAR* outSql, SQLINTEGER outMax, SQLINTEGER* outLen); }; -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/connection_attr.cpp b/odbc/src/connection_attr.cpp index 576f093b656..c1e350d5e39 100644 --- a/odbc/src/connection_attr.cpp +++ b/odbc/src/connection_attr.cpp @@ -120,6 +120,18 @@ SQLRETURN TConnectionAttributes::SetConnectAttr( return SetTxnIsolation(value, errors); case SQL_ATTR_CURRENT_CATALOG: return SetCurrentCatalog(value, stringLength, errors); + case SQL_ATTR_QUIET_MODE: + QuietMode_ = value; + return SQL_SUCCESS; + case SQL_ATTR_TRANSLATE_LIB: + if (!value) { + return Diag::AddNullPointer(errors); + } + return errors.AddError( + "HYC00", 0, "Translation libraries are not supported"); + case SQL_ATTR_TRANSLATE_OPTION: + TranslateOption_ = ReadIntegerAttr(value); + return SQL_SUCCESS; default: return Diag::AddNotImplemented(errors); } @@ -131,7 +143,9 @@ SQLRETURN TConnectionAttributes::GetConnectAttr( SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr, TErrorManager& errors) const { - if (!value) { + const bool stringAttribute = + attr == SQL_ATTR_CURRENT_CATALOG || attr == SQL_ATTR_TRANSLATE_LIB; + if (!value && (!stringAttribute || !stringLengthPtr)) { return Diag::AddNullPointer(errors); } if (stringLengthPtr) { @@ -146,6 +160,26 @@ SQLRETURN TConnectionAttributes::GetConnectAttr( return GetTxnIsolation(value); case SQL_ATTR_CURRENT_CATALOG: return GetCurrentCatalog(value, bufferLength, stringLengthPtr, errors); + case SQL_ATTR_QUIET_MODE: + if (!QuietMode_) { + return SQL_NO_DATA; + } + *reinterpret_cast(value) = *QuietMode_; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLPOINTER); + } + return SQL_SUCCESS; + case SQL_ATTR_TRANSLATE_LIB: + return SQL_NO_DATA; + case SQL_ATTR_TRANSLATE_OPTION: + if (!TranslateOption_) { + return SQL_NO_DATA; + } + *reinterpret_cast(value) = *TranslateOption_; + if (stringLengthPtr) { + *stringLengthPtr = sizeof(SQLUINTEGER); + } + return SQL_SUCCESS; default: return Diag::AddNotImplemented(errors); } @@ -172,16 +206,16 @@ SQLRETURN TConnectionAttributes::SetAccessMode(SQLPOINTER value, TErrorManager& if (!mode) { return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_ACCESS_MODE"); } - AccessMode_ = *mode; - auto txMode = Tx::ResolveTxMode(AccessMode_, TxnIsolation_); + auto txMode = Tx::ResolveTxMode(*mode, TxnIsolation_); if (!txMode) { return errors.AddError( "HYC00", 0, - AccessMode_ == SQL_MODE_READ_WRITE + *mode == SQL_MODE_READ_WRITE ? "Transaction isolation is not supported for read-write mode" : "Transaction isolation is not supported for read-only mode"); } + AccessMode_ = *mode; TxMode_ = *txMode; return SQL_SUCCESS; } @@ -204,11 +238,12 @@ SQLRETURN TConnectionAttributes::SetCurrentCatalog(SQLPOINTER value, SQLINTEGER if (!value) { return Diag::AddNullPointer(errors); } - CurrentCatalog_ = ReadAttributeString(value, stringLength); - Catalog::NormalizePath(CurrentCatalog_); - if (CurrentCatalog_.empty()) { + std::string catalog = ReadAttributeString(value, stringLength); + Catalog::NormalizePath(catalog); + if (catalog.empty()) { return Diag::AddInvalidAttrValue(errors, "SQL_ATTR_CURRENT_CATALOG"); } + CurrentCatalog_ = std::move(catalog); return SQL_SUCCESS; } diff --git a/odbc/src/connection_attr.h b/odbc/src/connection_attr.h index cd530554f2f..8f6a0614af4 100644 --- a/odbc/src/connection_attr.h +++ b/odbc/src/connection_attr.h @@ -79,6 +79,8 @@ class TConnectionAttributes { bool Autocommit_ = true; std::string CurrentCatalog_; + std::optional QuietMode_; + std::optional TranslateOption_; SQLUINTEGER AccessMode_ = SQL_MODE_READ_WRITE; SQLUINTEGER TxnIsolation_ = SQL_TXN_SERIALIZABLE; NQuery::TTxSettings::ETransactionMode TxMode_ = NQuery::TTxSettings::TS_SERIALIZABLE_RW; diff --git a/odbc/src/descriptor.cpp b/odbc/src/descriptor.cpp index 4586524ab9b..111fb5ff0b2 100644 --- a/odbc/src/descriptor.cpp +++ b/odbc/src/descriptor.cpp @@ -1,16 +1,78 @@ #include "descriptor.h" + #include "statement.h" -#include "utils/param_rewrite.h" +#include "utils/diag.h" +#include "utils/sql_type_map.h" #include #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { +namespace { + +bool IsCharacter(SQLSMALLINT type) { + return type == SQL_CHAR || type == SQL_VARCHAR || type == SQL_LONGVARCHAR + || type == SQL_WCHAR || type == SQL_WVARCHAR || type == SQL_WLONGVARCHAR; +} + +SQLSMALLINT DateTimeCode(SQLSMALLINT type) { + switch (type) { + case SQL_TYPE_DATE: return SQL_CODE_DATE; + case SQL_TYPE_TIME: return SQL_CODE_TIME; + case SQL_TYPE_TIMESTAMP: return SQL_CODE_TIMESTAMP; + default: return 0; + } +} + +std::string TypeName(SQLSMALLINT type) { + const TSqlTypeSpec* spec = FindSqlTypeSpec(type); + return spec ? std::string(spec->Name) : type == SQL_GUID ? "GUID" : ""; +} + +SQLRETURN WriteString(TErrorManager& errors, const std::string& text, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* lengthPtr) { + if (lengthPtr) { + *lengthPtr = static_cast(text.size()); + } + if (!value) { + return SQL_SUCCESS; + } + if (bufferLength < 0) { + return Diag::AddInvalidBufferLength(errors); + } + if (bufferLength == 0) { + return text.empty() ? SQL_SUCCESS : Diag::AddRightTruncated(errors); + } + const auto copyLength = std::min(text.size(), static_cast(bufferLength - 1)); + std::memcpy(value, text.data(), copyLength); + static_cast(value)[copyLength] = '\0'; + return copyLength == text.size() ? SQL_SUCCESS : Diag::AddRightTruncated(errors); +} + +template +SQLRETURN WriteScalar(SQLPOINTER value, T scalar) { + *static_cast(value) = scalar; + return SQL_SUCCESS; +} + +} // namespace -TDescriptor::TDescriptor(EDescType type, TStatement* stmt) +TDescriptor::TDescriptor(EDescType type, TConnection* conn) : Type_(type) - , Stmt_(stmt) {} + , Conn_(conn) { + if (Type_ == EDescType::Explicit) { + Conn_->RegisterDescriptor(this); + } +} + +TDescriptor::~TDescriptor() { + while (!Statements_.empty()) { + Statements_.back()->DetachDescriptor(this); + } + if (Type_ == EDescType::Explicit) { + Conn_->UnregisterDescriptor(this); + } +} TDescriptor* TDescriptor::FromHandle(SQLHDESC handle) { if (!handle) { @@ -19,352 +81,239 @@ TDescriptor* TDescriptor::FromHandle(SQLHDESC handle) { return static_cast(handle); } -SQLSMALLINT TDescriptor::GetRecordCount() const { - if (Type_ == EDescType::Explicit) { - return static_cast(ExplicitRecs_.size()); - } - if (!Stmt_) { - return 0; +TDescRecord& TDescriptor::Record(SQLSMALLINT number) { + if (number < 1) { + throw TOdbcException("07009", 0, "Invalid descriptor index"); } - switch (Type_) { - case EDescType::ImpRow: { - const auto& cols = Stmt_->GetColumnMeta(); - return static_cast(cols.size()); - } - case EDescType::AppRow: { - SQLSMALLINT maxRec = 0; - for (const auto& col : Stmt_->BoundColumns_) { - maxRec = std::max(maxRec, static_cast(col.ColumnNumber)); - } - return maxRec; - } - case EDescType::ImpParam: - return Stmt_->IsPrepared_ ? Stmt_->GetParamCount() : 0; - case EDescType::AppParam: { - SQLSMALLINT maxRec = 0; - for (const auto& param : Stmt_->BoundParams_) { - maxRec = std::max(maxRec, static_cast(param.ParamNumber)); - } - return maxRec; - } - default: - return 0; + if (Records_.size() < static_cast(number)) { + Records_.resize(static_cast(number)); } + TDescRecord& record = Records_[static_cast(number - 1)]; + record.Active = true; + return record; } -bool TDescriptor::GetExplicitRecord(SQLSMALLINT recNumber, TExplicitDescRec& out) const { - if (recNumber < 1 || static_cast(recNumber) > ExplicitRecs_.size()) { - return false; +const TDescRecord* TDescriptor::FindRecord(SQLSMALLINT number) const noexcept { + if (number < 1 || static_cast(number) > Records_.size()) { + return nullptr; } - out = ExplicitRecs_[static_cast(recNumber - 1)]; - return true; + const TDescRecord& record = Records_[static_cast(number - 1)]; + return record.Active ? &record : nullptr; } -TExplicitDescRec& TDescriptor::GetOrCreateExplicitRecord(SQLSMALLINT recNumber) { - if (recNumber < 1) { - throw TOdbcException("07009", 0, "Invalid descriptor index"); +TDescRecord* TDescriptor::FindRecord(SQLSMALLINT number) noexcept { + return const_cast(std::as_const(*this).FindRecord(number)); +} + +void TDescriptor::RemoveRecord(SQLSMALLINT number) { + if (number > 0 && static_cast(number) <= Records_.size()) { + Records_[static_cast(number - 1)] = {}; + while (!Records_.empty() && !Records_.back().Active) { + Records_.pop_back(); + } } - const size_t idx = static_cast(recNumber - 1); - if (ExplicitRecs_.size() <= idx) { - ExplicitRecs_.resize(idx + 1); +} + +SQLSMALLINT TDescriptor::GetRecordCount() const noexcept { + return static_cast(Records_.size()); +} + +void TDescriptor::Attach(TStatement* stmt) { + if (Type_ == EDescType::Explicit + && std::find(Statements_.begin(), Statements_.end(), stmt) == Statements_.end()) { + Statements_.push_back(stmt); } - return ExplicitRecs_[idx]; } -SQLRETURN TDescriptor::GetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, - SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr) { - if (!value && fieldIdentifier != SQL_DESC_ROWS_PROCESSED_PTR) { - return AddError("HY009", 0, "Invalid use of null pointer"); +void TDescriptor::Detach(TStatement* stmt) { + std::erase(Statements_, stmt); +} + +SQLRETURN TDescriptor::GetDescField(SQLSMALLINT recNumber, SQLSMALLINT field, SQLPOINTER value, + SQLINTEGER bufferLength, SQLINTEGER* lengthPtr) { + const bool stringField = field == SQL_DESC_BASE_COLUMN_NAME || field == SQL_DESC_NAME + || field == SQL_DESC_TYPE_NAME || field == SQL_DESC_LOCAL_TYPE_NAME + || field == SQL_DESC_LITERAL_PREFIX || field == SQL_DESC_LITERAL_SUFFIX; + if (!value && (!stringField || !lengthPtr)) { + return Diag::AddNullPointer(*this); } - switch (fieldIdentifier) { + switch (field) { case SQL_DESC_ALLOC_TYPE: - *reinterpret_cast(value) = - Type_ == EDescType::Explicit ? SQL_DESC_ALLOC_USER : SQL_DESC_ALLOC_AUTO; - return SQL_SUCCESS; - case SQL_DESC_COUNT: - *reinterpret_cast(value) = GetRecordCount(); - return SQL_SUCCESS; - case SQL_DESC_ARRAY_SIZE: - *reinterpret_cast(value) = 1; - return SQL_SUCCESS; + return WriteScalar(value, static_cast( + Type_ == EDescType::Explicit ? SQL_DESC_ALLOC_USER : SQL_DESC_ALLOC_AUTO)); + case SQL_DESC_COUNT: return WriteScalar(value, GetRecordCount()); + case SQL_DESC_ARRAY_SIZE: return WriteScalar(value, ArraySize_); + case SQL_DESC_BIND_TYPE: return WriteScalar(value, BindType_); + case SQL_DESC_BIND_OFFSET_PTR: return WriteScalar(value, BindOffsetPtr_); + case SQL_DESC_ARRAY_STATUS_PTR: return WriteScalar(value, ArrayStatusPtr_); + case SQL_DESC_ROWS_PROCESSED_PTR: return WriteScalar(value, RowsProcessedPtr_); + default: break; + } + + const TDescRecord* record = FindRecord(recNumber); + if (!record) { + return recNumber > GetRecordCount() + ? SQL_NO_DATA + : AddError("07009", 0, "Invalid descriptor index"); + } + switch (field) { + case SQL_DESC_BASE_COLUMN_NAME: + case SQL_DESC_NAME: + return WriteString(*this, record->Name, value, bufferLength, lengthPtr); + case SQL_DESC_TYPE_NAME: + case SQL_DESC_LOCAL_TYPE_NAME: + return WriteString(*this, TypeName(record->Type), value, bufferLength, lengthPtr); + case SQL_DESC_LITERAL_PREFIX: + case SQL_DESC_LITERAL_SUFFIX: + return WriteString( + *this, IsCharacter(record->Type) || DateTimeCode(record->Type) ? "'" : "", + value, bufferLength, lengthPtr); case SQL_DESC_TYPE: - case SQL_DESC_CONCISE_TYPE: - case SQL_DESC_LENGTH: - case SQL_DESC_PRECISION: - case SQL_DESC_SCALE: - case SQL_DESC_NULLABLE: - case SQL_DESC_NAME: { - SQLCHAR name[256] = {}; - SQLSMALLINT nameLen = 0; - SQLSMALLINT type = 0; - SQLSMALLINT subType = 0; - SQLLEN length = 0; - SQLSMALLINT precision = 0; - SQLSMALLINT scale = 0; - SQLSMALLINT nullable = 0; - const SQLRETURN rc = GetDescRec(recNumber, name, sizeof(name), &nameLen, &type, &subType, - &length, &precision, &scale, &nullable); - if (rc != SQL_SUCCESS) { - return rc; - } - if (fieldIdentifier == SQL_DESC_NAME) { - if (stringLengthPtr) { - *stringLengthPtr = nameLen; - } - if (bufferLength > 0) { - const SQLINTEGER copyLen = std::min(nameLen, bufferLength - 1); - std::memcpy(value, name, static_cast(copyLen)); - reinterpret_cast(value)[copyLen] = '\0'; - } - return SQL_SUCCESS; - } - if (fieldIdentifier == SQL_DESC_TYPE || fieldIdentifier == SQL_DESC_CONCISE_TYPE) { - *reinterpret_cast(value) = type; - } else if (fieldIdentifier == SQL_DESC_LENGTH) { - *reinterpret_cast(value) = length; - } else if (fieldIdentifier == SQL_DESC_PRECISION) { - *reinterpret_cast(value) = precision; - } else if (fieldIdentifier == SQL_DESC_SCALE) { - *reinterpret_cast(value) = scale; - } else if (fieldIdentifier == SQL_DESC_NULLABLE) { - *reinterpret_cast(value) = nullable; - } - return SQL_SUCCESS; - } - default: - return AddError("HYC00", 0, "Optional feature not implemented"); + return WriteScalar(value, static_cast( + DateTimeCode(record->Type) ? SQL_DATETIME : record->Type)); + case SQL_DESC_CONCISE_TYPE: return WriteScalar(value, record->Type); + case SQL_DESC_DATETIME_INTERVAL_CODE: + return WriteScalar(value, DateTimeCode(record->Type)); + case SQL_DESC_LENGTH: return WriteScalar(value, record->Length); + case SQL_DESC_OCTET_LENGTH: return WriteScalar(value, record->OctetLength); + case SQL_DESC_DISPLAY_SIZE: return WriteScalar(value, record->Length); + case SQL_DESC_PRECISION: return WriteScalar(value, record->Precision); + case SQL_DESC_SCALE: return WriteScalar(value, record->Scale); + case SQL_DESC_NULLABLE: return WriteScalar(value, record->Nullable); + case SQL_DESC_CASE_SENSITIVE: + return WriteScalar(value, static_cast(IsCharacter(record->Type))); + case SQL_DESC_FIXED_PREC_SCALE: + return WriteScalar(value, static_cast( + record->Type == SQL_DECIMAL || record->Type == SQL_NUMERIC)); + case SQL_DESC_SEARCHABLE: + return WriteScalar(value, static_cast(SQL_SEARCHABLE)); + case SQL_DESC_UNNAMED: + return WriteScalar(value, static_cast( + record->Name.empty() ? SQL_UNNAMED : SQL_NAMED)); + case SQL_DESC_UNSIGNED: return WriteScalar(value, static_cast(SQL_FALSE)); + case SQL_DESC_UPDATABLE: + return WriteScalar(value, static_cast(SQL_ATTR_READONLY)); + case SQL_DESC_PARAMETER_TYPE: return WriteScalar(value, record->ParameterType); + case SQL_DESC_DATA_PTR: return WriteScalar(value, record->DataPtr); + case SQL_DESC_INDICATOR_PTR: return WriteScalar(value, record->IndicatorPtr); + case SQL_DESC_OCTET_LENGTH_PTR: return WriteScalar(value, record->OctetLengthPtr); + default: return Diag::AddNotImplemented(*this); } } SQLRETURN TDescriptor::GetDescRec(SQLSMALLINT recNumber, SQLCHAR* name, SQLSMALLINT bufferLength, - SQLSMALLINT* stringLengthPtr, SQLSMALLINT* typePtr, SQLSMALLINT* subTypePtr, - SQLLEN* lengthPtr, SQLSMALLINT* precisionPtr, SQLSMALLINT* scalePtr, + SQLSMALLINT* nameLengthPtr, SQLSMALLINT* typePtr, + SQLSMALLINT* subTypePtr, SQLLEN* lengthPtr, + SQLSMALLINT* precisionPtr, SQLSMALLINT* scalePtr, SQLSMALLINT* nullablePtr) { - if (recNumber < 1) { - return AddError("07009", 0, "Invalid descriptor index"); + const TDescRecord* record = FindRecord(recNumber); + if (!record) { + return recNumber > GetRecordCount() + ? SQL_NO_DATA + : AddError("07009", 0, "Invalid descriptor index"); } - if (Type_ == EDescType::Explicit) { - TExplicitDescRec rec; - if (!GetExplicitRecord(recNumber, rec)) { - return AddError("07009", 0, "Invalid descriptor index"); - } - if (stringLengthPtr) { - *stringLengthPtr = static_cast(rec.Name.size()); - } - if (name && bufferLength > 0) { - const auto copyLen = std::min(rec.Name.size(), static_cast(bufferLength - 1)); - std::memcpy(name, rec.Name.data(), copyLen); - name[copyLen] = '\0'; - } - if (typePtr) { - *typePtr = rec.Type; - } - if (subTypePtr) { - *subTypePtr = rec.SubType; - } - if (lengthPtr) { - *lengthPtr = rec.Length; - } - if (precisionPtr) { - *precisionPtr = rec.Precision; - } - if (scalePtr) { - *scalePtr = rec.Scale; - } - if (nullablePtr) { - *nullablePtr = rec.Nullable; - } - return SQL_SUCCESS; - } - if (!Stmt_) { - return AddError("HY000", 0, "Invalid descriptor"); - } - if (Type_ == EDescType::ImpRow) { - const auto& cols = Stmt_->GetColumnMeta(); - if (static_cast(recNumber) > cols.size()) { - return AddError("07009", 0, "Invalid descriptor index"); - } - const auto& col = cols[static_cast(recNumber - 1)]; - if (stringLengthPtr) { - *stringLengthPtr = static_cast(col.Name.size()); - } - if (name && bufferLength > 0) { - const auto copyLen = std::min(col.Name.size(), static_cast(bufferLength - 1)); - std::memcpy(name, col.Name.data(), copyLen); - name[copyLen] = '\0'; - } - if (typePtr) { - *typePtr = col.SqlType; - } - if (subTypePtr) { - *subTypePtr = 0; - } - if (lengthPtr) { - *lengthPtr = static_cast(col.Size); - } - if (precisionPtr) { - *precisionPtr = static_cast(col.Size); - } - if (scalePtr) { - *scalePtr = col.DecimalDigits; - } - if (nullablePtr) { - *nullablePtr = col.Nullable; - } - return SQL_SUCCESS; + SQLINTEGER nameLength = 0; + SQLRETURN result = SQL_SUCCESS; + if (name) { + result = WriteString(*this, record->Name, name, bufferLength, &nameLength); + } else { + nameLength = static_cast(record->Name.size()); } - if (Type_ == EDescType::AppRow) { - const auto it = std::find_if(Stmt_->BoundColumns_.begin(), Stmt_->BoundColumns_.end(), - [recNumber](const TBoundColumn& col) { return col.ColumnNumber == static_cast(recNumber); }); - if (it == Stmt_->BoundColumns_.end()) { - return AddError("07009", 0, "Invalid descriptor index"); - } - if (stringLengthPtr) { - *stringLengthPtr = 0; - } - if (typePtr) { - *typePtr = it->TargetType; - } - if (subTypePtr) { - *subTypePtr = 0; - } - if (lengthPtr) { - *lengthPtr = it->BufferLength; - } - if (precisionPtr) { - *precisionPtr = 0; - } - if (scalePtr) { - *scalePtr = 0; - } - if (nullablePtr) { - *nullablePtr = SQL_NULLABLE; - } - return SQL_SUCCESS; - } - if (Type_ == EDescType::AppParam || Type_ == EDescType::ImpParam) { - const auto it = std::find_if(Stmt_->BoundParams_.begin(), Stmt_->BoundParams_.end(), - [recNumber](const TBoundParam& p) { return p.ParamNumber == static_cast(recNumber); }); - if (it != Stmt_->BoundParams_.end()) { - if (stringLengthPtr) { - *stringLengthPtr = 0; - } - if (typePtr) { - *typePtr = Type_ == EDescType::AppParam ? it->ValueType : it->ParameterType; - } - if (subTypePtr) { - *subTypePtr = 0; - } - if (lengthPtr) { - *lengthPtr = static_cast(it->ColumnSize); - } - if (precisionPtr) { - *precisionPtr = static_cast(it->ColumnSize); - } - if (scalePtr) { - *scalePtr = it->DecimalDigits; - } - if (nullablePtr) { - *nullablePtr = SQL_NULLABLE; - } + if (nameLengthPtr) *nameLengthPtr = static_cast(nameLength); + if (typePtr) *typePtr = DateTimeCode(record->Type) ? SQL_DATETIME : record->Type; + if (subTypePtr) *subTypePtr = DateTimeCode(record->Type); + if (lengthPtr) *lengthPtr = record->Length; + if (precisionPtr) *precisionPtr = record->Precision; + if (scalePtr) *scalePtr = record->Scale; + if (nullablePtr) *nullablePtr = record->Nullable; + return result; +} + +SQLRETURN TDescriptor::SetDescField(SQLSMALLINT recNumber, SQLSMALLINT field, SQLPOINTER value, + SQLINTEGER bufferLength) { + switch (field) { + case SQL_DESC_COUNT: { + const auto count = static_cast(reinterpret_cast(value)); + if (count < 0) return AddError("HY024", 0, "Invalid SQL_DESC_COUNT value"); + Records_.resize(static_cast(count)); + for (auto& record : Records_) record.Active = true; return SQL_SUCCESS; } - if (Type_ == EDescType::ImpParam && Stmt_->IsPrepared_ - && recNumber <= CountOdbcParams(Stmt_->PreparedQuery_)) { - if (stringLengthPtr) { - *stringLengthPtr = 0; - } - if (typePtr) { - *typePtr = SQL_UNKNOWN_TYPE; - } - if (subTypePtr) { - *subTypePtr = 0; - } - if (lengthPtr) { - *lengthPtr = 0; - } - if (precisionPtr) { - *precisionPtr = 0; - } - if (scalePtr) { - *scalePtr = 0; - } - if (nullablePtr) { - *nullablePtr = SQL_NULLABLE_UNKNOWN; - } + case SQL_DESC_ARRAY_SIZE: { + const auto size = static_cast(reinterpret_cast(value)); + if (size == 0) return AddError("HY024", 0, "Invalid SQL_DESC_ARRAY_SIZE value"); + ArraySize_ = size; return SQL_SUCCESS; } - return AddError("07009", 0, "Invalid descriptor index"); + case SQL_DESC_BIND_TYPE: + BindType_ = static_cast(reinterpret_cast(value)); + return SQL_SUCCESS; + case SQL_DESC_BIND_OFFSET_PTR: BindOffsetPtr_ = static_cast(value); return SQL_SUCCESS; + case SQL_DESC_ARRAY_STATUS_PTR: ArrayStatusPtr_ = static_cast(value); return SQL_SUCCESS; + case SQL_DESC_ROWS_PROCESSED_PTR: RowsProcessedPtr_ = static_cast(value); return SQL_SUCCESS; + default: break; } - return AddError("HYC00", 0, "Optional feature not implemented"); -} -SQLRETURN TDescriptor::SetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, - SQLINTEGER bufferLength) { - (void)bufferLength; - if (Type_ != EDescType::Explicit) { - return AddError("HY017", 0, "Invalid use of an automatically allocated descriptor handle"); + if (Type_ == EDescType::ImpRow) { + return AddError("HY016", 0, "Cannot modify an implementation row descriptor"); } - auto& rec = GetOrCreateExplicitRecord(recNumber); - switch (fieldIdentifier) { + + TDescRecord& record = Record(recNumber); + const auto integer = reinterpret_cast(value); + switch (field) { case SQL_DESC_TYPE: - case SQL_DESC_CONCISE_TYPE: - rec.Type = *reinterpret_cast(value); + case SQL_DESC_CONCISE_TYPE: record.Type = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_LENGTH: record.Length = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_OCTET_LENGTH: record.OctetLength = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_PRECISION: record.Precision = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_SCALE: record.Scale = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_NULLABLE: record.Nullable = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_PARAMETER_TYPE: record.ParameterType = static_cast(integer); return SQL_SUCCESS; + case SQL_DESC_DATA_PTR: record.DataPtr = value; return SQL_SUCCESS; + case SQL_DESC_INDICATOR_PTR: record.IndicatorPtr = static_cast(value); return SQL_SUCCESS; + case SQL_DESC_OCTET_LENGTH_PTR: record.OctetLengthPtr = static_cast(value); return SQL_SUCCESS; + case SQL_DESC_NAME: + if (!value) return Diag::AddNullPointer(*this); + record.Name = bufferLength == SQL_NTS + ? std::string(static_cast(value)) + : std::string(static_cast(value), static_cast(bufferLength)); return SQL_SUCCESS; - case SQL_DESC_LENGTH: - rec.Length = *reinterpret_cast(value); - return SQL_SUCCESS; - case SQL_DESC_PRECISION: - rec.Precision = *reinterpret_cast(value); - return SQL_SUCCESS; - case SQL_DESC_SCALE: - rec.Scale = *reinterpret_cast(value); - return SQL_SUCCESS; - case SQL_DESC_NULLABLE: - rec.Nullable = *reinterpret_cast(value); - return SQL_SUCCESS; - default: - return AddError("HYC00", 0, "Optional feature not implemented"); + default: return Diag::AddNotImplemented(*this); } } -SQLRETURN TDescriptor::SetDescRec(SQLSMALLINT recNumber, SQLSMALLINT type, SQLSMALLINT subType, SQLLEN length, - SQLSMALLINT precision, SQLSMALLINT scale, SQLPOINTER dataPtr, - SQLLEN* stringLengthPtr, SQLLEN* indicatorPtr) { - if (Type_ == EDescType::Explicit) { - auto& rec = GetOrCreateExplicitRecord(recNumber); - rec.Type = type; - rec.SubType = subType; - rec.Length = length; - rec.Precision = precision; - rec.Scale = scale; - rec.DataPtr = dataPtr; - rec.Indicator = indicatorPtr ? *indicatorPtr : 0; - return SQL_SUCCESS; - } - if (!Stmt_) { - return AddError("HY000", 0, "Invalid descriptor"); - } - if (Type_ == EDescType::AppRow) { - return Stmt_->BindCol(static_cast(recNumber), type, dataPtr, length, indicatorPtr); - } - if (Type_ == EDescType::AppParam) { - return Stmt_->BindParameter(static_cast(recNumber), SQL_PARAM_INPUT, type, subType, - static_cast(length), scale, dataPtr, length, indicatorPtr); +SQLRETURN TDescriptor::SetDescRec(SQLSMALLINT recNumber, SQLSMALLINT type, SQLSMALLINT subType, + SQLLEN length, SQLSMALLINT precision, SQLSMALLINT scale, + SQLPOINTER dataPtr, SQLLEN* stringLengthPtr, + SQLLEN* indicatorPtr) { + if (Type_ == EDescType::ImpRow) { + return AddError("HY016", 0, "Cannot modify an implementation row descriptor"); } - return AddError("HY017", 0, "Invalid use of an automatically allocated descriptor handle"); + TDescRecord& record = Record(recNumber); + record.Type = type; + record.SubType = subType; + record.Length = length; + record.OctetLength = length; + record.Precision = precision; + record.Scale = scale; + record.DataPtr = dataPtr; + record.OctetLengthPtr = stringLengthPtr; + record.IndicatorPtr = indicatorPtr; + return SQL_SUCCESS; } SQLRETURN TDescriptor::CopyDesc(TDescriptor* target) { - if (!target) { - return AddError("HY009", 0, "Invalid use of null pointer"); - } - if (Type_ != EDescType::Explicit || target->Type_ != EDescType::Explicit) { - return AddError("HYC00", 0, "Optional feature not implemented"); + if (!target) return Diag::AddNullPointer(*this); + if (target->Type_ == EDescType::ImpRow) { + return AddError("HY016", 0, "Cannot modify an implementation row descriptor"); } - target->ExplicitRecs_ = ExplicitRecs_; + target->ArraySize_ = ArraySize_; + target->BindType_ = BindType_; + target->BindOffsetPtr_ = BindOffsetPtr_; + target->ArrayStatusPtr_ = ArrayStatusPtr_; + target->RowsProcessedPtr_ = RowsProcessedPtr_; + target->Records_ = Records_; return SQL_SUCCESS; } -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/descriptor.h b/odbc/src/descriptor.h index 7e1abe3d822..3125ac5b80d 100644 --- a/odbc/src/descriptor.h +++ b/odbc/src/descriptor.h @@ -8,9 +8,9 @@ #include #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { +class TConnection; class TStatement; enum class EDescType { @@ -21,25 +21,54 @@ enum class EDescType { Explicit, }; -struct TExplicitDescRec { +struct TDescRecord { std::string Name; - SQLSMALLINT Type = 0; + SQLSMALLINT Type = SQL_UNKNOWN_TYPE; SQLSMALLINT SubType = 0; SQLLEN Length = 0; + SQLLEN OctetLength = 0; SQLSMALLINT Precision = 0; SQLSMALLINT Scale = 0; SQLSMALLINT Nullable = SQL_NULLABLE; SQLPOINTER DataPtr = nullptr; - SQLLEN Indicator = 0; - SQLLEN OctetLength = 0; + SQLLEN* IndicatorPtr = nullptr; + SQLLEN* OctetLengthPtr = nullptr; + SQLSMALLINT ParameterType = SQL_PARAM_INPUT; + bool Active = false; + bool AtExec = false; + bool AtExecComplete = false; + SQLLEN AtExecIndicator = 0; + std::string AtExecChunk; }; class TDescriptor : public TErrorManager { public: - TDescriptor(EDescType type, TStatement* stmt = nullptr); + TDescriptor(EDescType type, TConnection* conn); + ~TDescriptor(); EDescType GetDescType() const noexcept { return Type_; } - TStatement* GetStatement() const noexcept { return Stmt_; } + TConnection* GetConnection() const noexcept { return Conn_; } + + SQLULEN GetArraySize() const noexcept { return ArraySize_; } + SQLULEN GetBindType() const noexcept { return BindType_; } + SQLULEN* GetBindOffsetPtr() const noexcept { return BindOffsetPtr_; } + SQLUSMALLINT* GetArrayStatusPtr() const noexcept { return ArrayStatusPtr_; } + SQLULEN* GetRowsProcessedPtr() const noexcept { return RowsProcessedPtr_; } + void SetArraySize(SQLULEN value) noexcept { ArraySize_ = value; } + void SetBindType(SQLULEN value) noexcept { BindType_ = value; } + void SetBindOffsetPtr(SQLULEN* value) noexcept { BindOffsetPtr_ = value; } + void SetArrayStatusPtr(SQLUSMALLINT* value) noexcept { ArrayStatusPtr_ = value; } + void SetRowsProcessedPtr(SQLULEN* value) noexcept { RowsProcessedPtr_ = value; } + + TDescRecord& Record(SQLSMALLINT number); + const TDescRecord* FindRecord(SQLSMALLINT number) const noexcept; + TDescRecord* FindRecord(SQLSMALLINT number) noexcept; + void RemoveRecord(SQLSMALLINT number); + void ClearRecords() noexcept { Records_.clear(); } + SQLSMALLINT GetRecordCount() const noexcept; + + void Attach(TStatement* stmt); + void Detach(TStatement* stmt); SQLRETURN GetDescField(SQLSMALLINT recNumber, SQLSMALLINT fieldIdentifier, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLengthPtr); @@ -57,14 +86,15 @@ class TDescriptor : public TErrorManager { static TDescriptor* FromHandle(SQLHDESC handle); private: - SQLSMALLINT GetRecordCount() const; - bool GetExplicitRecord(SQLSMALLINT recNumber, TExplicitDescRec& out) const; - TExplicitDescRec& GetOrCreateExplicitRecord(SQLSMALLINT recNumber); - EDescType Type_; - TStatement* Stmt_; - std::vector ExplicitRecs_; + TConnection* Conn_; + SQLULEN ArraySize_ = 1; + SQLULEN BindType_ = SQL_BIND_BY_COLUMN; + SQLULEN* BindOffsetPtr_ = nullptr; + SQLUSMALLINT* ArrayStatusPtr_ = nullptr; + SQLULEN* RowsProcessedPtr_ = nullptr; + std::vector Records_; + std::vector Statements_; }; -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/environment.cpp b/odbc/src/environment.cpp index 5947823df44..047ebd1fb7c 100644 --- a/odbc/src/environment.cpp +++ b/odbc/src/environment.cpp @@ -99,18 +99,17 @@ SQLRETURN TEnvironment::EndTran(SQLSMALLINT completionType){ } catch (const std::exception& ex) { hasFailures = true; ++failedCount; - AddError("HY000", 0, ex.what(), SQL_SUCCESS_WITH_INFO); + AddError("HY000", 0, ex.what()); } catch (...) { hasFailures = true; ++failedCount; - AddError("HY000", 0, "Unknown error during ENV-level transaction completion", SQL_SUCCESS_WITH_INFO); + AddError("HY000", 0, "Unknown error during ENV-level transaction completion"); } } if (hasFailures) { - AddError("01000", 0, - "SQLEndTran(SQL_HANDLE_ENV): " + std::to_string(failedCount) + " connection(s) failed", - SQL_SUCCESS_WITH_INFO); - return SQL_SUCCESS_WITH_INFO; + AddError("HY000", 0, + "SQLEndTran(SQL_HANDLE_ENV): " + std::to_string(failedCount) + " connection(s) failed"); + return SQL_ERROR; } return SQL_SUCCESS; } diff --git a/odbc/src/metadata.cpp b/odbc/src/metadata.cpp index 2a648da1248..67f2be4ec61 100644 --- a/odbc/src/metadata.cpp +++ b/odbc/src/metadata.cpp @@ -158,9 +158,9 @@ SQLRETURN NMetadata::GetInfo( // Identifier Handling case SQL_IDENTIFIER_QUOTE_CHAR: - return WriteInfoString(conn, "\"", infoValuePtr, bufferLength, stringLengthPtr); + return WriteInfoString(conn, "`", infoValuePtr, bufferLength, stringLengthPtr); case SQL_IDENTIFIER_CASE: - return WriteInfoScalar(conn, SQL_IC_LOWER, infoValuePtr, stringLengthPtr); + return WriteInfoScalar(conn, SQL_IC_SENSITIVE, infoValuePtr, stringLengthPtr); // Catalog Support case SQL_CATALOG_NAME: @@ -198,7 +198,7 @@ SQLRETURN NMetadata::GetInfo( // Transaction Support case SQL_TXN_CAPABLE: - return WriteInfoScalar(conn, SQL_TC_ALL, infoValuePtr, stringLengthPtr); + return WriteInfoScalar(conn, SQL_TC_DML, infoValuePtr, stringLengthPtr); case SQL_DEFAULT_TXN_ISOLATION: return WriteInfoScalar(conn, SQL_TXN_SERIALIZABLE, infoValuePtr, stringLengthPtr); case SQL_TXN_ISOLATION_OPTION: @@ -221,6 +221,10 @@ SQLRETURN NMetadata::GetInfo( return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); case SQL_BATCH_ROW_COUNT: return WriteInfoScalar(conn, 0, infoValuePtr, stringLengthPtr); + case SQL_PARAM_ARRAY_ROW_COUNTS: + return WriteInfoScalar(conn, SQL_PARC_NO_BATCH, infoValuePtr, stringLengthPtr); + case SQL_PARAM_ARRAY_SELECTS: + return WriteInfoScalar(conn, SQL_PAS_NO_SELECT, infoValuePtr, stringLengthPtr); // Bookmarks (not supported) case SQL_BOOKMARK_PERSISTENCE: diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index e609d7b7130..75f26c2bfc4 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -64,15 +64,15 @@ SQLRETURN SQL_API SQLAllocHandle(SQLSMALLINT handleType, }); } case SQL_HANDLE_DESC: { - return NYdb::NOdbc::HandleOdbcExceptions( + return NYdb::NOdbc::HandleOdbcExceptions( inputHandle, - [&]() { - auto* const desc = new NYdb::NOdbc::TDescriptor(NYdb::NOdbc::EDescType::Explicit); + [&](auto* conn) { + auto* const desc = new NYdb::NOdbc::TDescriptor( + NYdb::NOdbc::EDescType::Explicit, conn); *outputHandle = desc; desc->SetLastReturnCode(SQL_SUCCESS); return SQL_SUCCESS; - }, - NYdb::NOdbc::ENullInputHandlePolicy::Allow); + }); } default: return SQL_ERROR; @@ -83,18 +83,24 @@ SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { switch (handleType) { case SQL_HANDLE_ENV: { return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* env) { + if (!env->GetConnectionsSnapshot().empty()) { + return env->AddError("HY010", 0, "Connection handles are still allocated"); + } delete env; - return SQL_SUCCESS; + return static_cast(SQL_SUCCESS); }); } case SQL_HANDLE_DBC: { return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* conn) { + if (conn->HasChildren()) { + return conn->AddError("HY010", 0, "Statement or descriptor handles are still allocated"); + } auto* env = conn->GetEnvironment(); if (env != nullptr){ env->UnregisterConnection(conn); } delete conn; - return SQL_SUCCESS; + return static_cast(SQL_SUCCESS); }); } case SQL_HANDLE_STMT: { @@ -105,8 +111,12 @@ SQLRETURN SQL_API SQLFreeHandle(SQLSMALLINT handleType, SQLHANDLE handle) { } case SQL_HANDLE_DESC: { return NYdb::NOdbc::HandleOdbcExceptionsConsuming(handle, [](auto* desc) { + if (desc->GetDescType() != NYdb::NOdbc::EDescType::Explicit) { + return desc->AddError( + "HY017", 0, "Invalid use of an automatically allocated descriptor handle"); + } delete desc; - return SQL_SUCCESS; + return static_cast(SQL_SUCCESS); }); } default: @@ -236,20 +246,25 @@ SQLRETURN SQL_API SQLGetDiagRec(SQLSMALLINT handleType, SQLSMALLINT* textLength) { switch (handleType) { case SQL_HANDLE_ENV: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* env) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* env) { return env->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); }); } case SQL_HANDLE_DBC: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* conn) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* conn) { return conn->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); }); } case SQL_HANDLE_STMT: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* stmt) { return stmt->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); }); } + case SQL_HANDLE_DESC: { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* desc) { + return desc->GetDiagRec(recNumber, sqlState, nativeError, messageText, bufferLength, textLength); + }); + } default: return SQL_ERROR; } @@ -264,20 +279,25 @@ SQLRETURN SQL_API SQLGetDiagField(SQLSMALLINT handleType, SQLSMALLINT* stringLengthPtr) { switch (handleType) { case SQL_HANDLE_ENV: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* env) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* env) { return env->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); }); } case SQL_HANDLE_DBC: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* conn) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* conn) { return conn->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); }); } case SQL_HANDLE_STMT: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* stmt) { return stmt->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); }); } + case SQL_HANDLE_DESC: { + return NYdb::NOdbc::HandleOdbcDiagnostics(handle, [&](auto* desc) { + return desc->GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); + }); + } default: return SQL_ERROR; } @@ -307,20 +327,7 @@ SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLI } else if (completionType == SQL_ROLLBACK) { return conn->RollbackTx(); } else { - throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid completion type"); - } - }); - } - case SQL_HANDLE_STMT: { - return NYdb::NOdbc::HandleOdbcExceptions(handle, [&](auto* stmt) -> SQLRETURN { - auto conn = stmt->GetConnection(); - if (!conn) return SQL_INVALID_HANDLE; - if (completionType == SQL_COMMIT) { - return conn->CommitTx(); - } else if (completionType == SQL_ROLLBACK) { - return conn->RollbackTx(); - } else { - throw NYdb::NOdbc::TOdbcException("HY000", 0, "Invalid completion type"); + throw NYdb::NOdbc::TOdbcException("HY012", 0, "Invalid transaction operation code"); } }); } @@ -330,7 +337,7 @@ SQLRETURN SQL_API SQLEndTran(SQLSMALLINT handleType, SQLHANDLE handle, SQLSMALLI }); } default: - return SQL_ERROR; + return SQL_INVALID_HANDLE; } } diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 399bbddbb91..8fbb821ffbf 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -1,6 +1,7 @@ #include "statement.h" #include "utils/convert.h" +#include "utils/attr.h" #include "utils/types.h" #include "utils/diag.h" #include "utils/escape.h" @@ -22,12 +23,55 @@ #include #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { namespace { - bool IsDdlQuery(const std::string& queryText) { + size_t CTypeSize(SQLSMALLINT type, SQLLEN bufferLength) { + switch (type) { + case SQL_C_CHAR: case SQL_C_BINARY: return std::max(bufferLength, 0); + case SQL_C_BIT: case SQL_C_TINYINT: case SQL_C_UTINYINT: return sizeof(SQLCHAR); + case SQL_C_SHORT: case SQL_C_USHORT: return sizeof(SQLSMALLINT); + 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(SQLREAL); + case SQL_C_DOUBLE: return sizeof(SQLDOUBLE); + case SQL_C_TYPE_DATE: return sizeof(SQL_DATE_STRUCT); + case SQL_C_TYPE_TIME: return sizeof(SQL_TIME_STRUCT); + case SQL_C_TYPE_TIMESTAMP: return sizeof(SQL_TIMESTAMP_STRUCT); + case SQL_C_GUID: return sizeof(SQLGUID); + default: + return static_cast(std::max(bufferLength, 0)); + } + } + + template + T* OffsetPointer(T* pointer, SQLULEN offset, SQLULEN row, SQLULEN stride) { + if (!pointer) { + return nullptr; + } + auto* bytes = reinterpret_cast(pointer); + return reinterpret_cast(bytes + offset + row * stride); + } + + TBoundParam ParamAt(const TBoundParam& param, SQLULEN row, SQLULEN bindType, SQLULEN offset) { + TBoundParam adjusted = param; + const SQLULEN dataStride = bindType == SQL_PARAM_BIND_BY_COLUMN + ? CTypeSize(param.ValueType, param.BufferLength) + : bindType; + const SQLULEN indicatorStride = bindType == SQL_PARAM_BIND_BY_COLUMN + ? sizeof(SQLLEN) + : bindType; + adjusted.ParameterValuePtr = OffsetPointer( + static_cast(param.ParameterValuePtr), offset, row, dataStride); + adjusted.StrLenOrIndPtr = OffsetPointer( + param.StrLenOrIndPtr, offset, row, indicatorStride); + return adjusted; + } + + bool StartsWithStatement( + const std::string& queryText, + std::initializer_list keywords) { size_t i = 0; while (i < queryText.size()) { if (std::isspace(static_cast(queryText[i]))) { @@ -50,31 +94,58 @@ namespace { break; } } - const char* start = queryText.c_str() + i; const size_t remaining = queryText.size() - i; - return StartsWithPrefix(start, remaining, "CREATE", 6) || - StartsWithPrefix(start, remaining, "DROP", 4) || - StartsWithPrefix(start, remaining, "ALTER", 5) || - StartsWithPrefix(start, remaining, "GRANT", 5) || - StartsWithPrefix(start, remaining, "REVOKE", 6); + for (const std::string_view keyword : keywords) { + if (StartsWithPrefix( + queryText.c_str() + i, remaining, keyword.data(), keyword.size())) { + return true; + } + } + return false; } } TStatement::TStatement(TConnection* conn) : Conn_(conn) - , AppRowDesc_(std::make_unique(EDescType::AppRow, this)) - , AppParamDesc_(std::make_unique(EDescType::AppParam, this)) - , ImpRowDesc_(std::make_unique(EDescType::ImpRow, this)) - , ImpParamDesc_(std::make_unique(EDescType::ImpParam, this)) {} + , AppRowDesc_(EDescType::AppRow, conn) + , AppParamDesc_(EDescType::AppParam, conn) + , ImpRowDesc_(EDescType::ImpRow, conn) + , ImpParamDesc_(EDescType::ImpParam, conn) + , CurrentAppRowDesc_(&AppRowDesc_) + , CurrentAppParamDesc_(&AppParamDesc_) { + Conn_->RegisterStatement(this); +} + +TStatement::~TStatement() { + CurrentAppRowDesc_->Detach(this); + CurrentAppParamDesc_->Detach(this); + Conn_->UnregisterStatement(this); +} + +void TStatement::DetachDescriptor(TDescriptor* desc) { + if (CurrentAppRowDesc_ == desc) { + CurrentAppRowDesc_ = &AppRowDesc_; + } + if (CurrentAppParamDesc_ == desc) { + CurrentAppParamDesc_ = &AppParamDesc_; + } + desc->Detach(this); +} SQLRETURN TStatement::Prepare(const std::string& statementText) { - StreamFetchError_ = false; RowsFetched_ = 0; - Cursor_.reset(); + SetCursor(nullptr); PreparedQuery_ = statementText; IsPrepared_ = true; ParamCount_ = CountOdbcParams(PreparedQuery_); + while (ImpParamDesc_.GetRecordCount() > ParamCount_) { + ImpParamDesc_.RemoveRecord(ImpParamDesc_.GetRecordCount()); + } + for (SQLSMALLINT i = 1; i <= ParamCount_; ++i) { + TDescRecord& record = ImpParamDesc_.Record(i); + record.Nullable = SQL_NULLABLE_UNKNOWN; + } return SQL_SUCCESS; } @@ -82,10 +153,18 @@ SQLRETURN TStatement::Execute() { if (!IsPrepared_ || PreparedQuery_.empty()) { throw TOdbcException("HY007", 0, "No prepared statement"); } + if (ParamCount_ > 0 && CurrentAppParamDesc_->GetArraySize() > 1 + && !StartsWithStatement(PreparedQuery_, {"INSERT", "UPDATE", "DELETE", "UPSERT", "REPLACE"})) { + return AddError("HYC00", 0, "Parameter arrays are supported only for data-modification statements"); + } const SQLUSMALLINT next = FindNextNeedDataParam(); if (next != 0) { + if (CurrentAppParamDesc_->GetArraySize() > 1) { + return AddError("HYC00", 0, "Data-at-execution parameter arrays are not supported"); + } NeedDataParam_ = next; InAtExec_ = true; + NeedDataTokenDelivered_ = false; return SQL_NEED_DATA; } InAtExec_ = false; @@ -94,15 +173,62 @@ SQLRETURN TStatement::Execute() { } SQLRETURN TStatement::ExecuteInternal() { - StreamFetchError_ = false; + const SQLULEN paramsetSize = ParamCount_ > 0 ? CurrentAppParamDesc_->GetArraySize() : 1; + SQLUSMALLINT* const operations = CurrentAppParamDesc_->GetArrayStatusPtr(); + SQLUSMALLINT* const statuses = ImpParamDesc_.GetArrayStatusPtr(); + SQLULEN* const processed = ImpParamDesc_.GetRowsProcessedPtr(); + if (processed) { + *processed = 0; + } + if (statuses) { + std::fill_n(statuses, paramsetSize, SQL_PARAM_UNUSED); + } + + SQLRETURN result = SQL_SUCCESS; + for (SQLULEN paramSet = 0; paramSet < paramsetSize; ++paramSet) { + if (operations && operations[paramSet] == SQL_PARAM_IGNORE) { + if (processed) { + *processed = paramSet + 1; + } + continue; + } + if (operations && operations[paramSet] != SQL_PARAM_PROCEED) { + if (statuses) { + statuses[paramSet] = SQL_PARAM_ERROR; + } + if (processed) { + *processed = paramSet + 1; + } + return AddError("HY024", 0, "Invalid parameter operation value"); + } + const SQLRETURN rc = ExecuteParamSet(paramSet); + if (statuses) { + statuses[paramSet] = rc == SQL_SUCCESS_WITH_INFO + ? SQL_PARAM_SUCCESS_WITH_INFO + : rc == SQL_SUCCESS ? SQL_PARAM_SUCCESS : SQL_PARAM_ERROR; + } + if (processed) { + *processed = paramSet + 1; + } + if (rc == SQL_ERROR) { + return SQL_ERROR; + } + if (rc == SQL_SUCCESS_WITH_INFO) { + result = SQL_SUCCESS_WITH_INFO; + } + } + return result; +} + +SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { RowsFetched_ = 0; - Cursor_.reset(); + SetCursor(nullptr); auto client = Conn_->GetClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } NYdb::TParams params = NYdb::TParamsBuilder().Build(); - const SQLRETURN buildRc = BuildParams(params); + const SQLRETURN buildRc = BuildParams(params, paramSet); if (buildRc != SQL_SUCCESS) { return buildRc; } @@ -114,15 +240,11 @@ SQLRETURN TStatement::ExecuteInternal() { const NYdb::TStatus execStatus = client->RetryQuerySync( [this, ¶ms](NQuery::TSession session) -> NYdb::TStatus { - auto retryIterator = CreateExecuteIterator(session, params); - if (!retryIterator.IsSuccess()) { - return StatusFrom(retryIterator); + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); + if (!result.IsSuccess()) { + return StatusFrom(result); } - TExecCursorCreateResult created = TryCreateExecCursor(this, std::move(retryIterator)); - if (!created.Status.IsSuccess()) { - return created.Status; - } - Cursor_ = std::move(created.Cursor); + SetCursor(CreateExecCursor(result)); return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }, retrySettings); @@ -130,22 +252,28 @@ SQLRETURN TStatement::ExecuteInternal() { NStatusHelpers::ThrowOnError(execStatus); } else { NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); - auto iterator = CreateExecuteIterator(session, params); - NStatusHelpers::ThrowOnError(iterator); - TExecCursorCreateResult created = TryCreateExecCursor(this, std::move(iterator)); - NStatusHelpers::ThrowOnError(created.Status); - Cursor_ = std::move(created.Cursor); + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); + NStatusHelpers::ThrowOnError(result); + SetCursor(CreateExecCursor(result)); } - RowCount_ = Cursor_ ? -1 : 0; + RowCount_ = -1; InAtExec_ = false; NeedDataParam_ = 0; + NeedDataTokenDelivered_ = false; + for (SQLSMALLINT i = 1; i <= CurrentAppParamDesc_->GetRecordCount(); ++i) { + if (TDescRecord* param = CurrentAppParamDesc_->FindRecord(i); param && param->AtExec) { + param->AtExecComplete = false; + param->AtExecChunk.clear(); + } + } return SQL_SUCCESS; } SQLUSMALLINT TStatement::FindNextNeedDataParam() const { - for (const auto& param : BoundParams_) { - if (param.AtExec && !param.AtExecComplete) { - return param.ParamNumber; + for (SQLSMALLINT i = 1; i <= CurrentAppParamDesc_->GetRecordCount(); ++i) { + const TDescRecord* record = CurrentAppParamDesc_->FindRecord(i); + if (record && record->AtExec && !record->AtExecComplete) { + return static_cast(i); } } return 0; @@ -153,7 +281,6 @@ SQLUSMALLINT TStatement::FindNextNeedDataParam() const { NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() { NYdb::NRetry::TRetryOperationSettings settings; - settings.Idempotent(true); SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { const TDuration deadline = TDuration::Seconds(queryTimeoutSec); @@ -162,15 +289,17 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() return settings; } -NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params) { +NQuery::TExecuteQueryResult TStatement::ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params) { const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ : RewriteOdbcEscapes(PreparedQuery_); - const TParamRewriteResult rewritten = RewriteOdbcQuestionMarks(sqlAfterEscapes, BoundParams_); + const std::vector activeParams = GetBoundParams(0); + const TParamRewriteResult rewritten = RewriteOdbcQuestionMarks(sqlAfterEscapes, activeParams); if (!rewritten.Success) { throw TOdbcException(rewritten.SqlState, 0, rewritten.Message); } - const bool isDdl = IsDdlQuery(rewritten.Sql); + const bool isDdl = StartsWithStatement( + rewritten.Sql, {"CREATE", "DROP", "ALTER", "GRANT", "REVOKE"}); const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); @@ -184,13 +313,13 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession const bool isSnapshotRw = (txSettings.GetMode() == NQuery::TTxSettings::TS_SNAPSHOT_RW); if (isSnapshotRw || isDdl) { - return session.StreamExecuteQuery( + return session.ExecuteQuery( queryText, NQuery::TTxControl::NoTx(), params, execSettings).ExtractValueSync(); } - return session.StreamExecuteQuery( + return session.ExecuteQuery( queryText, NQuery::TTxControl::BeginTx(txSettings).CommitTx(), params, @@ -201,7 +330,7 @@ NQuery::TExecuteQueryIterator TStatement::CreateExecuteIterator(NQuery::TSession NStatusHelpers::ThrowOnError(beginTxResult); Conn_->SetTx(beginTxResult.GetTransaction()); } - return session.StreamExecuteQuery( + return session.ExecuteQuery( queryText, NQuery::TTxControl::Tx(*Conn_->GetTx()).CommitTx(false), params, @@ -218,21 +347,45 @@ SQLRETURN TStatement::Fetch() { if (maxRows > 0 && RowsFetched_ >= maxRows) { return SQL_NO_DATA; } - StreamFetchError_ = false; - if (!Cursor_->Fetch()) { - return StreamFetchError_ ? SQL_ERROR : SQL_NO_DATA; + const SQLULEN rowArraySize = CurrentAppRowDesc_->GetArraySize(); + SQLUSMALLINT* const statuses = ImpRowDesc_.GetArrayStatusPtr(); + SQLULEN* const fetched = ImpRowDesc_.GetRowsProcessedPtr(); + if (fetched) { + *fetched = 0; } - ++RowsFetched_; - if (LastFetchRc_ != SQL_SUCCESS) { - return LastFetchRc_; + if (statuses) { + std::fill_n(statuses, rowArraySize, SQL_ROW_NOROW); } - return GetLastReturnCode() == SQL_SUCCESS_WITH_INFO ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; -} -void TStatement::OnStreamPartError(const TStatus& status) { - ClearErrors(); - AddError(status); - StreamFetchError_ = true; + SQLULEN rows = 0; + SQLRETURN result = SQL_SUCCESS; + for (; rows < rowArraySize; ++rows) { + if (maxRows > 0 && RowsFetched_ >= maxRows) { + break; + } + BindingRow_ = rows; + if (!Cursor_->Fetch()) { + break; + } + FillBoundColumns(); + ++RowsFetched_; + GetDataOffsets_.assign(Cursor_->GetColumnMeta().size(), 0); + if (fetched) { + *fetched = rows + 1; + } + if (statuses) { + statuses[rows] = LastFetchRc_ == SQL_SUCCESS_WITH_INFO + ? SQL_ROW_SUCCESS_WITH_INFO + : LastFetchRc_ == SQL_SUCCESS ? SQL_ROW_SUCCESS : SQL_ROW_ERROR; + } + if (LastFetchRc_ == SQL_ERROR) { + result = SQL_ERROR; + } else if (LastFetchRc_ == SQL_SUCCESS_WITH_INFO && result == SQL_SUCCESS) { + result = SQL_SUCCESS_WITH_INFO; + } + } + BindingRow_ = 0; + return rows == 0 && result != SQL_ERROR ? SQL_NO_DATA : result; } SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, @@ -240,7 +393,12 @@ SQLRETURN TStatement::GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, if (!Cursor_) { return SQL_NO_DATA; } - const SQLRETURN rc = Cursor_->GetData(columnNumber, targetType, targetValue, bufferLength, strLenOrInd); + if (columnNumber < 1 || columnNumber > GetDataOffsets_.size()) { + return AddError("07009", 0, "Invalid descriptor index"); + } + const SQLRETURN rc = Cursor_->GetData( + columnNumber, targetType, targetValue, bufferLength, strLenOrInd, + &GetDataOffsets_[columnNumber - 1]); if (const char* sqlState = ConsumeLastConvertSqlState()) { AddError(sqlState, 0, std::strcmp(sqlState, "22003") == 0 ? "Numeric value out of range" : "Conversion error"); } @@ -252,8 +410,49 @@ void TStatement::FillBoundColumns() { return; } LastFetchRc_ = SQL_SUCCESS; - for (const auto& col : BoundColumns_) { - const SQLRETURN rc = Cursor_->GetData(col.ColumnNumber, col.TargetType, col.TargetValue, col.BufferLength, col.StrLenOrInd); + const SQLULEN bindType = CurrentAppRowDesc_->GetBindType(); + const SQLULEN offset = CurrentAppRowDesc_->GetBindOffsetPtr() + ? *CurrentAppRowDesc_->GetBindOffsetPtr() + : 0; + for (SQLSMALLINT number = 1; number <= CurrentAppRowDesc_->GetRecordCount(); ++number) { + const TDescRecord* col = CurrentAppRowDesc_->FindRecord(number); + if (!col || !col->DataPtr) { + continue; + } + const SQLULEN dataStride = bindType == SQL_BIND_BY_COLUMN + ? CTypeSize(col->Type, col->OctetLength) + : bindType; + const SQLULEN indicatorStride = bindType == SQL_BIND_BY_COLUMN + ? sizeof(SQLLEN) + : bindType; + SQLPOINTER target = OffsetPointer( + static_cast(col->DataPtr), offset, BindingRow_, dataStride); + SQLLEN* indicator = OffsetPointer( + col->IndicatorPtr, offset, BindingRow_, indicatorStride); + SQLLEN* length = OffsetPointer( + col->OctetLengthPtr, offset, BindingRow_, indicatorStride); + SQLLEN convertedLength = 0; + SQLRETURN rc = Cursor_->GetData( + static_cast(number), col->Type, target, col->OctetLength, + &convertedLength); + if (convertedLength == SQL_NULL_DATA) { + if (!indicator) { + AddError("22002", 0, "Indicator variable required but not supplied"); + rc = SQL_ERROR; + } else { + *indicator = SQL_NULL_DATA; + if (length && length != indicator) { + *length = 0; + } + } + } else { + if (length) { + *length = convertedLength; + } + if (indicator && indicator != length) { + *indicator = 0; + } + } if (rc == SQL_SUCCESS_WITH_INFO) { AddError("01004", 0, "String data, right truncated", SQL_SUCCESS_WITH_INFO); if (LastFetchRc_ == SQL_SUCCESS) { @@ -279,13 +478,17 @@ SQLRETURN TStatement::BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, } } - BoundColumns_.erase(std::remove_if(BoundColumns_.begin(), BoundColumns_.end(), - [columnNumber](const TBoundColumn& col) { return col.ColumnNumber == columnNumber; }), BoundColumns_.end()); - if (!targetValue) { + CurrentAppRowDesc_->RemoveRecord(static_cast(columnNumber)); return SQL_SUCCESS; } - BoundColumns_.push_back({columnNumber, targetType, targetValue, bufferLength, strLenOrInd}); + TDescRecord& record = CurrentAppRowDesc_->Record(static_cast(columnNumber)); + record.Type = targetType; + record.Length = bufferLength; + record.OctetLength = bufferLength; + record.DataPtr = targetValue; + record.IndicatorPtr = strLenOrInd; + record.OctetLengthPtr = strLenOrInd; return SQL_SUCCESS; } @@ -303,32 +506,88 @@ SQLRETURN TStatement::BindParameter(SQLUSMALLINT paramNumber, throw TOdbcException("HYC00", 0, "Only input parameters are supported"); } - const bool atExec = strLenOrIndPtr && *strLenOrIndPtr == SQL_DATA_AT_EXEC; - - BoundParams_.erase(std::remove_if(BoundParams_.begin(), BoundParams_.end(), - [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }), BoundParams_.end()); + const bool atExec = strLenOrIndPtr + && (*strLenOrIndPtr == SQL_DATA_AT_EXEC + || *strLenOrIndPtr <= SQL_LEN_DATA_AT_EXEC_OFFSET); - if (!parameterValuePtr && !atExec) { + if (!parameterValuePtr && !strLenOrIndPtr) { + CurrentAppParamDesc_->RemoveRecord(static_cast(paramNumber)); + ImpParamDesc_.RemoveRecord(static_cast(paramNumber)); return SQL_SUCCESS; } - BoundParams_.push_back({paramNumber, inputOutputType, valueType, parameterType, columnSize, decimalDigits, - parameterValuePtr, bufferLength, strLenOrIndPtr, atExec, false, {}}); + TDescRecord& app = CurrentAppParamDesc_->Record(static_cast(paramNumber)); + app.Type = valueType; + app.Length = bufferLength; + app.OctetLength = bufferLength; + app.DataPtr = parameterValuePtr; + app.IndicatorPtr = strLenOrIndPtr; + app.OctetLengthPtr = strLenOrIndPtr; + app.ParameterType = inputOutputType; + app.AtExec = atExec; + app.AtExecComplete = false; + app.AtExecIndicator = 0; + app.AtExecChunk.clear(); + + TDescRecord& imp = ImpParamDesc_.Record(static_cast(paramNumber)); + imp.Type = parameterType; + imp.Length = static_cast(columnSize); + imp.OctetLength = static_cast(columnSize); + imp.Precision = static_cast(columnSize); + imp.Scale = decimalDigits; + imp.Nullable = SQL_NULLABLE; + imp.ParameterType = inputOutputType; return SQL_SUCCESS; } -SQLRETURN TStatement::BuildParams(NYdb::TParams& out) { +std::vector TStatement::GetBoundParams(SQLULEN paramSet) const { + std::vector params; + const SQLULEN offset = CurrentAppParamDesc_->GetBindOffsetPtr() + ? *CurrentAppParamDesc_->GetBindOffsetPtr() + : 0; + for (SQLSMALLINT number = 1; number <= ParamCount_; ++number) { + const TDescRecord* app = CurrentAppParamDesc_->FindRecord(number); + const TDescRecord* imp = ImpParamDesc_.FindRecord(number); + if (!app || !imp) { + continue; + } + SQLLEN* lengthOrIndicator = app->IndicatorPtr == app->OctetLengthPtr + ? app->IndicatorPtr + : app->OctetLengthPtr; + TBoundParam param{ + static_cast(number), imp->ParameterType, app->Type, imp->Type, + static_cast(imp->Length), imp->Scale, app->DataPtr, app->OctetLength, + lengthOrIndicator, app->AtExec, app->AtExecComplete, app->AtExecChunk}; + param = ParamAt(param, paramSet, CurrentAppParamDesc_->GetBindType(), offset); + SQLLEN* indicator = OffsetPointer( + app->IndicatorPtr, offset, paramSet, + CurrentAppParamDesc_->GetBindType() == SQL_PARAM_BIND_BY_COLUMN + ? sizeof(SQLLEN) + : CurrentAppParamDesc_->GetBindType()); + if (indicator && *indicator == SQL_NULL_DATA) { + param.StrLenOrIndPtr = indicator; + } + params.push_back(std::move(param)); + } + return params; +} + +SQLRETURN TStatement::BuildParams(NYdb::TParams& out, SQLULEN paramSet) { ClearErrors(); NYdb::TParamsBuilder paramsBuilder; - for (const auto& param : BoundParams_) { + for (const TBoundParam& param : GetBoundParams(paramSet)) { const std::string paramName = "$p" + std::to_string(param.ParamNumber); if (param.AtExec) { - if (!param.AtExecComplete || param.AtExecChunk.empty()) { + if (!param.AtExecComplete) { return AddError("HY000", 0, "Missing data-at-execution parameter value"); } - SQLLEN nts = SQL_NTS; + const TDescRecord* record = CurrentAppParamDesc_->FindRecord( + static_cast(param.ParamNumber)); + SQLLEN indicator = record && record->AtExecIndicator == SQL_NULL_DATA + ? SQL_NULL_DATA + : SQL_NTS; TBoundParam tmp = param; tmp.ParameterValuePtr = const_cast(param.AtExecChunk.data()); - tmp.StrLenOrIndPtr = &nts; + tmp.StrLenOrIndPtr = &indicator; const SQLRETURN convRc = ConvertParam(tmp, paramsBuilder.AddParam(paramName)); if (convRc != SQL_SUCCESS) { return AddError("07006", 0, "Unsupported or invalid ODBC parameter type for parameter " @@ -365,7 +624,7 @@ SQLRETURN TStatement::NumParams(SQLSMALLINT* paramCount) { void TStatement::ResetForMetadata() { ClearErrors(); RowsFetched_ = 0; - Cursor_.reset(); + SetCursor(nullptr); } SQLRETURN TStatement::DescribeParam(SQLUSMALLINT paramNumber, SQLSMALLINT* dataTypePtr, SQLULEN* paramSizePtr, @@ -376,12 +635,11 @@ SQLRETURN TStatement::DescribeParam(SQLUSMALLINT paramNumber, SQLSMALLINT* dataT if (paramNumber < 1 || paramNumber > ParamCount_) { throw TOdbcException("07009", 0, "Invalid descriptor index"); } - const auto it = std::find_if(BoundParams_.begin(), BoundParams_.end(), - [paramNumber](const TBoundParam& p) { return p.ParamNumber == paramNumber; }); - const SQLSMALLINT dataType = it != BoundParams_.end() ? it->ParameterType : SQL_UNKNOWN_TYPE; - const SQLULEN paramSize = it != BoundParams_.end() ? it->ColumnSize : 0; - const SQLSMALLINT decimalDigits = it != BoundParams_.end() ? it->DecimalDigits : 0; - const SQLSMALLINT nullable = it != BoundParams_.end() ? SQL_NULLABLE : SQL_NULLABLE_UNKNOWN; + const TDescRecord* record = ImpParamDesc_.FindRecord(static_cast(paramNumber)); + const SQLSMALLINT dataType = record ? record->Type : SQL_UNKNOWN_TYPE; + const SQLULEN paramSize = record ? static_cast(record->Length) : 0; + const SQLSMALLINT decimalDigits = record ? record->Scale : 0; + const SQLSMALLINT nullable = record ? record->Nullable : SQL_NULLABLE_UNKNOWN; if (dataTypePtr) { *dataTypePtr = dataType; } @@ -404,10 +662,19 @@ SQLRETURN TStatement::ParamData(SQLPOINTER* valuePtr) { if (!InAtExec_) { return SQL_NO_DATA; } + if (NeedDataParam_ != 0 && NeedDataTokenDelivered_) { + if (TDescRecord* record = CurrentAppParamDesc_->FindRecord( + static_cast(NeedDataParam_))) { + record->AtExecComplete = true; + } + NeedDataParam_ = 0; + NeedDataTokenDelivered_ = false; + } const SQLUSMALLINT next = FindNextNeedDataParam(); if (next != 0) { NeedDataParam_ = next; - *valuePtr = reinterpret_cast(static_cast(next)); + NeedDataTokenDelivered_ = true; + *valuePtr = CurrentAppParamDesc_->FindRecord(static_cast(next))->DataPtr; return SQL_NEED_DATA; } InAtExec_ = false; @@ -419,26 +686,33 @@ SQLRETURN TStatement::PutData(SQLPOINTER data, SQLLEN strLenOrInd) { if (!InAtExec_ || NeedDataParam_ == 0) { throw TOdbcException("HY010", 0, "Function sequence error"); } - for (auto& param : BoundParams_) { - if (param.ParamNumber != NeedDataParam_) { - continue; - } - SQLLEN chunkLen = strLenOrInd; - if (chunkLen == SQL_NTS) { - if (!data) { - throw TOdbcException("HY009", 0, "Invalid use of null pointer"); - } - chunkLen = static_cast(std::strlen(static_cast(data))); - } - if (chunkLen > 0 && data) { - const char* bytes = static_cast(data); - param.AtExecChunk.append(bytes, static_cast(chunkLen)); + TDescRecord* param = CurrentAppParamDesc_->FindRecord( + static_cast(NeedDataParam_)); + if (!param || !NeedDataTokenDelivered_) { + throw TOdbcException("HY010", 0, "Function sequence error"); + } + SQLLEN chunkLen = strLenOrInd; + if (chunkLen == SQL_NULL_DATA) { + param->AtExecIndicator = SQL_NULL_DATA; + return SQL_SUCCESS; + } + if (chunkLen == SQL_DEFAULT_PARAM) { + throw TOdbcException("07S01", 0, "Default parameters are not supported"); + } + if (chunkLen == SQL_NTS) { + if (!data) { + throw TOdbcException("HY009", 0, "Invalid use of null pointer"); } - if (strLenOrInd == 0 || strLenOrInd == SQL_NTS) { - param.AtExecComplete = true; - NeedDataParam_ = 0; + chunkLen = static_cast(std::strlen(static_cast(data))); + } + if (chunkLen < 0) { + throw TOdbcException("HY090", 0, "Invalid string or buffer length"); + } + if (chunkLen > 0) { + if (!data) { + throw TOdbcException("HY009", 0, "Invalid use of null pointer"); } - break; + param->AtExecChunk.append(static_cast(data), static_cast(chunkLen)); } return SQL_SUCCESS; } @@ -447,12 +721,16 @@ SQLRETURN TStatement::Cancel() { if (!Cursor_ && !InAtExec_) { return SQL_SUCCESS; } - Cursor_.reset(); + SetCursor(nullptr); InAtExec_ = false; NeedDataParam_ = 0; - for (auto& param : BoundParams_) { - param.AtExecComplete = false; - param.AtExecChunk.clear(); + NeedDataTokenDelivered_ = false; + for (SQLSMALLINT i = 1; i <= CurrentAppParamDesc_->GetRecordCount(); ++i) { + if (TDescRecord* param = CurrentAppParamDesc_->FindRecord(i)) { + param->AtExecComplete = false; + param->AtExecIndicator = 0; + param->AtExecChunk.clear(); + } } RowsFetched_ = 0; return SQL_SUCCESS; @@ -473,18 +751,19 @@ SQLRETURN TStatement::Close(bool force) { throw TOdbcException("24000", 0, "Invalid handle"); } - Cursor_.reset(); + SetCursor(nullptr); RowsFetched_ = 0; ClearErrors(); return SQL_SUCCESS; } void TStatement::UnbindColumns() { - BoundColumns_.clear(); + CurrentAppRowDesc_->ClearRecords(); } void TStatement::ResetParams() { - BoundParams_.clear(); + CurrentAppParamDesc_->ClearRecords(); + ImpParamDesc_.ClearRecords(); } SQLRETURN TStatement::RowCount(SQLLEN* rowCount) { @@ -513,7 +792,76 @@ const std::vector& TStatement::GetColumnMeta() const { return Cursor_ ? Cursor_->GetColumnMeta() : EmptyColumns; } +void TStatement::SetCursor(std::unique_ptr cursor) { + Cursor_ = std::move(cursor); + GetDataOffsets_.clear(); + ImpRowDesc_.ClearRecords(); + if (!Cursor_) { + return; + } + SQLSMALLINT number = 0; + for (const TColumnMeta& column : Cursor_->GetColumnMeta()) { + TDescRecord& record = ImpRowDesc_.Record(++number); + record.Name = column.Name; + record.Type = column.SqlType; + record.Length = static_cast(column.Size); + record.OctetLength = static_cast(column.Size); + record.Precision = static_cast(column.Size); + record.Scale = column.DecimalDigits; + record.Nullable = column.Nullable; + } +} + SQLRETURN TStatement::SetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER stringLength) { + if (attr == SQL_ATTR_APP_ROW_DESC || attr == SQL_ATTR_APP_PARAM_DESC) { + TDescriptor* desc = value ? TDescriptor::FromHandle(value) : nullptr; + if (desc && (desc->GetDescType() != EDescType::Explicit + || desc->GetConnection() != Conn_)) { + return AddError("HY024", 0, "Descriptor belongs to another connection"); + } + TDescriptor*& current = attr == SQL_ATTR_APP_ROW_DESC + ? CurrentAppRowDesc_ + : CurrentAppParamDesc_; + TDescriptor* const automatic = attr == SQL_ATTR_APP_ROW_DESC + ? &AppRowDesc_ + : &AppParamDesc_; + TDescriptor* const next = desc ? desc : automatic; + if (current != next) { + TDescriptor* const previous = current; + current = next; + current->Attach(this); + if (CurrentAppRowDesc_ != previous && CurrentAppParamDesc_ != previous) { + previous->Detach(this); + } + } + return SQL_SUCCESS; + } + const SQLULEN integer = ReadIntegerAttr(value); + switch (attr) { + case SQL_ATTR_PARAM_BIND_TYPE: CurrentAppParamDesc_->SetBindType(integer); return SQL_SUCCESS; + case SQL_ATTR_PARAMSET_SIZE: + if (integer == 0) return Diag::AddInvalidAttrValue(*this, "SQL_ATTR_PARAMSET_SIZE"); + CurrentAppParamDesc_->SetArraySize(integer); return SQL_SUCCESS; + case SQL_ATTR_PARAM_BIND_OFFSET_PTR: + CurrentAppParamDesc_->SetBindOffsetPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_PARAM_OPERATION_PTR: + CurrentAppParamDesc_->SetArrayStatusPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_PARAM_STATUS_PTR: + ImpParamDesc_.SetArrayStatusPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_PARAMS_PROCESSED_PTR: + ImpParamDesc_.SetRowsProcessedPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_ROW_BIND_TYPE: CurrentAppRowDesc_->SetBindType(integer); return SQL_SUCCESS; + case SQL_ATTR_ROW_ARRAY_SIZE: + if (integer == 0) return Diag::AddInvalidAttrValue(*this, "SQL_ATTR_ROW_ARRAY_SIZE"); + CurrentAppRowDesc_->SetArraySize(integer); return SQL_SUCCESS; + case SQL_ATTR_ROW_BIND_OFFSET_PTR: + CurrentAppRowDesc_->SetBindOffsetPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_ROW_STATUS_PTR: + ImpRowDesc_.SetArrayStatusPtr(static_cast(value)); return SQL_SUCCESS; + case SQL_ATTR_ROWS_FETCHED_PTR: + ImpRowDesc_.SetRowsProcessedPtr(static_cast(value)); return SQL_SUCCESS; + default: break; + } return Attributes_.SetStmtAttr(attr, value, stringLength, *this); } @@ -523,17 +871,39 @@ SQLRETURN TStatement::GetStmtAttr(SQLINTEGER attr, SQLPOINTER value, SQLINTEGER } switch (attr) { case SQL_ATTR_APP_ROW_DESC: - *reinterpret_cast(value) = AppRowDesc_.get(); + *reinterpret_cast(value) = CurrentAppRowDesc_; return SQL_SUCCESS; case SQL_ATTR_APP_PARAM_DESC: - *reinterpret_cast(value) = AppParamDesc_.get(); + *reinterpret_cast(value) = CurrentAppParamDesc_; return SQL_SUCCESS; case SQL_ATTR_IMP_ROW_DESC: - *reinterpret_cast(value) = ImpRowDesc_.get(); + *reinterpret_cast(value) = &ImpRowDesc_; return SQL_SUCCESS; case SQL_ATTR_IMP_PARAM_DESC: - *reinterpret_cast(value) = ImpParamDesc_.get(); + *reinterpret_cast(value) = &ImpParamDesc_; return SQL_SUCCESS; + case SQL_ATTR_PARAM_BIND_TYPE: + *static_cast(value) = CurrentAppParamDesc_->GetBindType(); return SQL_SUCCESS; + case SQL_ATTR_PARAMSET_SIZE: + *static_cast(value) = CurrentAppParamDesc_->GetArraySize(); return SQL_SUCCESS; + case SQL_ATTR_PARAM_BIND_OFFSET_PTR: + *static_cast(value) = CurrentAppParamDesc_->GetBindOffsetPtr(); return SQL_SUCCESS; + case SQL_ATTR_PARAM_OPERATION_PTR: + *static_cast(value) = CurrentAppParamDesc_->GetArrayStatusPtr(); return SQL_SUCCESS; + case SQL_ATTR_PARAM_STATUS_PTR: + *static_cast(value) = ImpParamDesc_.GetArrayStatusPtr(); return SQL_SUCCESS; + case SQL_ATTR_PARAMS_PROCESSED_PTR: + *static_cast(value) = ImpParamDesc_.GetRowsProcessedPtr(); return SQL_SUCCESS; + case SQL_ATTR_ROW_BIND_TYPE: + *static_cast(value) = CurrentAppRowDesc_->GetBindType(); return SQL_SUCCESS; + case SQL_ATTR_ROW_ARRAY_SIZE: + *static_cast(value) = CurrentAppRowDesc_->GetArraySize(); return SQL_SUCCESS; + case SQL_ATTR_ROW_BIND_OFFSET_PTR: + *static_cast(value) = CurrentAppRowDesc_->GetBindOffsetPtr(); return SQL_SUCCESS; + case SQL_ATTR_ROW_STATUS_PTR: + *static_cast(value) = ImpRowDesc_.GetArrayStatusPtr(); return SQL_SUCCESS; + case SQL_ATTR_ROWS_FETCHED_PTR: + *static_cast(value) = ImpRowDesc_.GetRowsProcessedPtr(); return SQL_SUCCESS; default: break; } @@ -556,5 +926,4 @@ SQLRETURN TStatement::GetDiagField( return TErrorManager::GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); } -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 7a49ea48b02..c67848d4be4 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -17,13 +17,13 @@ #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { -class TStatement : public TErrorManager, public IBindingFiller { +class TStatement : public TErrorManager { friend class TDescriptor; public: TStatement(TConnection* conn); + ~TStatement(); SQLRETURN Prepare(const std::string& statementText); SQLRETURN Execute(); @@ -33,9 +33,6 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); - void FillBoundColumns() override; - void OnStreamPartError(const TStatus& status) override; - SQLRETURN Close(bool force = false); void UnbindColumns(); void ResetParams(); @@ -82,10 +79,7 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN SetCursorName(const std::string& name); SQLRETURN GetCursorName(SQLCHAR* name, SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr); - TDescriptor& GetAppRowDesc() { return *AppRowDesc_; } - TDescriptor& GetAppParamDesc() { return *AppParamDesc_; } - TDescriptor& GetImpRowDesc() { return *ImpRowDesc_; } - TDescriptor& GetImpParamDesc() { return *ImpParamDesc_; } + void DetachDescriptor(TDescriptor* desc); SQLRETURN RowCount(SQLLEN* rowCount); SQLRETURN NumResultCols(SQLSMALLINT* colCount); @@ -96,12 +90,6 @@ class TStatement : public TErrorManager, public IBindingFiller { SQLRETURN GetDiagField(SQLSMALLINT recNumber, SQLSMALLINT diagIdentifier, SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) override; - SQLSMALLINT GetParamCount() const { return ParamCount_; } - - TConnection* GetConnection() { - return Conn_; - } - private: TConnection* Conn_; std::unique_ptr Cursor_; @@ -109,29 +97,35 @@ class TStatement : public TErrorManager, public IBindingFiller { bool IsPrepared_ = false; SQLSMALLINT ParamCount_ = 0; - std::vector BoundColumns_; - std::vector BoundParams_; - bool StreamFetchError_ = false; SQLULEN RowsFetched_ = 0; SQLLEN RowCount_ = -1; TStatementAttributes Attributes_; std::string CursorName_; - std::unique_ptr AppRowDesc_; - std::unique_ptr AppParamDesc_; - std::unique_ptr ImpRowDesc_; - std::unique_ptr ImpParamDesc_; + TDescriptor AppRowDesc_; + TDescriptor AppParamDesc_; + TDescriptor ImpRowDesc_; + TDescriptor ImpParamDesc_; + TDescriptor* CurrentAppRowDesc_; + TDescriptor* CurrentAppParamDesc_; SQLUSMALLINT NeedDataParam_ = 0; bool InAtExec_ = false; + bool NeedDataTokenDelivered_ = false; SQLRETURN LastFetchRc_ = SQL_SUCCESS; + SQLULEN BindingRow_ = 0; + std::vector GetDataOffsets_; - SQLRETURN BuildParams(NYdb::TParams& out); + SQLRETURN BuildParams(NYdb::TParams& out, SQLULEN paramSet); + SQLRETURN ExecuteParamSet(SQLULEN paramSet); + void FillBoundColumns(); + std::vector GetBoundParams(SQLULEN paramSet) const; + void SetCursor(std::unique_ptr cursor); void ResetForMetadata(); SQLUSMALLINT FindNextNeedDataParam() const; std::string GetTraversalRoot(const std::string& pattern) const; - NQuery::TExecuteQueryIterator CreateExecuteIterator(NQuery::TSession& session, const NYdb::TParams& params); + NQuery::TExecuteQueryResult ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); std::vector GetPatternEntries(const std::string& pattern); @@ -140,5 +134,4 @@ class TStatement : public TErrorManager, public IBindingFiller { std::optional GetTableType(NScheme::ESchemeEntryType type); }; -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/statement_attr.cpp b/odbc/src/statement_attr.cpp index f0baad0016a..1f15001444d 100644 --- a/odbc/src/statement_attr.cpp +++ b/odbc/src/statement_attr.cpp @@ -46,6 +46,15 @@ SQLRETURN TStatementAttributes::SetStmtAttr( MetadataId_ = *mode; return SQL_SUCCESS; } + case SQL_ATTR_CURSOR_TYPE: { + const SQLULEN cursorType = ReadIntegerAttr(value); + if (cursorType != SQL_CURSOR_FORWARD_ONLY) { + return errors.AddError( + "01S02", 0, "Only SQL_CURSOR_FORWARD_ONLY is supported", SQL_SUCCESS_WITH_INFO); + } + CursorType_ = cursorType; + return SQL_SUCCESS; + } default: return Diag::AddNotImplemented(errors); } @@ -76,6 +85,9 @@ SQLRETURN TStatementAttributes::GetStmtAttr( case SQL_ATTR_METADATA_ID: *reinterpret_cast(value) = MetadataId_; return SQL_SUCCESS; + case SQL_ATTR_CURSOR_TYPE: + *reinterpret_cast(value) = CursorType_; + return SQL_SUCCESS; default: return Diag::AddNotImplemented(errors); } diff --git a/odbc/src/statement_attr.h b/odbc/src/statement_attr.h index b0d6e9bd97f..8b92ba10ed1 100644 --- a/odbc/src/statement_attr.h +++ b/odbc/src/statement_attr.h @@ -33,6 +33,7 @@ class TStatementAttributes { SQLULEN MaxRows_ = 0; SQLULEN NoScan_ = SQL_NOSCAN_OFF; SQLULEN MetadataId_ = SQL_FALSE; + SQLULEN CursorType_ = SQL_CURSOR_FORWARD_ONLY; }; } // namespace NOdbc diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp index 1a7744e31a0..4acc63e14b5 100644 --- a/odbc/src/statement_metadata.cpp +++ b/odbc/src/statement_metadata.cpp @@ -103,7 +103,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, table.reserve(entries.size()); if (entries.empty()) { - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } @@ -178,7 +178,7 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, NStatusHelpers::ThrowOnError(status); } - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } @@ -216,7 +216,7 @@ SQLRETURN TStatement::Tables(const std::string& catalogName, }); } - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } @@ -245,7 +245,7 @@ SQLRETURN TStatement::GetTypeInfo(SQLSMALLINT dataType) { {"INTERVAL_PRECISION", SQL_SMALLINT, 0, SQL_NULLABLE}, }; - Cursor_ = CreateVirtualCursor(this, columns, BuildTypeInfoRows(dataType)); + SetCursor(CreateVirtualCursor(columns, BuildTypeInfoRows(dataType))); return SQL_SUCCESS; } @@ -272,7 +272,7 @@ SQLRETURN TStatement::Statistics(const std::string& /*catalogName*/, {"FILTER_CONDITION", SQL_VARCHAR, 128, SQL_NULLABLE}, }; - Cursor_ = CreateVirtualCursor(this, columns, TTable{}); + SetCursor(CreateVirtualCursor(columns, TTable{})); return SQL_SUCCESS; } @@ -302,7 +302,7 @@ SQLRETURN TStatement::SpecialColumns(const std::string& /*catalogName*/, auto entries = GetPatternEntries(tableName); if (entries.size() != 1) { if (entries.empty()) { - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } throw TOdbcException("HY000", 0, "Ambiguous table name"); @@ -344,7 +344,7 @@ SQLRETURN TStatement::SpecialColumns(const std::string& /*catalogName*/, }); NStatusHelpers::ThrowOnError(status); - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } @@ -366,7 +366,7 @@ SQLRETURN TStatement::PrimaryKeys(const std::string& /*catalogName*/, auto entries = GetPatternEntries(tableName); if (entries.size() != 1) { if (entries.empty()) { - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } throw TOdbcException("HY000", 0, "Ambiguous table name"); @@ -398,7 +398,7 @@ SQLRETURN TStatement::PrimaryKeys(const std::string& /*catalogName*/, }); NStatusHelpers::ThrowOnError(status); - Cursor_ = CreateVirtualCursor(this, columns, table); + SetCursor(CreateVirtualCursor(columns, table)); return SQL_SUCCESS; } @@ -427,7 +427,7 @@ SQLRETURN TStatement::ForeignKeys(const std::string& /*pkCatalogName*/, {"DEFERRABILITY", SQL_SMALLINT, 0, SQL_NULLABLE}, }; - Cursor_ = CreateVirtualCursor(this, columns, TTable{}); + SetCursor(CreateVirtualCursor(columns, TTable{})); return SQL_SUCCESS; } diff --git a/odbc/src/utils/bindings.h b/odbc/src/utils/bindings.h index f75cd12289f..17ce8158609 100644 --- a/odbc/src/utils/bindings.h +++ b/odbc/src/utils/bindings.h @@ -25,22 +25,5 @@ struct TBoundParam { std::string AtExecChunk; }; -struct TBoundColumn { - SQLUSMALLINT ColumnNumber; - SQLSMALLINT TargetType; - SQLPOINTER TargetValue; - SQLLEN BufferLength; - SQLLEN* StrLenOrInd; -}; - -class IBindingFiller { -public: - virtual void FillBoundColumns() = 0; - virtual void OnStreamPartError([[maybe_unused]] const TStatus& status) { - } - - virtual ~IBindingFiller() = default; -}; - } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/utils/convert.cpp b/odbc/src/utils/convert.cpp index c25715c893c..46e658beda2 100644 --- a/odbc/src/utils/convert.cpp +++ b/odbc/src/utils/convert.cpp @@ -1,21 +1,24 @@ #include "convert.h" #include -#include #include #include #include -#include +#include +#include #include +#include -namespace NYdb { -namespace NOdbc { - +namespace NYdb::NOdbc { namespace { thread_local const char* LastConvertSqlState = nullptr; +void SetNumericOutOfRange() { + LastConvertSqlState = "22003"; +} + bool FitsInt16(int64_t value) { return value >= INT16_MIN && value <= INT16_MAX; } @@ -24,12 +27,9 @@ bool FitsInt32(int64_t value) { return value >= INT32_MIN && value <= INT32_MAX; } -void SetNumericOutOfRange() { - LastConvertSqlState = "22003"; -} - -std::optional GetAsInt64(TValueParser& parser, EPrimitiveType ydbType) { - switch (ydbType) { +std::optional GetAsInt64(TValueParser& parser, EPrimitiveType type) { + switch (type) { + case EPrimitiveType::Bool: return parser.GetBool() ? 1 : 0; case EPrimitiveType::Int8: return parser.GetInt8(); case EPrimitiveType::Uint8: return parser.GetUint8(); case EPrimitiveType::Int16: return parser.GetInt16(); @@ -38,544 +38,259 @@ std::optional GetAsInt64(TValueParser& parser, EPrimitiveType ydbType) case EPrimitiveType::Uint32: return parser.GetUint32(); case EPrimitiveType::Int64: return parser.GetInt64(); case EPrimitiveType::Uint64: { - const uint64_t unsignedValue = parser.GetUint64(); - if (unsignedValue > static_cast(INT64_MAX)) { - SetNumericOutOfRange(); - return std::nullopt; - } - return static_cast(unsignedValue); + const uint64_t value = parser.GetUint64(); + if (value <= static_cast(INT64_MAX)) return static_cast(value); + SetNumericOutOfRange(); + return std::nullopt; } - case EPrimitiveType::Bool: return parser.GetBool() ? 1 : 0; default: return std::nullopt; } } -} // namespace - -template -struct TSqlTypeTraits; - -template<> struct TSqlTypeTraits { using Type = std::string; }; -template<> struct TSqlTypeTraits { using Type = std::string; }; -template<> struct TSqlTypeTraits { using Type = SQLBIGINT; }; -template<> struct TSqlTypeTraits { using Type = SQLUBIGINT; }; -template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; -template<> struct TSqlTypeTraits { using Type = SQLINTEGER; }; -template<> struct TSqlTypeTraits { using Type = SQLUINTEGER; }; -template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; -template<> struct TSqlTypeTraits { using Type = SQLSMALLINT; }; -template<> struct TSqlTypeTraits { using Type = SQLUSMALLINT; }; -template<> struct TSqlTypeTraits { using Type = SQLSCHAR; }; -template<> struct TSqlTypeTraits { using Type = SQLCHAR; }; -template<> struct TSqlTypeTraits { using Type = SQLDOUBLE; }; -template<> struct TSqlTypeTraits { using Type = SQLFLOAT; }; -template<> struct TSqlTypeTraits { using Type = SQLCHAR; }; - -template -struct TTypedValue { - using TSrcType = typename TSqlTypeTraits::Type; - - TSrcType Data; - - TTypedValue(const TBoundParam& param) { - Data = *static_cast(param.ParameterValuePtr); - } -}; - -template<> -TTypedValue::TTypedValue(const TBoundParam& param) { - if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { - Data.clear(); - return; +std::optional ReadInteger(const TBoundParam& param) { + if (!param.ParameterValuePtr) return std::nullopt; + const auto type = param.ValueType; + if (type == SQL_C_SBIGINT) return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_UBIGINT) { + const SQLUBIGINT value = *static_cast(param.ParameterValuePtr); + if (value <= static_cast(INT64_MAX)) return static_cast(value); + SetNumericOutOfRange(); + return std::nullopt; } - - const char* ptr = static_cast(param.ParameterValuePtr); - if (!ptr) { - Data.clear(); - return; - } - + if (type == SQL_C_LONG || type == SQL_C_SLONG) + return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_ULONG) + return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_SHORT || type == SQL_C_SSHORT) + return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_USHORT) + return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_TINYINT || type == SQL_C_STINYINT) + return *static_cast(param.ParameterValuePtr); + if (type == SQL_C_UTINYINT || type == SQL_C_BIT) + return *static_cast(param.ParameterValuePtr); + return std::nullopt; +} + +std::optional ReadBytes(const TBoundParam& param) { + if (!param.ParameterValuePtr) return std::nullopt; + const char* data = static_cast(param.ParameterValuePtr); + SQLLEN length = param.BufferLength; if (param.StrLenOrIndPtr) { - SQLLEN len = *param.StrLenOrIndPtr; - if (len == SQL_NTS) { - Data = std::string(ptr); - } else if (len >= 0) { - Data = std::string(ptr, static_cast(len)); - } else { - Data = std::string(ptr, param.BufferLength); - } - } else { - Data = std::string(ptr, param.BufferLength); - } -} - -template<> -TTypedValue::TTypedValue(const TBoundParam& param) { - if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { - Data.clear(); - return; - } - - const char* ptr = static_cast(param.ParameterValuePtr); - if (!ptr) { - Data.clear(); - return; - } - - if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr >= 0) { - Data = std::string(ptr, static_cast(*param.StrLenOrIndPtr)); - } else { - Data = std::string(ptr, param.BufferLength); - } -} - -class IConverter { -public: - virtual void AddToBuilder(const TBoundParam& param, TParamValueBuilder& builder) = 0; - - virtual ~IConverter() = default; -}; - -template -class TConverter : public IConverter { -public: - virtual void AddToBuilder(const TBoundParam& param, TParamValueBuilder& builder) override { - TTypedValue value(param); - Convert(param, std::move(value.Data), builder); - if (param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA) { - builder.EmptyOptional(GetType()); - } - builder.Build(); - } - -private: - void Convert(const TBoundParam& param, TTypedValue::TSrcType&& data, TParamValueBuilder& builder); - TType GetType(); -}; - -class TConverterRegistry { -public: - static TConverterRegistry& GetInstance() { - static TConverterRegistry instance; - return instance; + length = *param.StrLenOrIndPtr; + if (length == SQL_NTS) return std::string(data); + if (length < 0) length = param.BufferLength; } - - void RegisterConverter(SQLSMALLINT cType, SQLSMALLINT sqlType, std::unique_ptr converter) { - Converters_.emplace(std::make_pair(cType, sqlType), std::move(converter)); - } - - IConverter* GetConverter(SQLSMALLINT cType, SQLSMALLINT sqlType) { - auto it = Converters_.find(std::make_pair(cType, sqlType)); - if (it != Converters_.end()) { - return it->second.get(); - } - return nullptr; + if (length < 0) return std::nullopt; + return std::string(data, static_cast(length)); +} + +std::optional ParameterPrimitive(SQLSMALLINT sqlType) { + switch (sqlType) { + case SQL_BIGINT: return EPrimitiveType::Int64; + case SQL_INTEGER: return EPrimitiveType::Int32; + case SQL_SMALLINT: return EPrimitiveType::Int16; + case SQL_TINYINT: return EPrimitiveType::Int8; + case SQL_BIT: return EPrimitiveType::Bool; + case SQL_REAL: return EPrimitiveType::Float; + case SQL_FLOAT: + case SQL_DOUBLE: return EPrimitiveType::Double; + case SQL_CHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: return EPrimitiveType::Utf8; + case SQL_BINARY: + case SQL_VARBINARY: + case SQL_LONGVARBINARY: return EPrimitiveType::String; + default: return std::nullopt; } - -private: - std::map, std::unique_ptr> Converters_; -}; - -#define REGISTER_CONVERTER(CType, SqlType, YdbType) \ - struct TConverterRegistration##CType##SqlType { \ - TConverterRegistration##CType##SqlType() { \ - TConverterRegistry::GetInstance().RegisterConverter(CType, SqlType, std::make_unique>()); \ - } \ - }; \ - static const TConverterRegistration##CType##SqlType converterRegistration##CType##SqlType; \ - template<> \ - TType TConverter::GetType() { \ - return TTypeBuilder().Primitive(YdbType).Build(); \ - } \ - template<> \ - void TConverter::Convert(const TBoundParam& param, TTypedValue::TSrcType&& data, TParamValueBuilder& builder) - -// Integer types - -REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_BIGINT, EPrimitiveType::Int64) { - builder.OptionalInt64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_LONG, SQL_BIGINT, EPrimitiveType::Int64) { - builder.OptionalInt64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SLONG, SQL_BIGINT, EPrimitiveType::Int64) { - builder.OptionalInt64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SHORT, SQL_BIGINT, EPrimitiveType::Int64) { - builder.OptionalInt64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_TINYINT, SQL_BIGINT, EPrimitiveType::Int64) { - builder.OptionalInt64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_BIGINT, EPrimitiveType::Uint64) { - builder.OptionalUint64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_ULONG, SQL_BIGINT, EPrimitiveType::Uint64) { - builder.OptionalUint64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_USHORT, SQL_BIGINT, EPrimitiveType::Uint64) { - builder.OptionalUint64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_BIGINT, EPrimitiveType::Uint64) { - builder.OptionalUint64(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_INTEGER, EPrimitiveType::Int32) { - builder.OptionalInt32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_LONG, SQL_INTEGER, EPrimitiveType::Int32) { - builder.OptionalInt32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SLONG, SQL_INTEGER, EPrimitiveType::Int32) { - builder.OptionalInt32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SHORT, SQL_INTEGER, EPrimitiveType::Int32) { - builder.OptionalInt32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_TINYINT, SQL_INTEGER, EPrimitiveType::Int32) { - builder.OptionalInt32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_INTEGER, EPrimitiveType::Uint32) { - builder.OptionalUint32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_ULONG, SQL_INTEGER, EPrimitiveType::Uint32) { - builder.OptionalUint32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_USHORT, SQL_INTEGER, EPrimitiveType::Uint32) { - builder.OptionalUint32(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_INTEGER, EPrimitiveType::Uint32) { - builder.OptionalUint32(static_cast(data)); } -REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_SMALLINT, EPrimitiveType::Int16) { - builder.OptionalInt16(static_cast(data)); +bool IsNull(const TBoundParam& param) { + return param.StrLenOrIndPtr && *param.StrLenOrIndPtr == SQL_NULL_DATA; } -REGISTER_CONVERTER(SQL_C_LONG, SQL_SMALLINT, EPrimitiveType::Int16) { - builder.OptionalInt16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SLONG, SQL_SMALLINT, EPrimitiveType::Int16) { - builder.OptionalInt16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SHORT, SQL_SMALLINT, EPrimitiveType::Int16) { - builder.OptionalInt16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_TINYINT, SQL_SMALLINT, EPrimitiveType::Int16) { - builder.OptionalInt16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_SMALLINT, EPrimitiveType::Uint16) { - builder.OptionalUint16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_ULONG, SQL_SMALLINT, EPrimitiveType::Uint16) { - builder.OptionalUint16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_USHORT, SQL_SMALLINT, EPrimitiveType::Uint16) { - builder.OptionalUint16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_SMALLINT, EPrimitiveType::Uint16) { - builder.OptionalUint16(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SBIGINT, SQL_TINYINT, EPrimitiveType::Int8) { - builder.OptionalInt8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_LONG, SQL_TINYINT, EPrimitiveType::Int8) { - builder.OptionalInt8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SLONG, SQL_TINYINT, EPrimitiveType::Int8) { - builder.OptionalInt8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_SHORT, SQL_TINYINT, EPrimitiveType::Int8) { - builder.OptionalInt8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_TINYINT, SQL_TINYINT, EPrimitiveType::Int8) { - builder.OptionalInt8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UBIGINT, SQL_TINYINT, EPrimitiveType::Uint8) { - builder.OptionalUint8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_ULONG, SQL_TINYINT, EPrimitiveType::Uint8) { - builder.OptionalUint8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_USHORT, SQL_TINYINT, EPrimitiveType::Uint8) { - builder.OptionalUint8(static_cast(data)); -} - -REGISTER_CONVERTER(SQL_C_UTINYINT, SQL_TINYINT, EPrimitiveType::Uint8) { - builder.OptionalUint8(static_cast(data)); -} - -// Floating point types - -REGISTER_CONVERTER(SQL_C_FLOAT, SQL_REAL, EPrimitiveType::Float) { - builder.OptionalFloat(data); -} - -REGISTER_CONVERTER(SQL_C_DOUBLE, SQL_FLOAT, EPrimitiveType::Double) { - builder.OptionalDouble(data); -} - -REGISTER_CONVERTER(SQL_C_DOUBLE, SQL_DOUBLE, EPrimitiveType::Double) { - builder.OptionalDouble(data); -} - -// String types - -REGISTER_CONVERTER(SQL_C_CHAR, SQL_CHAR, EPrimitiveType::Utf8) { - builder.OptionalUtf8(std::move(data)); -} - -REGISTER_CONVERTER(SQL_C_CHAR, SQL_VARCHAR, EPrimitiveType::Utf8) { - builder.OptionalUtf8(std::move(data)); -} - -REGISTER_CONVERTER(SQL_C_CHAR, SQL_LONGVARCHAR, EPrimitiveType::Utf8) { - builder.OptionalUtf8(std::move(data)); -} - -// Binary types - -REGISTER_CONVERTER(SQL_C_BINARY, SQL_BINARY, EPrimitiveType::String) { - builder.OptionalString(std::move(data)); -} - -REGISTER_CONVERTER(SQL_C_BINARY, SQL_VARBINARY, EPrimitiveType::String) { - builder.OptionalString(std::move(data)); -} - -REGISTER_CONVERTER(SQL_C_BINARY, SQL_LONGVARBINARY, EPrimitiveType::String) { - builder.OptionalString(std::move(data)); -} - -#undef REGISTER_CONVERTER +} // namespace SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder) { - auto converter = TConverterRegistry::GetInstance().GetConverter(param.ValueType, param.ParameterType); - if (!converter) { - return SQL_ERROR; + const auto primitive = ParameterPrimitive(param.ParameterType); + if (!primitive) return SQL_ERROR; + if (IsNull(param)) { + builder.EmptyOptional(TTypeBuilder().Primitive(*primitive).Build()).Build(); + return SQL_SUCCESS; } - converter->AddToBuilder(param, builder); + if (param.ParameterType == SQL_BIGINT || param.ParameterType == SQL_INTEGER + || param.ParameterType == SQL_SMALLINT || param.ParameterType == SQL_TINYINT + || param.ParameterType == SQL_BIT) { + const auto value = ReadInteger(param); + if (!value) return SQL_ERROR; + switch (param.ParameterType) { + case SQL_BIGINT: builder.OptionalInt64(*value); break; + case SQL_INTEGER: + if (!FitsInt32(*value)) { SetNumericOutOfRange(); return SQL_ERROR; } + builder.OptionalInt32(static_cast(*value)); break; + case SQL_SMALLINT: + if (!FitsInt16(*value)) { SetNumericOutOfRange(); return SQL_ERROR; } + builder.OptionalInt16(static_cast(*value)); break; + case SQL_TINYINT: + if (*value < INT8_MIN || *value > INT8_MAX) { SetNumericOutOfRange(); return SQL_ERROR; } + builder.OptionalInt8(static_cast(*value)); break; + case SQL_BIT: + if (*value != 0 && *value != 1) { SetNumericOutOfRange(); return SQL_ERROR; } + builder.OptionalBool(*value != 0); break; + } + } else if (param.ParameterType == SQL_REAL) { + if (param.ValueType != SQL_C_FLOAT || !param.ParameterValuePtr) return SQL_ERROR; + builder.OptionalFloat(*static_cast(param.ParameterValuePtr)); + } else if (param.ParameterType == SQL_FLOAT || param.ParameterType == SQL_DOUBLE) { + if (param.ValueType != SQL_C_DOUBLE || !param.ParameterValuePtr) return SQL_ERROR; + builder.OptionalDouble(*static_cast(param.ParameterValuePtr)); + } else { + const auto bytes = ReadBytes(param); + if (!bytes) return SQL_ERROR; + if (param.ValueType == SQL_C_CHAR + && (param.ParameterType == SQL_CHAR || param.ParameterType == SQL_VARCHAR + || param.ParameterType == SQL_LONGVARCHAR)) { + builder.OptionalUtf8(*bytes); + } else if (param.ValueType == SQL_C_BINARY + && (param.ParameterType == SQL_BINARY || param.ParameterType == SQL_VARBINARY + || param.ParameterType == SQL_LONGVARBINARY)) { + builder.OptionalString(*bytes); + } else { + return SQL_ERROR; + } + } + builder.Build(); return SQL_SUCCESS; } -SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) { +SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, + SQLLEN bufferLength, SQLLEN* strLenOrInd, SQLLEN* offset) { LastConvertSqlState = nullptr; + if (bufferLength < 0) { + LastConvertSqlState = "HY090"; + return SQL_ERROR; + } if (parser.IsNull()) { - if (strLenOrInd) { - *strLenOrInd = SQL_NULL_DATA; + if (!strLenOrInd) { + LastConvertSqlState = "22002"; + return SQL_ERROR; } + *strLenOrInd = SQL_NULL_DATA; return SQL_SUCCESS; } - if (parser.GetKind() == TTypeParser::ETypeKind::Optional) { parser.OpenOptional(); - SQLRETURN ret = ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); + const SQLRETURN result = ConvertColumn( + parser, targetType, targetValue, bufferLength, strLenOrInd, offset); parser.CloseOptional(); - return ret; + return result; } - - if (parser.GetKind() != TTypeParser::ETypeKind::Primitive) { - return SQL_ERROR; + if (parser.GetKind() != TTypeParser::ETypeKind::Primitive) return SQL_ERROR; + const EPrimitiveType ydbType = parser.GetPrimitiveType(); + + if (targetType == SQL_C_SHORT || targetType == SQL_C_SSHORT + || targetType == SQL_C_LONG || targetType == SQL_C_SLONG + || targetType == SQL_C_SBIGINT || targetType == SQL_C_BIT) { + const auto raw = GetAsInt64(parser, ydbType); + if (!raw) return SQL_ERROR; + if (targetType == SQL_C_SHORT || targetType == SQL_C_SSHORT) { + if (!FitsInt16(*raw)) { SetNumericOutOfRange(); return SQL_ERROR; } + if (targetValue) *static_cast(targetValue) = static_cast(*raw); + if (strLenOrInd) *strLenOrInd = sizeof(SQLSMALLINT); + } else if (targetType == SQL_C_LONG || targetType == SQL_C_SLONG) { + if (!FitsInt32(*raw)) { SetNumericOutOfRange(); return SQL_ERROR; } + if (targetValue) *static_cast(targetValue) = static_cast(*raw); + if (strLenOrInd) *strLenOrInd = sizeof(SQLINTEGER); + } else if (targetType == SQL_C_SBIGINT) { + if (targetValue) *static_cast(targetValue) = *raw; + if (strLenOrInd) *strLenOrInd = sizeof(SQLBIGINT); + } else { + if (*raw != 0 && *raw != 1) { SetNumericOutOfRange(); return SQL_ERROR; } + if (targetValue) *static_cast(targetValue) = *raw != 0; + if (strLenOrInd) *strLenOrInd = sizeof(SQLCHAR); + } + return SQL_SUCCESS; } + if (targetType == SQL_C_DOUBLE) { + double value; + if (ydbType == EPrimitiveType::Double) value = parser.GetDouble(); + else if (ydbType == EPrimitiveType::Float) value = parser.GetFloat(); + else return SQL_ERROR; + if (targetValue) *static_cast(targetValue) = value; + if (strLenOrInd) *strLenOrInd = sizeof(SQLDOUBLE); + return SQL_SUCCESS; + } + if (targetType != SQL_C_CHAR) return SQL_ERROR; - EPrimitiveType ydbType = parser.GetPrimitiveType(); - - switch (targetType) { - case SQL_C_SHORT: - case SQL_C_SSHORT: - { - const auto raw = GetAsInt64(parser, ydbType); - if (!raw) { - return SQL_ERROR; - } - if (!FitsInt16(*raw)) { - SetNumericOutOfRange(); - return SQL_ERROR; - } - if (targetValue) { - *reinterpret_cast(targetValue) = static_cast(*raw); - } - if (strLenOrInd) { - *strLenOrInd = sizeof(SQLSMALLINT); - } - return SQL_SUCCESS; + std::string text; + switch (ydbType) { + case EPrimitiveType::Utf8: text = parser.GetUtf8(); break; + case EPrimitiveType::String: text = parser.GetString(); break; + case EPrimitiveType::Json: text = parser.GetJson(); break; + case EPrimitiveType::JsonDocument: text = parser.GetJsonDocument(); break; + case EPrimitiveType::Bool: text = parser.GetBool() ? "1" : "0"; break; + case EPrimitiveType::Int8: text = std::to_string(parser.GetInt8()); break; + case EPrimitiveType::Uint8: text = std::to_string(parser.GetUint8()); break; + case EPrimitiveType::Int16: text = std::to_string(parser.GetInt16()); break; + case EPrimitiveType::Uint16: text = std::to_string(parser.GetUint16()); break; + case EPrimitiveType::Int32: text = std::to_string(parser.GetInt32()); break; + case EPrimitiveType::Uint32: text = std::to_string(parser.GetUint32()); break; + case EPrimitiveType::Int64: text = std::to_string(parser.GetInt64()); break; + case EPrimitiveType::Uint64: text = std::to_string(parser.GetUint64()); break; + case EPrimitiveType::Float: text = std::to_string(parser.GetFloat()); break; + case EPrimitiveType::Double: text = std::to_string(parser.GetDouble()); break; + case EPrimitiveType::Date: { + const TString value = parser.GetDate().FormatGmTime("%Y-%m-%d"); + text.assign(value.data(), value.size()); break; } - case SQL_C_SLONG: - case SQL_C_LONG: - { - const auto raw = GetAsInt64(parser, ydbType); - if (!raw) { - return SQL_ERROR; - } - if (!FitsInt32(*raw)) { - SetNumericOutOfRange(); - return SQL_ERROR; - } - if (targetValue) { - *reinterpret_cast(targetValue) = static_cast(*raw); - } - if (strLenOrInd) { - *strLenOrInd = sizeof(int32_t); - } - return SQL_SUCCESS; + case EPrimitiveType::Date32: { + const auto days = parser.GetDate32().time_since_epoch().count(); + if (days < 0) return SQL_ERROR; + const TString value = TInstant::Days(static_cast(days)).FormatGmTime("%Y-%m-%d"); + text.assign(value.data(), value.size()); break; } - case SQL_C_SBIGINT: - { - const auto raw = GetAsInt64(parser, ydbType); - if (!raw) { - return SQL_ERROR; - } - if (targetValue) { - *reinterpret_cast(targetValue) = static_cast(*raw); - } - if (strLenOrInd) { - *strLenOrInd = sizeof(SQLBIGINT); - } - return SQL_SUCCESS; + case EPrimitiveType::Datetime: { + const TString value = parser.GetDatetime().FormatGmTime("%Y-%m-%d %H:%M:%S"); + text.assign(value.data(), value.size()); break; } - case SQL_C_DOUBLE: - { - double v = 0.0; - switch (ydbType) { - case EPrimitiveType::Double: v = parser.GetDouble(); break; - case EPrimitiveType::Float: v = parser.GetFloat(); break; - default: return SQL_ERROR; - } - if (targetValue) { - *reinterpret_cast(targetValue) = v; - } - if (strLenOrInd) { - *strLenOrInd = sizeof(double); - } - return SQL_SUCCESS; + case EPrimitiveType::Datetime64: { + const auto seconds = parser.GetDatetime64().time_since_epoch().count(); + if (seconds < 0) return SQL_ERROR; + const TString value = TInstant::Seconds(static_cast(seconds)) + .FormatGmTime("%Y-%m-%d %H:%M:%S"); + text.assign(value.data(), value.size()); break; } - case SQL_C_CHAR: - { - std::string str; - switch (ydbType) { - case EPrimitiveType::Utf8: str = parser.GetUtf8(); break; - case EPrimitiveType::String: str = parser.GetString(); break; - case EPrimitiveType::Json: str = parser.GetJson(); break; - case EPrimitiveType::JsonDocument: str = parser.GetJsonDocument(); break; - case EPrimitiveType::Date: { - const TString t = parser.GetDate().FormatGmTime("%Y-%m-%d"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::Date32: { - const auto days = parser.GetDate32().time_since_epoch(); - if (days.count() < 0) { - return SQL_ERROR; - } - const TString t = - TInstant::Days(static_cast(days.count())).FormatGmTime("%Y-%m-%d"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::Datetime: { - const TString t = parser.GetDatetime().FormatGmTime("%Y-%m-%d %H:%M:%S"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::Datetime64: { - const auto secs = parser.GetDatetime64().time_since_epoch(); - if (secs.count() < 0) { - return SQL_ERROR; - } - const TString t = TInstant::Seconds(static_cast(static_cast(secs.count()))) - .FormatGmTime("%Y-%m-%d %H:%M:%S"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::Timestamp: { - const TString t = parser.GetTimestamp().FormatGmTime("%Y-%m-%d %H:%M:%S"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::Timestamp64: { - const auto micros = parser.GetTimestamp64().time_since_epoch(); - if (micros.count() < 0) { - return SQL_ERROR; - } - const TString t = - TInstant::MicroSeconds(static_cast(static_cast(micros.count()))) - .FormatGmTime("%Y-%m-%d %H:%M:%S"); - str.assign(t.data(), t.size()); - break; - } - case EPrimitiveType::TzDate: str = parser.GetTzDate(); break; - case EPrimitiveType::TzDatetime: str = parser.GetTzDatetime(); break; - case EPrimitiveType::TzTimestamp: str = parser.GetTzTimestamp(); break; - default: return SQL_ERROR; - } - SQLLEN len = str.size(); - if (targetValue && bufferLength > 0) { - SQLLEN copyLen = std::min(len, bufferLength - 1); - memcpy(targetValue, str.data(), copyLen); - reinterpret_cast(targetValue)[copyLen] = 0; - } - if (strLenOrInd) { - *strLenOrInd = len; - } - if (targetValue && bufferLength > 0 && len >= static_cast(bufferLength)) { - return SQL_SUCCESS_WITH_INFO; - } - return SQL_SUCCESS; + case EPrimitiveType::Timestamp: { + const TString value = parser.GetTimestamp().FormatGmTime("%Y-%m-%d %H:%M:%S"); + text.assign(value.data(), value.size()); break; } - case SQL_C_BIT: - { - const auto raw = GetAsInt64(parser, ydbType); - if (!raw) { - return SQL_ERROR; - } - if (*raw != 0 && *raw != 1) { - SetNumericOutOfRange(); - return SQL_ERROR; - } - const char v = *raw != 0 ? 1 : 0; - if (targetValue) { - *reinterpret_cast(targetValue) = v; - } - if (strLenOrInd) { - *strLenOrInd = sizeof(char); - } - return SQL_SUCCESS; + case EPrimitiveType::Timestamp64: { + const auto micros = parser.GetTimestamp64().time_since_epoch().count(); + if (micros < 0) return SQL_ERROR; + const TString value = TInstant::MicroSeconds(static_cast(micros)) + .FormatGmTime("%Y-%m-%d %H:%M:%S"); + text.assign(value.data(), value.size()); break; } - default: - return SQL_ERROR; + case EPrimitiveType::TzDate: text = parser.GetTzDate(); break; + case EPrimitiveType::TzDatetime: text = parser.GetTzDatetime(); break; + case EPrimitiveType::TzTimestamp: text = parser.GetTzTimestamp(); break; + default: return SQL_ERROR; + } + + if (offset && *offset < 0) return SQL_NO_DATA; + const SQLLEN start = offset ? *offset : 0; + const SQLLEN remaining = static_cast(text.size()) - start; + if (targetValue && bufferLength > 0) { + const SQLLEN copied = std::min(remaining, bufferLength - 1); + std::memcpy(targetValue, text.data() + start, static_cast(copied)); + static_cast(targetValue)[copied] = 0; + if (offset) *offset = copied == remaining ? -1 : start + copied; } + if (strLenOrInd) *strLenOrInd = remaining; + return targetValue && bufferLength > 0 && remaining >= bufferLength + ? SQL_SUCCESS_WITH_INFO + : SQL_SUCCESS; } const char* ConsumeLastConvertSqlState() { @@ -584,5 +299,4 @@ const char* ConsumeLastConvertSqlState() { return result; } -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/convert.h b/odbc/src/utils/convert.h index 27f14cc3f3a..70e098e2e68 100644 --- a/odbc/src/utils/convert.h +++ b/odbc/src/utils/convert.h @@ -11,9 +11,9 @@ namespace NYdb { namespace NOdbc { SQLRETURN ConvertParam(const TBoundParam& param, TParamValueBuilder& builder); -SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); +SQLRETURN ConvertColumn(TValueParser& parser, SQLSMALLINT targetType, SQLPOINTER targetValue, + SQLLEN bufferLength, SQLLEN* strLenOrInd, SQLLEN* offset = nullptr); const char* ConsumeLastConvertSqlState(); } // namespace NOdbc } // namespace NYdb - diff --git a/odbc/src/utils/cursor.cpp b/odbc/src/utils/cursor.cpp index d7e521b6727..b145639b3f2 100644 --- a/odbc/src/utils/cursor.cpp +++ b/odbc/src/utils/cursor.cpp @@ -1,6 +1,4 @@ #include "cursor.h" -#include "status_util.h" - #include "convert.h" #include "types.h" @@ -9,70 +7,30 @@ namespace NYdb { namespace NOdbc { -NYdb::TStatus PrefetchFirstResultSet( - NQuery::TExecuteQueryIterator& iterator, - std::optional* resultSet) { - resultSet->reset(); - while (true) { - auto part = iterator.ReadNext().ExtractValueSync(); - if (part.EOS()) { - break; - } - if (!part.IsSuccess()) { - return StatusFrom(part); - } - if (part.HasResultSet()) { - resultSet->emplace(part.ExtractResultSet()); - return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); - } - } - return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); -} - class TExecCursor : public ICursor { public: - TExecCursor(IBindingFiller* bindingFiller, NQuery::TExecuteQueryIterator iterator, - std::optional firstResultSet) - : BindingFiller_(bindingFiller) - , Iterator_(std::move(iterator)) - { - if (firstResultSet) { - InitResultSet(std::move(*firstResultSet)); + explicit TExecCursor(TResultSet resultSet) + : Parser_(resultSet) { + for (const auto& col : resultSet.GetColumnsMeta()) { + const SQLSMALLINT sqlType = GetTypeId(col.Type); + Columns_.push_back({col.Name, sqlType, GetColumnSize(sqlType), IsNullable(col.Type), + GetDecimalDigits(col.Type).value_or(0)}); } } bool Fetch() override { - while (true) { - if (ResultSetParser_) { - if (ResultSetParser_->TryNextRow()) { - BindingFiller_->FillBoundColumns(); - return true; - } - ResultSetParser_.reset(); - } - NQuery::TExecuteQueryPart part = Iterator_.ReadNext().ExtractValueSync(); - if (part.EOS()) { - return false; - } - if (!part.IsSuccess()) { - BindingFiller_->OnStreamPartError(part); - return false; - } - if (part.HasResultSet()) { - InitResultSet(part.ExtractResultSet()); - } - } + return Parser_.TryNextRow(); } SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { - if (!ResultSetParser_) { - return SQL_NO_DATA; - } - if (columnNumber < 1 || columnNumber > ResultSetParser_->ColumnsCount()) { + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd, + SQLLEN* offset) override { + if (columnNumber < 1 || columnNumber > Parser_.ColumnsCount()) { return SQL_ERROR; } - return ConvertColumn(ResultSetParser_->ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd); + return ConvertColumn( + Parser_.ColumnParser(columnNumber - 1), targetType, targetValue, bufferLength, strLenOrInd, + offset); } const std::vector& GetColumnMeta() const override { @@ -80,35 +38,14 @@ class TExecCursor : public ICursor { } private: - void InitResultSet(TResultSet resultSet) { - Columns_.clear(); - FillColumnsMeta(resultSet); - ResultSetParser_ = std::make_unique(std::move(resultSet)); - } - - void FillColumnsMeta(const TResultSet& resultSet) { - for (const auto& col : resultSet.GetColumnsMeta()) { - const SQLSMALLINT sqlType = GetTypeId(col.Type); - Columns_.push_back(TColumnMeta{ - col.Name, - sqlType, - GetColumnSize(sqlType), - IsNullable(col.Type), - GetDecimalDigits(col.Type).value_or(0)}); - } - } - - IBindingFiller* BindingFiller_; - NQuery::TExecuteQueryIterator Iterator_; - std::unique_ptr ResultSetParser_; + TResultSetParser Parser_; std::vector Columns_; }; class TVirtualCursor : public ICursor { public: - TVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) - : BindingFiller_(bindingFiller) - , Columns_(columns) + TVirtualCursor(const std::vector& columns, const TTable& table) + : Columns_(columns) , Table_(table) {} @@ -117,12 +54,12 @@ class TVirtualCursor : public ICursor { if (Cursor_ >= static_cast(Table_.size())) { return false; } - BindingFiller_->FillBoundColumns(); return true; } SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) override { + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd, + SQLLEN* offset) override { if (Cursor_ >= static_cast(Table_.size())) { return SQL_NO_DATA; } @@ -130,7 +67,7 @@ class TVirtualCursor : public ICursor { return SQL_ERROR; } TValueParser parser{Table_[Cursor_][columnNumber - 1]}; - return ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd); + return ConvertColumn(parser, targetType, targetValue, bufferLength, strLenOrInd, offset); } const std::vector& GetColumnMeta() const override { @@ -138,30 +75,19 @@ class TVirtualCursor : public ICursor { } private: - IBindingFiller* BindingFiller_; std::vector Columns_; TTable Table_; int64_t Cursor_ = -1; }; -TExecCursorCreateResult TryCreateExecCursor( - IBindingFiller* bindingFiller, - NQuery::TExecuteQueryIterator iterator) { - std::optional firstResultSet; - const NYdb::TStatus prefetchStatus = PrefetchFirstResultSet(iterator, &firstResultSet); - if (!prefetchStatus.IsSuccess()) { - return {prefetchStatus, nullptr}; - } - if (!firstResultSet) { - return {NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()), nullptr}; - } - return { - NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()), - std::make_unique(bindingFiller, std::move(iterator), std::move(firstResultSet))}; +std::unique_ptr CreateExecCursor(const NQuery::TExecuteQueryResult& result) { + return result.GetResultSets().empty() + ? nullptr + : std::make_unique(result.GetResultSet(0)); } -std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table) { - return std::make_unique(bindingFiller, columns, table); +std::unique_ptr CreateVirtualCursor(const std::vector& columns, const TTable& table) { + return std::make_unique(columns, table); } } // namespace NOdbc diff --git a/odbc/src/utils/cursor.h b/odbc/src/utils/cursor.h index 4fa2682e59b..3032eb050f9 100644 --- a/odbc/src/utils/cursor.h +++ b/odbc/src/utils/cursor.h @@ -30,20 +30,16 @@ class ICursor { virtual ~ICursor() = default; virtual bool Fetch() = 0; virtual SQLRETURN GetData(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, - SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd) = 0; + SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd, + SQLLEN* offset = nullptr) = 0; virtual const std::vector& GetColumnMeta() const = 0; }; -struct TExecCursorCreateResult { - NYdb::TStatus Status; - std::unique_ptr Cursor; -}; - -TExecCursorCreateResult TryCreateExecCursor( - IBindingFiller* bindingFiller, - NYdb::NQuery::TExecuteQueryIterator iterator); +std::unique_ptr CreateExecCursor(const NYdb::NQuery::TExecuteQueryResult& result); -std::unique_ptr CreateVirtualCursor(IBindingFiller* bindingFiller, const std::vector& columns, const TTable& table); +std::unique_ptr CreateVirtualCursor( + const std::vector& columns, + const TTable& table); } // namespace NOdbc } // namespace NYdb diff --git a/odbc/src/utils/error_manager.cpp b/odbc/src/utils/error_manager.cpp index 8e540e20c83..86eedb05b51 100644 --- a/odbc/src/utils/error_manager.cpp +++ b/odbc/src/utils/error_manager.cpp @@ -6,8 +6,7 @@ #include #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { namespace { struct OdbcErrorMapping { @@ -21,8 +20,8 @@ namespace { {EStatus::BAD_REQUEST, {"42000", "Syntax error or access rule violation", SQL_ERROR}}, {EStatus::UNAUTHORIZED, {"28000", "Invalid authorization specification", SQL_ERROR}}, {EStatus::INTERNAL_ERROR, {"HY000", "General error", SQL_ERROR}}, - {EStatus::ABORTED, {"25000", "Invalid transaction state", SQL_ERROR}}, - {EStatus::UNAVAILABLE, {"08001", "Client unable to establish connection", SQL_ERROR}}, + {EStatus::ABORTED, {"40001", "Serialization failure", SQL_ERROR}}, + {EStatus::UNAVAILABLE, {"08S01", "Communication link failure", SQL_ERROR}}, {EStatus::OVERLOADED, {"HY000", "General error - server overloaded", SQL_ERROR}}, {EStatus::SCHEME_ERROR, {"42S02", "Base table or view not found", SQL_ERROR}}, {EStatus::GENERIC_ERROR, {"HY000", "General error", SQL_ERROR}}, @@ -33,11 +32,11 @@ namespace { {EStatus::NOT_FOUND, {"02000", "No data found", SQL_NO_DATA}}, {EStatus::SESSION_EXPIRED, {"08003", "Connection does not exist", SQL_ERROR}}, {EStatus::CANCELLED, {"HY008", "Operation canceled", SQL_ERROR}}, - {EStatus::UNDETERMINED, {"HY000", "General error", SQL_ERROR}}, + {EStatus::UNDETERMINED, {"40003", "Statement completion unknown", SQL_ERROR}}, {EStatus::UNSUPPORTED, {"HYC00", "Optional feature not implemented", SQL_ERROR}}, {EStatus::SESSION_BUSY, {"HY000", "General error - session busy", SQL_ERROR}}, // Transport errors - {EStatus::TRANSPORT_UNAVAILABLE, {"08001", "Client unable to establish connection", SQL_ERROR}}, + {EStatus::TRANSPORT_UNAVAILABLE, {"08S01", "Communication link failure", SQL_ERROR}}, {EStatus::CLIENT_RESOURCE_EXHAUSTED, {"HY000", "General error - resource exhausted", SQL_ERROR}}, {EStatus::CLIENT_DEADLINE_EXCEEDED, {"HYT00", "Timeout expired", SQL_ERROR}}, {EStatus::CLIENT_INTERNAL_ERROR, {"HY000", "General error", SQL_ERROR}}, @@ -222,5 +221,4 @@ SQLRETURN HandleOdbcExceptions( } } -} // namespace NOdbc -} // namespace NYdb \ No newline at end of file +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/error_manager.h b/odbc/src/utils/error_manager.h index da25cd412ed..6677f3fdcdf 100644 --- a/odbc/src/utils/error_manager.h +++ b/odbc/src/utils/error_manager.h @@ -6,11 +6,11 @@ #include #include #include +#include #include -namespace NYdb { -namespace NOdbc { +namespace NYdb::NOdbc { struct TErrorInfo { std::string SqlState; @@ -65,6 +65,7 @@ class TErrorManager { SQLRETURN AddError(const TStatus& status); void ClearErrors(); + std::recursive_mutex& GetMutex() const noexcept { return Mutex_; } void SetLastReturnCode(SQLRETURN code) { LastReturnCode_ = code; @@ -79,6 +80,7 @@ class TErrorManager { SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr); private: + mutable std::recursive_mutex Mutex_; TErrorList Errors_; SQLRETURN LastReturnCode_ = SQL_SUCCESS; }; @@ -94,6 +96,7 @@ SQLRETURN HandleOdbcExceptionsConsuming(SQLHANDLE handlePtr, std::function(handlePtr); + handle->ClearErrors(); try { return func(handle); @@ -108,12 +111,28 @@ SQLRETURN HandleOdbcExceptionsConsuming(SQLHANDLE handlePtr, std::function +SQLRETURN HandleOdbcDiagnostics(SQLHANDLE handlePtr, std::function&& func) { + if (!handlePtr) { + return SQL_INVALID_HANDLE; + } + auto* handle = static_cast(handlePtr); + std::lock_guard lock(handle->GetMutex()); + try { + return func(handle); + } catch (...) { + return SQL_ERROR; + } +} + template SQLRETURN HandleOdbcExceptions(SQLHANDLE handlePtr, std::function&& func) { if (!handlePtr) { return SQL_INVALID_HANDLE; } auto handle = static_cast(handlePtr); + std::lock_guard lock(handle->GetMutex()); + handle->ClearErrors(); try { const SQLRETURN ret = func(handle); @@ -135,5 +154,4 @@ SQLRETURN HandleOdbcExceptions( std::function&& func, ENullInputHandlePolicy nullInputPolicy = ENullInputHandlePolicy::Reject); -} // namespace NOdbc -} // namespace NYdb +} // namespace NYdb::NOdbc diff --git a/odbc/src/utils/sql_type_map.cpp b/odbc/src/utils/sql_type_map.cpp index 2685c6cbe23..c8ba5f19af3 100644 --- a/odbc/src/utils/sql_type_map.cpp +++ b/odbc/src/utils/sql_type_map.cpp @@ -1,106 +1,68 @@ #include "sql_type_map.h" #include +#include #include #include -#include -#include namespace NYdb::NOdbc { - namespace { -std::string ToUpperAscii(std::string_view sv) { - std::string upper; - upper.resize(sv.size()); - std::transform(sv.begin(), sv.end(), upper.begin(), [](unsigned char byte) { +constexpr std::array TypeSpecs{ + TSqlTypeSpec{SQL_BIGINT, "BIGINT", "Int64", 19, true}, + TSqlTypeSpec{SQL_INTEGER, "INTEGER", "Int32", 10, true}, + TSqlTypeSpec{SQL_SMALLINT, "SMALLINT", "Int16", 5, true}, + TSqlTypeSpec{SQL_DOUBLE, "DOUBLE", "Double", 15, true}, + TSqlTypeSpec{SQL_REAL, "REAL", "Float", 7, true}, + TSqlTypeSpec{SQL_VARCHAR, "VARCHAR", "Utf8", 255, true}, + TSqlTypeSpec{SQL_CHAR, "CHAR", "Utf8", 255, true}, + TSqlTypeSpec{SQL_LONGVARCHAR, "LONGVARCHAR", "Utf8", 4096, false}, + TSqlTypeSpec{SQL_WCHAR, "WCHAR", "Utf8", 255, false}, + TSqlTypeSpec{SQL_WVARCHAR, "WVARCHAR", "Utf8", 255, false}, + TSqlTypeSpec{SQL_WLONGVARCHAR, "WLONGVARCHAR", "Utf8", 4096, false}, + TSqlTypeSpec{SQL_BIT, "BIT", "Bool", 1, false}, + TSqlTypeSpec{SQL_TINYINT, "TINYINT", "Int8", 3, false}, + TSqlTypeSpec{SQL_FLOAT, "FLOAT", "Double", 15, false}, + TSqlTypeSpec{SQL_DECIMAL, "DECIMAL", "Decimal(22, 9)", 22, false}, + TSqlTypeSpec{SQL_NUMERIC, "NUMERIC", "Decimal(22, 9)", 22, false}, + TSqlTypeSpec{SQL_BINARY, "BINARY", "String", 4096, false}, + TSqlTypeSpec{SQL_VARBINARY, "VARBINARY", "String", 4096, false}, + TSqlTypeSpec{SQL_LONGVARBINARY, "LONGVARBINARY", "String", 4096, false}, + TSqlTypeSpec{SQL_TYPE_DATE, "DATE", "Date", 10, false}, + TSqlTypeSpec{SQL_TYPE_TIME, "TIME", "Time", 8, false}, + TSqlTypeSpec{SQL_TYPE_TIMESTAMP, "TIMESTAMP", "Datetime", 26, false}, +}; + +std::string ToUpperAscii(std::string_view value) { + std::string upper(value); + std::ranges::transform(upper, upper.begin(), [](unsigned char byte) { return static_cast(std::toupper(byte)); }); return upper; } -const std::unordered_map& SqlTypeTokenToYqlMap() { - static const std::unordered_map kMap = { - {"CHAR", "Utf8"}, - {"VARCHAR", "Utf8"}, - {"LONGVARCHAR", "Utf8"}, - {"WCHAR", "Utf8"}, - {"WVARCHAR", "Utf8"}, - {"WLONGVARCHAR", "Utf8"}, - {"BIT", "Bool"}, - {"TINYINT", "Int8"}, - {"SMALLINT", "Int16"}, - {"INTEGER", "Int32"}, - {"BIGINT", "Int64"}, - {"REAL", "Float"}, - {"FLOAT", "Double"}, - {"DOUBLE", "Double"}, - {"DECIMAL", "Decimal(22, 9)"}, - {"NUMERIC", "Decimal(22, 9)"}, - {"BINARY", "String"}, - {"VARBINARY", "String"}, - {"LONGVARBINARY", "String"}, - {"DATE", "Date"}, - {"TIME", "Time"}, - {"TIMESTAMP", "Datetime"}, - {"TYPE_DATE", "Date"}, - {"TYPE_TIME", "Time"}, - {"TYPE_TIMESTAMP", "Datetime"}, - }; - return kMap; -} +} // namespace -const std::unordered_map& OdbcSqlTypeTokens() { - static const std::unordered_map kMap = { - {SQL_CHAR, "CHAR"}, - {SQL_VARCHAR, "VARCHAR"}, - {SQL_LONGVARCHAR, "LONGVARCHAR"}, - {SQL_WCHAR, "WCHAR"}, - {SQL_WVARCHAR, "WVARCHAR"}, - {SQL_WLONGVARCHAR, "WLONGVARCHAR"}, - {SQL_BIT, "BIT"}, - {SQL_TINYINT, "TINYINT"}, - {SQL_SMALLINT, "SMALLINT"}, - {SQL_INTEGER, "INTEGER"}, - {SQL_BIGINT, "BIGINT"}, - {SQL_REAL, "REAL"}, - {SQL_FLOAT, "FLOAT"}, - {SQL_DOUBLE, "DOUBLE"}, - {SQL_DECIMAL, "DECIMAL"}, - {SQL_NUMERIC, "NUMERIC"}, - {SQL_BINARY, "BINARY"}, - {SQL_VARBINARY, "VARBINARY"}, - {SQL_LONGVARBINARY, "LONGVARBINARY"}, - {SQL_TYPE_DATE, "TYPE_DATE"}, - {SQL_TYPE_TIME, "TYPE_TIME"}, - {SQL_TYPE_TIMESTAMP, "TYPE_TIMESTAMP"}, - }; - return kMap; +std::span GetSqlTypeSpecs() { + return TypeSpecs; } -} // namespace +const TSqlTypeSpec* FindSqlTypeSpec(SQLSMALLINT sqlType) { + const auto it = std::ranges::find(TypeSpecs, sqlType, &TSqlTypeSpec::Type); + return it == TypeSpecs.end() ? nullptr : &*it; +} std::string MapSqlTypeToken(std::string_view sqlType) { std::string key = ToUpperAscii(sqlType); - const std::string kSql = "SQL_"; - if (key.size() > kSql.size() && key.compare(0, kSql.size(), kSql) == 0) { - key.erase(0, kSql.size()); - } - const auto& map = SqlTypeTokenToYqlMap(); - const auto mapped = map.find(key); - if (mapped != map.end()) { - return mapped->second; - } - return key; + if (key.starts_with("SQL_")) key.erase(0, 4); + if (key.starts_with("TYPE_")) key.erase(0, 5); + const auto it = std::ranges::find(TypeSpecs, key, &TSqlTypeSpec::Name); + return it == TypeSpecs.end() ? key : std::string(it->YqlType); } std::string FormatYqlParamDeclareType(SQLSMALLINT sqlType) { - const auto& tokens = OdbcSqlTypeTokens(); - const auto tokenIt = tokens.find(sqlType); - const std::string yql = tokenIt != tokens.end() - ? MapSqlTypeToken(tokenIt->second) - : MapSqlTypeToken(std::to_string(sqlType)); - return yql + '?'; + const TSqlTypeSpec* spec = FindSqlTypeSpec(sqlType); + return (spec ? std::string(spec->YqlType) : std::to_string(sqlType)) + '?'; } } // namespace NYdb::NOdbc diff --git a/odbc/src/utils/sql_type_map.h b/odbc/src/utils/sql_type_map.h index 56549c39208..913064babef 100644 --- a/odbc/src/utils/sql_type_map.h +++ b/odbc/src/utils/sql_type_map.h @@ -3,11 +3,22 @@ #include #include +#include #include #include namespace NYdb::NOdbc { +struct TSqlTypeSpec { + SQLSMALLINT Type; + std::string_view Name; + std::string_view YqlType; + SQLULEN ColumnSize; + bool Advertise; +}; + +std::span GetSqlTypeSpecs(); +const TSqlTypeSpec* FindSqlTypeSpec(SQLSMALLINT sqlType); std::string MapSqlTypeToken(std::string_view sqlType); std::string FormatYqlParamDeclareType(SQLSMALLINT sqlType); diff --git a/odbc/src/utils/type_info_rows.cpp b/odbc/src/utils/type_info_rows.cpp index 8dd62500f9d..24fd54d7d2d 100644 --- a/odbc/src/utils/type_info_rows.cpp +++ b/odbc/src/utils/type_info_rows.cpp @@ -1,30 +1,15 @@ #include "type_info_rows.h" +#include "sql_type_map.h" #include #include +#include #include namespace NYdb::NOdbc { namespace { -struct TTypeInfoSpec { - const char* TypeName; - SQLSMALLINT DataType; - SQLULEN ColumnSize; - SQLSMALLINT Nullable; -}; - -const TTypeInfoSpec kTypeInfoSpecs[] = { - {"bigint", SQL_BIGINT, 19, SQL_NULLABLE}, - {"integer", SQL_INTEGER, 10, SQL_NULLABLE}, - {"smallint", SQL_SMALLINT, 5, SQL_NULLABLE}, - {"double", SQL_DOUBLE, 53, SQL_NULLABLE}, - {"real", SQL_REAL, 24, SQL_NULLABLE}, - {"varchar", SQL_VARCHAR, 255, SQL_NULLABLE}, - {"char", SQL_CHAR, 255, SQL_NULLABLE}, -}; - TValue MakeOptionalInt16(SQLSMALLINT value) { return TValueBuilder().OptionalInt16(value).Build(); } @@ -37,24 +22,28 @@ TValue MakeNullUtf8() { return TValueBuilder().OptionalUtf8(std::nullopt).Build(); } -std::vector MakeTypeInfoRow(const TTypeInfoSpec& spec) { +std::vector MakeTypeInfoRow(const TSqlTypeSpec& spec) { + std::string typeName(spec.Name); + std::ranges::transform(typeName, typeName.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); return { - TValueBuilder().Utf8(spec.TypeName).Build(), - TValueBuilder().Int16(spec.DataType).Build(), + TValueBuilder().Utf8(typeName).Build(), + TValueBuilder().Int16(spec.Type).Build(), MakeOptionalInt32(static_cast(spec.ColumnSize)), MakeNullUtf8(), MakeNullUtf8(), MakeNullUtf8(), - MakeOptionalInt16(spec.Nullable), + MakeOptionalInt16(SQL_NULLABLE), MakeOptionalInt16(SQL_FALSE), MakeOptionalInt16(SQL_PRED_SEARCHABLE), MakeNullUtf8(), MakeOptionalInt16(SQL_FALSE), MakeOptionalInt16(SQL_FALSE), - TValueBuilder().OptionalUtf8(spec.TypeName).Build(), + TValueBuilder().OptionalUtf8(typeName).Build(), MakeOptionalInt16(0), MakeOptionalInt16(0), - MakeOptionalInt16(spec.DataType), + MakeOptionalInt16(spec.Type), MakeOptionalInt16(0), MakeOptionalInt32(10), MakeOptionalInt32(0), @@ -65,8 +54,8 @@ std::vector MakeTypeInfoRow(const TTypeInfoSpec& spec) { TTable BuildTypeInfoRows(SQLSMALLINT dataType) { TTable table; - for (const auto& spec : kTypeInfoSpecs) { - if (dataType != SQL_ALL_TYPES && spec.DataType != dataType) { + for (const TSqlTypeSpec& spec : GetSqlTypeSpecs()) { + if (!spec.Advertise || (dataType != SQL_ALL_TYPES && spec.Type != dataType)) { continue; } table.push_back(MakeTypeInfoRow(spec)); diff --git a/odbc/src/utils/types.cpp b/odbc/src/utils/types.cpp index 3c9c70549fa..d991ca24f6a 100644 --- a/odbc/src/utils/types.cpp +++ b/odbc/src/utils/types.cpp @@ -1,4 +1,5 @@ #include "types.h" +#include "sql_type_map.h" namespace NYdb { namespace NOdbc { @@ -85,32 +86,8 @@ SQLSMALLINT IsNullable(const TType& type) { } SQLULEN GetColumnSize(SQLSMALLINT sqlType) { - switch (sqlType) { - case SQL_BIT: - return 1; - case SQL_TINYINT: - return 3; - case SQL_SMALLINT: - return 5; - case SQL_INTEGER: - return 10; - case SQL_BIGINT: - return 20; - case SQL_REAL: - return 7; - case SQL_DOUBLE: - return 15; - case SQL_TYPE_DATE: - return 10; - case SQL_TYPE_TIMESTAMP: - return 26; - case SQL_GUID: - return 36; - case SQL_VARCHAR: - case SQL_VARBINARY: - default: - return 4096; - } + const TSqlTypeSpec* spec = FindSqlTypeSpec(sqlType); + return spec ? spec->ColumnSize : sqlType == SQL_GUID ? 36 : 4096; } std::optional GetDecimalDigits(const TType& type) { diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 8abcd081832..160517d941e 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -18,4 +18,5 @@ Database=${YDB_ODBC_DSN_DATABASE} ") add_subdirectory(integration) +add_subdirectory(conformance) add_subdirectory(unit) diff --git a/odbc/tests/conformance/CMakeLists.txt b/odbc/tests/conformance/CMakeLists.txt new file mode 100644 index 00000000000..9b336ba4265 --- /dev/null +++ b/odbc/tests/conformance/CMakeLists.txt @@ -0,0 +1,8 @@ +add_odbc_test(NAME odbc-core-conformance_it + SOURCES + core_conformance_it.cpp + LABELS + odbc + core-conformance + linux +) diff --git a/odbc/tests/conformance/README.md b/odbc/tests/conformance/README.md new file mode 100644 index 00000000000..a5b19de0ffc --- /dev/null +++ b/odbc/tests/conformance/README.md @@ -0,0 +1,56 @@ +# ODBC Core conformance audit + +This directory contains a Linux/headless audit for the ODBC 3.x **Core interface +conformance** contract. It deliberately tests only Core requirements; claiming +Level 1 or Level 2 is outside its scope. It is a regression and gap-finding +suite, not an official certification program. + +The executable talks through unixODBC, not through driver internals. It checks: + +- the declared interface level and the complete mandatory function bitmap; +- Driver Manager enumeration entry points; +- Core environment, connection, and statement attributes; +- parameter/row arrays, bind offsets, and conservative array capabilities; +- application descriptor assignment, binding, lifetime, and mandatory fields; +- pre-`SQLExecDirect` bindings and scalar data-at-execution sequencing; +- numeric/text conversion, partial `SQLGetData`, and diagnostic lifetime; +- transaction completion edge semantics; +- standard result shapes for the Core catalog functions. + +The existing integration tests under `../integration` provide deeper behavioral +coverage for connection lifecycle, statement execution, binding and fetching, +data-at-execution, transactions, diagnostics, metadata, and cursor operations. + +## Running + +A YDB server must be available at `localhost:2136`, with database `/local`. + +```bash +cmake --preset release-test-clang +cmake --build build --target odbc-core-conformance_it -j$(nproc) +ctest --test-dir build -L core-conformance --output-on-failure +``` + +Failures are intentional evidence of missing Core behavior. Do not convert a +mandatory failure into a skip unless the cited ODBC conformance contract says +the feature is optional or belongs to a higher level. + +## Open-source solutions evaluated + +Research was refreshed on 2026-07-14. + +| Project | Evaluated revision | Useful parts | Why it was not vendored | +|---|---:|---|---| +| [unixODBC-Test](https://sourceforge.net/projects/unixodbc-test/) | SVN r19 (2018-03-08) | The closest generic corpus: AutoTests, TestFarm, and a `MyODBC3/funccore` API set | GPLv2; the broad AutoTest corpus is ODBC 2-era, requires Qt plus the ODBCTest GUI/gtrtst library, and is not a headless CTest suite. TestFarm's ODBC 3 Core directory contains only handle-allocation tests. | +| [Microsoft ODBCTest](https://github.com/microsoft/ODBCTest/tree/0d629c7e4ff7b01398a5ac71d20c43362d0f43bf) | `0d629c7e` | MIT-licensed interactive API exerciser and reference implementation | Visual Studio/Windows GUI application, interactive rather than an automated Linux conformance corpus. | +| [pyodbc](https://github.com/mkleehammer/pyodbc/tree/9fd386c370288d802ddec58e5580fe1ec9adeade) | `9fd386c3` | Mature real-client compatibility tests | Tests the Python DB-API wrapper and database-specific SQL, not raw driver Core conformance. | +| [nanodbc](https://github.com/nanodbc/nanodbc/tree/fd9b4f551b0f03780168c4b2ba880dcb5777aad4) | `fd9b4f55` | Portable C++ client smoke and type-conversion tests | Tests the nanodbc wrapper and is configured around a SQLite ODBC data source. | + +The audit is implemented locally instead of copying those sources. That keeps +the repository Apache-2.0-only, avoids a GUI/runtime dependency, and makes every +assertion traceable to the current ODBC 3.x Core contract: + +- [Core Interface Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/core-interface-conformance) +- [Function Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/function-conformance) +- [Attribute Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/attribute-conformance) +- [Descriptor Field Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/descriptor-field-conformance) diff --git a/odbc/tests/conformance/core_conformance_it.cpp b/odbc/tests/conformance/core_conformance_it.cpp new file mode 100644 index 00000000000..3dc3c31dc02 --- /dev/null +++ b/odbc/tests/conformance/core_conformance_it.cpp @@ -0,0 +1,1036 @@ +#include "../integration/test_utils.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +bool IsOdbcSuccess(SQLRETURN rc) { + return rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO; +} + +::testing::AssertionResult OdbcSuccess( + SQLRETURN rc, + SQLHANDLE handle, + SQLSMALLINT handleType) { + if (IsOdbcSuccess(rc)) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure() + << "ODBC return code " << rc << ": " << GetOdbcError(handle, handleType); +} + +struct TCoreFunction { + SQLUSMALLINT Id; + const char* Name; +}; + +// ODBC 3.x Core functions from the Microsoft/Open Group conformance table. +// SQLDataSources and SQLDrivers are Driver Manager functions and are exercised +// separately; all entries below must be advertised by the connected driver. +constexpr std::array CoreDriverFunctions{{ + {SQL_API_SQLALLOCHANDLE, "SQLAllocHandle"}, + {SQL_API_SQLBINDCOL, "SQLBindCol"}, + {SQL_API_SQLBINDPARAMETER, "SQLBindParameter"}, + {SQL_API_SQLCANCEL, "SQLCancel"}, + {SQL_API_SQLCLOSECURSOR, "SQLCloseCursor"}, + {SQL_API_SQLCOLATTRIBUTE, "SQLColAttribute"}, + {SQL_API_SQLCOLUMNS, "SQLColumns"}, + {SQL_API_SQLCONNECT, "SQLConnect"}, + {SQL_API_SQLCOPYDESC, "SQLCopyDesc"}, + {SQL_API_SQLDESCRIBECOL, "SQLDescribeCol"}, + {SQL_API_SQLDISCONNECT, "SQLDisconnect"}, + {SQL_API_SQLDRIVERCONNECT, "SQLDriverConnect"}, + {SQL_API_SQLENDTRAN, "SQLEndTran"}, + {SQL_API_SQLEXECDIRECT, "SQLExecDirect"}, + {SQL_API_SQLEXECUTE, "SQLExecute"}, + {SQL_API_SQLFETCH, "SQLFetch"}, + {SQL_API_SQLFETCHSCROLL, "SQLFetchScroll"}, + {SQL_API_SQLFREEHANDLE, "SQLFreeHandle"}, + {SQL_API_SQLFREESTMT, "SQLFreeStmt"}, + {SQL_API_SQLGETCONNECTATTR, "SQLGetConnectAttr"}, + {SQL_API_SQLGETCURSORNAME, "SQLGetCursorName"}, + {SQL_API_SQLGETDATA, "SQLGetData"}, + {SQL_API_SQLGETDESCFIELD, "SQLGetDescField"}, + {SQL_API_SQLGETDESCREC, "SQLGetDescRec"}, + {SQL_API_SQLGETDIAGFIELD, "SQLGetDiagField"}, + {SQL_API_SQLGETDIAGREC, "SQLGetDiagRec"}, + {SQL_API_SQLGETENVATTR, "SQLGetEnvAttr"}, + {SQL_API_SQLGETFUNCTIONS, "SQLGetFunctions"}, + {SQL_API_SQLGETINFO, "SQLGetInfo"}, + {SQL_API_SQLGETSTMTATTR, "SQLGetStmtAttr"}, + {SQL_API_SQLGETTYPEINFO, "SQLGetTypeInfo"}, + {SQL_API_SQLNATIVESQL, "SQLNativeSql"}, + {SQL_API_SQLNUMPARAMS, "SQLNumParams"}, + {SQL_API_SQLNUMRESULTCOLS, "SQLNumResultCols"}, + {SQL_API_SQLPARAMDATA, "SQLParamData"}, + {SQL_API_SQLPREPARE, "SQLPrepare"}, + {SQL_API_SQLPUTDATA, "SQLPutData"}, + {SQL_API_SQLROWCOUNT, "SQLRowCount"}, + {SQL_API_SQLSETCONNECTATTR, "SQLSetConnectAttr"}, + {SQL_API_SQLSETCURSORNAME, "SQLSetCursorName"}, + {SQL_API_SQLSETDESCFIELD, "SQLSetDescField"}, + {SQL_API_SQLSETDESCREC, "SQLSetDescRec"}, + {SQL_API_SQLSETENVATTR, "SQLSetEnvAttr"}, + {SQL_API_SQLSETSTMTATTR, "SQLSetStmtAttr"}, + {SQL_API_SQLSPECIALCOLUMNS, "SQLSpecialColumns"}, + {SQL_API_SQLSTATISTICS, "SQLStatistics"}, + {SQL_API_SQLTABLES, "SQLTables"}, +}}; + +void ExpectDescriptorField( + SQLHDESC desc, + SQLSMALLINT record, + SQLSMALLINT field, + const char* name, + bool characterField = false) { + alignas(std::max_align_t) std::array storage{}; + SQLINTEGER length = 0; + const SQLRETURN rc = SQLGetDescField( + desc, + record, + field, + storage.data(), + characterField ? static_cast(storage.size()) : 0, + &length); + EXPECT_TRUE(OdbcSuccess(rc, desc, SQL_HANDLE_DESC)) << "field " << name; +} + +class OdbcCoreConformance : public ::testing::Test { +protected: + void SetUp() override { + AllocEnvAndConnect(&Env_, &Dbc_); + } + + void TearDown() override { + if (Dbc_ != SQL_NULL_HDBC) { + SQLDisconnect(Dbc_); + SQLFreeHandle(SQL_HANDLE_DBC, Dbc_); + } + if (Env_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, Env_); + } + } + + SQLHSTMT AllocStatement() { + SQLHSTMT stmt = SQL_NULL_HSTMT; + EXPECT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &stmt), SQL_SUCCESS); + return stmt; + } + + SQLHENV Env_ = SQL_NULL_HENV; + SQLHDBC Dbc_ = SQL_NULL_HDBC; +}; + +} // namespace + +TEST_F(OdbcCoreConformance, DeclaresAtLeastCoreInterfaceConformance) { + // SQL_ATTR_ODBC_VERSION is the only Core-level environment attribute. + SQLINTEGER version = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetEnvAttr(Env_, SQL_ATTR_ODBC_VERSION, &version, sizeof(version), nullptr), + Env_, + SQL_HANDLE_ENV)); + EXPECT_EQ(version, SQL_OV_ODBC3); + + SQLUINTEGER level = 0; + SQLSMALLINT length = 0; + const SQLRETURN rc = SQLGetInfo( + Dbc_, SQL_ODBC_INTERFACE_CONFORMANCE, &level, sizeof(level), &length); + ASSERT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)); + EXPECT_GE(level, static_cast(SQL_OIC_CORE)); + EXPECT_EQ(length, sizeof(level)); +} + +TEST_F(OdbcCoreConformance, AdvertisesEveryMandatoryDriverFunction) { + std::array bitmap{}; + ASSERT_TRUE(OdbcSuccess( + SQLGetFunctions(Dbc_, SQL_API_ODBC3_ALL_FUNCTIONS, bitmap.data()), + Dbc_, + SQL_HANDLE_DBC)); + + for (const auto& function : CoreDriverFunctions) { + SQLUSMALLINT supported = SQL_FALSE; + const SQLRETURN rc = SQLGetFunctions(Dbc_, function.Id, &supported); + EXPECT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)) << function.Name; + EXPECT_EQ(supported, SQL_TRUE) << function.Name; + EXPECT_EQ(SQL_FUNC_EXISTS(bitmap.data(), function.Id), SQL_TRUE) + << function.Name << " missing from SQL_API_ODBC3_ALL_FUNCTIONS"; + } +} + +TEST_F(OdbcCoreConformance, DriverManagerEnumerationFunctionsAreUsable) { + SQLCHAR description[256] = {}; + SQLCHAR attributes[512] = {}; + SQLSMALLINT descriptionLength = 0; + SQLSMALLINT attributesLength = 0; + SQLRETURN rc = SQLDrivers( + Env_, + SQL_FETCH_FIRST, + description, + sizeof(description), + &descriptionLength, + attributes, + sizeof(attributes), + &attributesLength); + EXPECT_TRUE(IsOdbcSuccess(rc) || rc == SQL_NO_DATA) << "SQLDrivers returned " << rc; + + SQLCHAR dsn[256] = {}; + SQLCHAR dsnDescription[256] = {}; + SQLSMALLINT dsnLength = 0; + SQLSMALLINT dsnDescriptionLength = 0; + rc = SQLDataSources( + Env_, + SQL_FETCH_FIRST, + dsn, + sizeof(dsn), + &dsnLength, + dsnDescription, + sizeof(dsnDescription), + &dsnDescriptionLength); + EXPECT_TRUE(IsOdbcSuccess(rc) || rc == SQL_NO_DATA) << "SQLDataSources returned " << rc; +} + +TEST_F(OdbcCoreConformance, CoreConnectionAttributesFollowUnsetAndRoundTripSemantics) { + const std::array, 3> integerAttributes{{ + {SQL_ATTR_ACCESS_MODE, "SQL_ATTR_ACCESS_MODE"}, + {SQL_ATTR_ODBC_CURSORS, "SQL_ATTR_ODBC_CURSORS"}, + {SQL_ATTR_TRACE, "SQL_ATTR_TRACE"}, + }}; + for (const auto& [attribute, name] : integerAttributes) { + SQLULEN value = 0; + SQLINTEGER length = 0; + const SQLRETURN rc = SQLGetConnectAttr( + Dbc_, attribute, &value, sizeof(value), &length); + EXPECT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)) << name; + } + + SQLCHAR traceFile[512] = {}; + SQLINTEGER traceFileLength = 0; + EXPECT_TRUE(OdbcSuccess( + SQLGetConnectAttr( + Dbc_, SQL_ATTR_TRACEFILE, traceFile, sizeof(traceFile), &traceFileLength), + Dbc_, SQL_HANDLE_DBC)); + + SQLPOINTER quietMode = reinterpret_cast(uintptr_t{1}); + EXPECT_EQ( + SQLGetConnectAttr(Dbc_, SQL_ATTR_QUIET_MODE, &quietMode, sizeof(quietMode), nullptr), + SQL_NO_DATA); + SQLUINTEGER translateOption = 0; + EXPECT_EQ( + SQLGetConnectAttr( + Dbc_, SQL_ATTR_TRANSLATE_OPTION, &translateOption, sizeof(translateOption), nullptr), + SQL_NO_DATA); + SQLCHAR translateLib[32] = {}; + EXPECT_EQ( + SQLGetConnectAttr( + Dbc_, SQL_ATTR_TRANSLATE_LIB, translateLib, sizeof(translateLib), nullptr), + SQL_NO_DATA); + + quietMode = reinterpret_cast(uintptr_t{42}); + ASSERT_TRUE(OdbcSuccess( + SQLSetConnectAttr(Dbc_, SQL_ATTR_QUIET_MODE, quietMode, 0), + Dbc_, SQL_HANDLE_DBC)); + SQLPOINTER actualQuietMode = nullptr; + SQLINTEGER quietModeLength = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetConnectAttr( + Dbc_, SQL_ATTR_QUIET_MODE, &actualQuietMode, sizeof(actualQuietMode), &quietModeLength), + Dbc_, SQL_HANDLE_DBC)); + EXPECT_EQ(actualQuietMode, quietMode); + EXPECT_EQ(quietModeLength, static_cast(sizeof(SQLPOINTER))); + + constexpr SQLUINTEGER ExpectedTranslateOption = 17; + ASSERT_TRUE(OdbcSuccess( + SQLSetConnectAttr( + Dbc_, SQL_ATTR_TRANSLATE_OPTION, + reinterpret_cast(static_cast(ExpectedTranslateOption)), 0), + Dbc_, SQL_HANDLE_DBC)); + SQLINTEGER translateOptionLength = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetConnectAttr( + Dbc_, SQL_ATTR_TRANSLATE_OPTION, &translateOption, sizeof(translateOption), + &translateOptionLength), + Dbc_, SQL_HANDLE_DBC)); + EXPECT_EQ(translateOption, ExpectedTranslateOption); + EXPECT_EQ(translateOptionLength, static_cast(sizeof(SQLUINTEGER))); + + ASSERT_TRUE(OdbcSuccess( + SQLSetConnectAttr( + Dbc_, SQL_ATTR_ACCESS_MODE, reinterpret_cast(SQL_MODE_READ_ONLY), 0), + Dbc_, + SQL_HANDLE_DBC)); + SQLULEN accessMode = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetConnectAttr(Dbc_, SQL_ATTR_ACCESS_MODE, &accessMode, sizeof(accessMode), nullptr), + Dbc_, + SQL_HANDLE_DBC)); + EXPECT_EQ(accessMode, static_cast(SQL_MODE_READ_ONLY)); +} + +TEST_F(OdbcCoreConformance, CoreStatementAttributeDefaultsAreReadable) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + + const std::array, 7> integerAttributes{{ + {SQL_ATTR_CURSOR_TYPE, SQL_CURSOR_FORWARD_ONLY, "SQL_ATTR_CURSOR_TYPE"}, + {SQL_ATTR_METADATA_ID, SQL_FALSE, "SQL_ATTR_METADATA_ID"}, + {SQL_ATTR_NOSCAN, SQL_NOSCAN_OFF, "SQL_ATTR_NOSCAN"}, + {SQL_ATTR_PARAM_BIND_TYPE, SQL_PARAM_BIND_BY_COLUMN, "SQL_ATTR_PARAM_BIND_TYPE"}, + {SQL_ATTR_PARAMSET_SIZE, 1, "SQL_ATTR_PARAMSET_SIZE"}, + {SQL_ATTR_ROW_ARRAY_SIZE, 1, "SQL_ATTR_ROW_ARRAY_SIZE"}, + {SQL_ATTR_ROW_BIND_TYPE, SQL_BIND_BY_COLUMN, "SQL_ATTR_ROW_BIND_TYPE"}, + }}; + for (const auto& [attribute, expected, name] : integerAttributes) { + SQLULEN value = 0; + const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &value, sizeof(value), nullptr); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + if (IsOdbcSuccess(rc)) { + EXPECT_EQ(value, expected) << name; + } + } + + const std::array, 7> pointerAttributes{{ + {SQL_ATTR_PARAM_BIND_OFFSET_PTR, "SQL_ATTR_PARAM_BIND_OFFSET_PTR"}, + {SQL_ATTR_PARAM_OPERATION_PTR, "SQL_ATTR_PARAM_OPERATION_PTR"}, + {SQL_ATTR_PARAM_STATUS_PTR, "SQL_ATTR_PARAM_STATUS_PTR"}, + {SQL_ATTR_PARAMS_PROCESSED_PTR, "SQL_ATTR_PARAMS_PROCESSED_PTR"}, + {SQL_ATTR_ROW_BIND_OFFSET_PTR, "SQL_ATTR_ROW_BIND_OFFSET_PTR"}, + {SQL_ATTR_ROW_STATUS_PTR, "SQL_ATTR_ROW_STATUS_PTR"}, + {SQL_ATTR_ROWS_FETCHED_PTR, "SQL_ATTR_ROWS_FETCHED_PTR"}, + }}; + for (const auto& [attribute, name] : pointerAttributes) { + SQLPOINTER value = reinterpret_cast(uintptr_t{1}); + const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &value, sizeof(value), nullptr); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + if (IsOdbcSuccess(rc)) { + EXPECT_EQ(value, nullptr) << name; + } + } + + for (const auto& [attribute, name] : std::array, 4>{{ + {SQL_ATTR_APP_PARAM_DESC, "SQL_ATTR_APP_PARAM_DESC"}, + {SQL_ATTR_APP_ROW_DESC, "SQL_ATTR_APP_ROW_DESC"}, + {SQL_ATTR_IMP_PARAM_DESC, "SQL_ATTR_IMP_PARAM_DESC"}, + {SQL_ATTR_IMP_ROW_DESC, "SQL_ATTR_IMP_ROW_DESC"}, + }}) { + SQLHDESC desc = SQL_NULL_HDESC; + const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &desc, sizeof(desc), nullptr); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + if (IsOdbcSuccess(rc)) { + EXPECT_NE(desc, nullptr) << name; + } + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, CoreStatementArrayAttributesCanBeConfigured) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + + auto setAndGetInteger = [&](SQLINTEGER attribute, SQLULEN expected, const char* name) { + SQLRETURN rc = SQLSetStmtAttr( + stmt, attribute, reinterpret_cast(static_cast(expected)), 0); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + if (!IsOdbcSuccess(rc)) { + return; + } + SQLULEN actual = 0; + rc = SQLGetStmtAttr(stmt, attribute, &actual, sizeof(actual), nullptr); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + EXPECT_EQ(actual, expected) << name; + }; + + setAndGetInteger(SQL_ATTR_METADATA_ID, SQL_TRUE, "SQL_ATTR_METADATA_ID"); + setAndGetInteger(SQL_ATTR_NOSCAN, SQL_NOSCAN_ON, "SQL_ATTR_NOSCAN"); + setAndGetInteger(SQL_ATTR_PARAM_BIND_TYPE, 32, "SQL_ATTR_PARAM_BIND_TYPE"); + setAndGetInteger(SQL_ATTR_PARAMSET_SIZE, 2, "SQL_ATTR_PARAMSET_SIZE"); + setAndGetInteger(SQL_ATTR_ROW_ARRAY_SIZE, 2, "SQL_ATTR_ROW_ARRAY_SIZE"); + setAndGetInteger(SQL_ATTR_ROW_BIND_TYPE, 32, "SQL_ATTR_ROW_BIND_TYPE"); + + SQLULEN offset = 8; + SQLUSMALLINT operation[2] = {SQL_PARAM_PROCEED, SQL_PARAM_PROCEED}; + SQLUSMALLINT status[2] = {}; + SQLULEN processed = 0; + SQLUSMALLINT rowStatus[2] = {}; + SQLULEN rowsFetched = 0; + const std::array, 7> pointerAttributes{{ + {SQL_ATTR_PARAM_BIND_OFFSET_PTR, &offset, "SQL_ATTR_PARAM_BIND_OFFSET_PTR"}, + {SQL_ATTR_PARAM_OPERATION_PTR, operation, "SQL_ATTR_PARAM_OPERATION_PTR"}, + {SQL_ATTR_PARAM_STATUS_PTR, status, "SQL_ATTR_PARAM_STATUS_PTR"}, + {SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, "SQL_ATTR_PARAMS_PROCESSED_PTR"}, + {SQL_ATTR_ROW_BIND_OFFSET_PTR, &offset, "SQL_ATTR_ROW_BIND_OFFSET_PTR"}, + {SQL_ATTR_ROW_STATUS_PTR, rowStatus, "SQL_ATTR_ROW_STATUS_PTR"}, + {SQL_ATTR_ROWS_FETCHED_PTR, &rowsFetched, "SQL_ATTR_ROWS_FETCHED_PTR"}, + }}; + for (const auto& [attribute, expected, name] : pointerAttributes) { + SQLRETURN rc = SQLSetStmtAttr(stmt, attribute, expected, 0); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + if (!IsOdbcSuccess(rc)) { + continue; + } + SQLPOINTER actual = nullptr; + rc = SQLGetStmtAttr(stmt, attribute, &actual, sizeof(actual), nullptr); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + EXPECT_EQ(actual, expected) << name; + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, ApplicationDescriptorsCanBeAssigned) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + SQLHDESC ard = SQL_NULL_HDESC; + SQLHDESC apd = SQL_NULL_HDESC; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &ard), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &apd), SQL_SUCCESS); + + SQLRETURN rc = SQLSetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, ard, 0); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << "SQL_ATTR_APP_ROW_DESC"; + SQLHDESC actual = SQL_NULL_HDESC; + if (IsOdbcSuccess(rc)) { + ASSERT_TRUE(OdbcSuccess( + SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &actual, sizeof(actual), nullptr), + stmt, + SQL_HANDLE_STMT)); + EXPECT_EQ(actual, ard); + } + + rc = SQLSetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, apd, 0); + EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << "SQL_ATTR_APP_PARAM_DESC"; + actual = SQL_NULL_HDESC; + if (IsOdbcSuccess(rc)) { + ASSERT_TRUE(OdbcSuccess( + SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &actual, sizeof(actual), nullptr), + stmt, + SQL_HANDLE_STMT)); + EXPECT_EQ(actual, apd); + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLFreeHandle(SQL_HANDLE_DESC, ard); + SQLFreeHandle(SQL_HANDLE_DESC, apd); +} + +TEST_F(OdbcCoreConformance, ApplicationDescriptorsDriveBindingsAndDetachSafely) { + SQLHSTMT first = AllocStatement(); + SQLHSTMT second = AllocStatement(); + ASSERT_NE(first, nullptr); + ASSERT_NE(second, nullptr); + SQLHDESC apd = SQL_NULL_HDESC; + SQLHDESC ard = SQL_NULL_HDESC; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &apd), SQL_SUCCESS); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &ard), SQL_SUCCESS); + + SQLINTEGER fallbackParameter = 1; + ASSERT_TRUE(OdbcSuccess( + SQLPrepare(first, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), + first, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLBindParameter(first, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, + &fallbackParameter, 0, nullptr), + first, SQL_HANDLE_STMT)); + SQLINTEGER descriptorParameter = 42; + ASSERT_TRUE(OdbcSuccess( + SQLSetDescRec(apd, 1, SQL_C_LONG, 0, sizeof(descriptorParameter), 0, 0, + &descriptorParameter, nullptr, nullptr), + apd, SQL_HANDLE_DESC)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(first, SQL_ATTR_APP_PARAM_DESC, apd, 0), first, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecute(first), first, SQL_HANDLE_STMT)); + + SQLINTEGER fallbackResult = 0; + SQLLEN fallbackIndicator = 0; + ASSERT_TRUE(OdbcSuccess( + SQLBindCol(first, 1, SQL_C_LONG, &fallbackResult, 0, &fallbackIndicator), + first, SQL_HANDLE_STMT)); + SQLINTEGER descriptorResult = 0; + SQLLEN descriptorLength = -1; + SQLLEN descriptorIndicator = -1; + ASSERT_TRUE(OdbcSuccess( + SQLSetDescRec(ard, 1, SQL_C_LONG, 0, 0, 0, 0, + &descriptorResult, &descriptorLength, &descriptorIndicator), + ard, SQL_HANDLE_DESC)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(first, SQL_ATTR_APP_ROW_DESC, ard, 0), first, SQL_HANDLE_STMT)); + ASSERT_EQ(SQLFetch(first), SQL_SUCCESS); + EXPECT_EQ(descriptorResult, 42); + EXPECT_EQ(descriptorLength, static_cast(sizeof(descriptorResult))); + EXPECT_EQ(descriptorIndicator, 0); + EXPECT_EQ(fallbackResult, 0); + + ASSERT_TRUE(OdbcSuccess( + SQLSetDescField( + apd, 0, SQL_DESC_ARRAY_SIZE, reinterpret_cast(uintptr_t{2}), 0), + apd, SQL_HANDLE_DESC)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(second, SQL_ATTR_APP_PARAM_DESC, apd, 0), second, SQL_HANDLE_STMT)); + SQLULEN size = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetStmtAttr(second, SQL_ATTR_PARAMSET_SIZE, &size, sizeof(size), nullptr), + second, SQL_HANDLE_STMT)); + EXPECT_EQ(size, 2u); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr( + second, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{3}), 0), + second, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLGetDescField(apd, 0, SQL_DESC_ARRAY_SIZE, &size, 0, nullptr), + apd, SQL_HANDLE_DESC)); + EXPECT_EQ(size, 3u); + + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_DESC, apd), SQL_SUCCESS); + ASSERT_TRUE(OdbcSuccess( + SQLGetStmtAttr(second, SQL_ATTR_PARAMSET_SIZE, &size, sizeof(size), nullptr), + second, SQL_HANDLE_STMT)); + EXPECT_EQ(size, 1u); + ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_DESC, ard), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, first); + SQLFreeHandle(SQL_HANDLE_STMT, second); +} + +TEST_F(OdbcCoreConformance, ParameterArraysRespectBindOffset) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "DROP TABLE IF EXISTS odbc_core_parameter_array")), + SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "CREATE TABLE odbc_core_parameter_array (id Int32, PRIMARY KEY (id))")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLFreeStmt(stmt, SQL_CLOSE); + + struct TParameterRow { + SQLINTEGER Id; + SQLLEN Indicator; + }; + std::array parameters{{ + {0, 0}, // deliberately skipped by SQL_ATTR_PARAM_BIND_OFFSET_PTR + {101, 0}, + {102, 0}, + }}; + SQLULEN bindOffset = sizeof(TParameterRow); + SQLUSMALLINT operations[2] = {SQL_PARAM_PROCEED, SQL_PARAM_PROCEED}; + SQLUSMALLINT statuses[2] = {}; + SQLULEN processed = 0; + + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr( + stmt, + SQL_ATTR_PARAM_BIND_TYPE, + reinterpret_cast(sizeof(TParameterRow)), + 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, &bindOffset, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr( + stmt, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{2}), 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_OPERATION_PTR, operations, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_STATUS_PTR, statuses, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLPrepare( + stmt, + reinterpret_cast(const_cast( + "UPSERT INTO odbc_core_parameter_array (id) VALUES (?)")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLBindParameter( + stmt, + 1, + SQL_PARAM_INPUT, + SQL_C_LONG, + SQL_INTEGER, + 0, + 0, + ¶meters[0].Id, + 0, + ¶meters[0].Indicator), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); + EXPECT_EQ(processed, 2u); + for (SQLUSMALLINT status : statuses) { + EXPECT_TRUE(status == SQL_PARAM_SUCCESS || status == SQL_PARAM_SUCCESS_WITH_INFO) + << "parameter status " << status; + } + + SQLFreeStmt(stmt, SQL_CLOSE); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "SELECT COUNT(*) FROM odbc_core_parameter_array")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLBIGINT count = 0; + ASSERT_TRUE(OdbcSuccess( + SQLGetData(stmt, 1, SQL_C_SBIGINT, &count, sizeof(count), nullptr), + stmt, + SQL_HANDLE_STMT)); + EXPECT_EQ(count, 2); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "DROP TABLE odbc_core_parameter_array")), + SQL_NTS); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, RowArraysRespectBindOffset) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "DROP TABLE IF EXISTS odbc_core_row_array")), + SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "CREATE TABLE odbc_core_row_array (id Int32, PRIMARY KEY (id))")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLFreeStmt(stmt, SQL_CLOSE); + for (const char* sql : { + "UPSERT INTO odbc_core_row_array (id) VALUES (1)", + "UPSERT INTO odbc_core_row_array (id) VALUES (2)", + "UPSERT INTO odbc_core_row_array (id) VALUES (3)", + }) { + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect(stmt, reinterpret_cast(const_cast(sql)), SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLFreeStmt(stmt, SQL_CLOSE); + } + + struct TRow { + SQLINTEGER Id; + SQLLEN Indicator; + }; + std::array rows{}; + SQLULEN bindOffset = sizeof(TRow); + SQLUSMALLINT statuses[2] = {}; + SQLULEN fetched = 0; + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr( + stmt, + SQL_ATTR_ROW_BIND_TYPE, + reinterpret_cast(sizeof(TRow)), + 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, &bindOffset, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr( + stmt, SQL_ATTR_ROW_ARRAY_SIZE, reinterpret_cast(uintptr_t{2}), 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_ROW_STATUS_PTR, statuses, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLSetStmtAttr(stmt, SQL_ATTR_ROWS_FETCHED_PTR, &fetched, 0), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "SELECT id FROM odbc_core_row_array ORDER BY id")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLBindCol(stmt, 1, SQL_C_LONG, &rows[0].Id, 0, &rows[0].Indicator), + stmt, + SQL_HANDLE_STMT)); + + ASSERT_TRUE(OdbcSuccess(SQLFetchScroll(stmt, SQL_FETCH_NEXT, 0), stmt, SQL_HANDLE_STMT)); + ASSERT_EQ(fetched, 2u); + EXPECT_EQ(rows[1].Id, 1); + EXPECT_EQ(rows[2].Id, 2); + rows[1].Id = 0; + ASSERT_TRUE(OdbcSuccess(SQLFetchScroll(stmt, SQL_FETCH_NEXT, 0), stmt, SQL_HANDLE_STMT)); + ASSERT_EQ(fetched, 1u); + EXPECT_EQ(rows[1].Id, 3); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "DROP TABLE odbc_core_row_array")), + SQL_NTS); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, CoreDescriptorHeaderFieldsAreReadable) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + ASSERT_TRUE(OdbcSuccess( + SQLPrepare(stmt, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLINTEGER parameter = 7; + ASSERT_TRUE(OdbcSuccess( + SQLBindParameter( + stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, ¶meter, 0, nullptr), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); + SQLINTEGER result = 0; + SQLLEN indicator = 0; + ASSERT_TRUE(OdbcSuccess( + SQLBindCol(stmt, 1, SQL_C_LONG, &result, 0, &indicator), + stmt, + SQL_HANDLE_STMT)); + + SQLHDESC ard = SQL_NULL_HDESC; + SQLHDESC apd = SQL_NULL_HDESC; + SQLHDESC ird = SQL_NULL_HDESC; + SQLHDESC ipd = SQL_NULL_HDESC; + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, sizeof(ard), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &apd, sizeof(apd), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_PARAM_DESC, &ipd, sizeof(ipd), nullptr), stmt, SQL_HANDLE_STMT)); + + for (SQLHDESC desc : {ard, apd, ird, ipd}) { + ExpectDescriptorField(desc, 0, SQL_DESC_ALLOC_TYPE, "SQL_DESC_ALLOC_TYPE"); + ExpectDescriptorField(desc, 0, SQL_DESC_COUNT, "SQL_DESC_COUNT"); + } + for (SQLHDESC desc : {ard, apd}) { + ExpectDescriptorField(desc, 0, SQL_DESC_ARRAY_SIZE, "SQL_DESC_ARRAY_SIZE"); + ExpectDescriptorField(desc, 0, SQL_DESC_BIND_OFFSET_PTR, "SQL_DESC_BIND_OFFSET_PTR"); + ExpectDescriptorField(desc, 0, SQL_DESC_BIND_TYPE, "SQL_DESC_BIND_TYPE"); + } + for (SQLHDESC desc : {apd, ipd, ird}) { + ExpectDescriptorField(desc, 0, SQL_DESC_ARRAY_STATUS_PTR, "SQL_DESC_ARRAY_STATUS_PTR"); + } + for (SQLHDESC desc : {ipd, ird}) { + ExpectDescriptorField(desc, 0, SQL_DESC_ROWS_PROCESSED_PTR, "SQL_DESC_ROWS_PROCESSED_PTR"); + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, CoreDescriptorRecordFieldsAreReadable) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + ASSERT_TRUE(OdbcSuccess( + SQLPrepare(stmt, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLINTEGER parameter = 7; + SQLLEN parameterIndicator = 0; + ASSERT_TRUE(OdbcSuccess( + SQLBindParameter( + stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, + ¶meter, 0, ¶meterIndicator), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); + SQLINTEGER result = 0; + SQLLEN resultIndicator = 0; + ASSERT_TRUE(OdbcSuccess( + SQLBindCol(stmt, 1, SQL_C_LONG, &result, 0, &resultIndicator), + stmt, + SQL_HANDLE_STMT)); + + SQLHDESC ard = SQL_NULL_HDESC; + SQLHDESC apd = SQL_NULL_HDESC; + SQLHDESC ird = SQL_NULL_HDESC; + SQLHDESC ipd = SQL_NULL_HDESC; + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, sizeof(ard), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &apd, sizeof(apd), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_PARAM_DESC, &ipd, sizeof(ipd), nullptr), stmt, SQL_HANDLE_STMT)); + + for (const auto& [field, name] : std::array, 6>{{ + {SQL_DESC_BASE_COLUMN_NAME, "SQL_DESC_BASE_COLUMN_NAME"}, + {SQL_DESC_LITERAL_PREFIX, "SQL_DESC_LITERAL_PREFIX"}, + {SQL_DESC_LITERAL_SUFFIX, "SQL_DESC_LITERAL_SUFFIX"}, + {SQL_DESC_LOCAL_TYPE_NAME, "SQL_DESC_LOCAL_TYPE_NAME"}, + {SQL_DESC_NAME, "SQL_DESC_NAME"}, + {SQL_DESC_TYPE_NAME, "SQL_DESC_TYPE_NAME"}, + }}) { + ExpectDescriptorField(ird, 1, field, name, true); + } + for (const auto& [field, name] : std::array, 14>{{ + {SQL_DESC_CASE_SENSITIVE, "SQL_DESC_CASE_SENSITIVE"}, + {SQL_DESC_CONCISE_TYPE, "SQL_DESC_CONCISE_TYPE"}, + {SQL_DESC_DISPLAY_SIZE, "SQL_DESC_DISPLAY_SIZE"}, + {SQL_DESC_FIXED_PREC_SCALE, "SQL_DESC_FIXED_PREC_SCALE"}, + {SQL_DESC_LENGTH, "SQL_DESC_LENGTH"}, + {SQL_DESC_NULLABLE, "SQL_DESC_NULLABLE"}, + {SQL_DESC_OCTET_LENGTH, "SQL_DESC_OCTET_LENGTH"}, + {SQL_DESC_PRECISION, "SQL_DESC_PRECISION"}, + {SQL_DESC_SCALE, "SQL_DESC_SCALE"}, + {SQL_DESC_SEARCHABLE, "SQL_DESC_SEARCHABLE"}, + {SQL_DESC_TYPE, "SQL_DESC_TYPE"}, + {SQL_DESC_UNNAMED, "SQL_DESC_UNNAMED"}, + {SQL_DESC_UNSIGNED, "SQL_DESC_UNSIGNED"}, + {SQL_DESC_UPDATABLE, "SQL_DESC_UPDATABLE"}, + }}) { + ExpectDescriptorField(ird, 1, field, name); + } + + for (const auto& [field, name] : std::array, 3>{{ + {SQL_DESC_DATA_PTR, "SQL_DESC_DATA_PTR"}, + {SQL_DESC_INDICATOR_PTR, "SQL_DESC_INDICATOR_PTR"}, + {SQL_DESC_OCTET_LENGTH_PTR, "SQL_DESC_OCTET_LENGTH_PTR"}, + }}) { + ExpectDescriptorField(ard, 1, field, name); + ExpectDescriptorField(apd, 1, field, name); + } + ExpectDescriptorField(ipd, 1, SQL_DESC_PARAMETER_TYPE, "SQL_DESC_PARAMETER_TYPE"); + + SQLFreeStmt(stmt, SQL_CLOSE); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "SELECT CAST('2024-06-15T14:30:00Z' AS Datetime) AS value")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess( + SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), + stmt, + SQL_HANDLE_STMT)); + ExpectDescriptorField( + ird, + 1, + SQL_DESC_DATETIME_INTERVAL_CODE, + "SQL_DESC_DATETIME_INTERVAL_CODE"); + + SQLSMALLINT conciseType = 0; + EXPECT_TRUE(OdbcSuccess( + SQLGetDescRec(ird, 1, nullptr, 0, nullptr, &conciseType, nullptr, + nullptr, nullptr, nullptr, nullptr), + ird, SQL_HANDLE_DESC)); + EXPECT_EQ(SQLGetDescField( + ird, 2, SQL_DESC_CONCISE_TYPE, &conciseType, 0, nullptr), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, ReportsConservativeCapabilitiesAndTransactionSemantics) { + SQLUINTEGER value = 0; + ASSERT_TRUE(OdbcSuccess(SQLGetInfo( + Dbc_, SQL_PARAM_ARRAY_ROW_COUNTS, &value, sizeof(value), nullptr), Dbc_, SQL_HANDLE_DBC)); + EXPECT_EQ(value, static_cast(SQL_PARC_NO_BATCH)); + ASSERT_TRUE(OdbcSuccess(SQLGetInfo( + Dbc_, SQL_PARAM_ARRAY_SELECTS, &value, sizeof(value), nullptr), Dbc_, SQL_HANDLE_DBC)); + EXPECT_EQ(value, static_cast(SQL_PAS_NO_SELECT)); + + SQLUSMALLINT txnCapable = 0; + ASSERT_TRUE(OdbcSuccess(SQLGetInfo( + Dbc_, SQL_TXN_CAPABLE, &txnCapable, sizeof(txnCapable), nullptr), Dbc_, SQL_HANDLE_DBC)); + EXPECT_EQ(txnCapable, static_cast(SQL_TC_DML)); + EXPECT_EQ(SQLEndTran(SQL_HANDLE_DBC, Dbc_, SQL_COMMIT), SQL_SUCCESS); + EXPECT_EQ(SQLEndTran(SQL_HANDLE_DBC, Dbc_, 99), SQL_ERROR); + SQLCHAR state[6] = {}; + ASSERT_TRUE(IsOdbcSuccess(SQLGetDiagRec( + SQL_HANDLE_DBC, Dbc_, 1, state, nullptr, nullptr, 0, nullptr))); + EXPECT_STREQ(reinterpret_cast(state), "HY012"); +} + +TEST_F(OdbcCoreConformance, RejectsSelectParameterArrays) { + SQLHSTMT stmt = AllocStatement(); + SQLINTEGER values[2] = {1, 2}; + SQLLEN indicators[2] = {0, 0}; + ASSERT_TRUE(OdbcSuccess(SQLSetStmtAttr( + stmt, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{2}), 0), + stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLPrepare( + stmt, reinterpret_cast(const_cast("SELECT ?")), SQL_NTS), + stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLBindParameter( + stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, + values, 0, indicators), stmt, SQL_HANDLE_STMT)); + EXPECT_EQ(SQLExecute(stmt), SQL_ERROR); + EXPECT_NE(GetOdbcError(stmt, SQL_HANDLE_STMT).find("HYC00"), std::string::npos); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, BindingsMayPrecedeExecDirect) { + SQLHSTMT stmt = AllocStatement(); + SQLINTEGER parameter = 77; + ASSERT_TRUE(OdbcSuccess(SQLBindParameter( + stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, + ¶meter, 0, nullptr), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecDirect( + stmt, reinterpret_cast(const_cast("SELECT ?")), SQL_NTS), + stmt, SQL_HANDLE_STMT)); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLINTEGER result = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &result, 0, nullptr), SQL_SUCCESS); + EXPECT_EQ(result, parameter); + + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_RESET_PARAMS), SQL_SUCCESS); + SQLLEN nullIndicator = SQL_NULL_DATA; + ASSERT_TRUE(OdbcSuccess(SQLBindParameter( + stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, + nullptr, 0, &nullIndicator), stmt, SQL_HANDLE_STMT)); + ASSERT_TRUE(OdbcSuccess(SQLExecDirect( + stmt, reinterpret_cast(const_cast("SELECT ? IS NULL")), SQL_NTS), + stmt, SQL_HANDLE_STMT)); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + SQLCHAR isNull = 0; + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_BIT, &isNull, 0, nullptr), SQL_SUCCESS); + EXPECT_EQ(isNull, 1); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, GetDataConvertsNumbersAndContinuesText) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_TRUE(OdbcSuccess(SQLExecDirect( + stmt, reinterpret_cast(const_cast( + "SELECT 123 AS number, 'abcdef' AS text")), SQL_NTS), + stmt, SQL_HANDLE_STMT)); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + char numeric[16] = {}; + SQLLEN length = 0; + EXPECT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, numeric, -1, &length), SQL_ERROR); + EXPECT_NE(GetOdbcError(stmt, SQL_HANDLE_STMT).find("HY090"), std::string::npos); + ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, numeric, sizeof(numeric), &length), SQL_SUCCESS); + EXPECT_STREQ(numeric, "123"); + + char chunk[4] = {}; + EXPECT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_SUCCESS_WITH_INFO); + EXPECT_STREQ(chunk, "abc"); + EXPECT_EQ(length, 6); + ASSERT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_SUCCESS); + EXPECT_STREQ(chunk, "def"); + EXPECT_EQ(length, 3); + EXPECT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, DiagnosticReadsAreStableAndSuccessClearsThem) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_EQ(SQLExecDirect( + stmt, reinterpret_cast(const_cast("invalid syntax")), SQL_NTS), SQL_ERROR); + SQLCHAR first[6] = {}; + SQLCHAR second[6] = {}; + ASSERT_EQ(SQLGetDiagRec( + SQL_HANDLE_STMT, stmt, 1, first, nullptr, nullptr, 0, nullptr), SQL_SUCCESS); + ASSERT_EQ(SQLGetDiagRec( + SQL_HANDLE_STMT, stmt, 1, second, nullptr, nullptr, 0, nullptr), SQL_SUCCESS); + EXPECT_STREQ(reinterpret_cast(first), reinterpret_cast(second)); + + SQLRETURN prior = SQL_SUCCESS; + ASSERT_EQ(SQLGetDiagField( + SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_RETURNCODE, &prior, 0, nullptr), SQL_SUCCESS); + EXPECT_EQ(prior, SQL_ERROR); + EXPECT_EQ(SQLPrepare( + stmt, reinterpret_cast(const_cast("SELECT 1")), SQL_NTS), SQL_SUCCESS); + EXPECT_EQ(SQLGetDiagRec( + SQL_HANDLE_STMT, stmt, 1, first, nullptr, nullptr, 0, nullptr), SQL_NO_DATA); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +TEST_F(OdbcCoreConformance, CoreCatalogFunctionsReturnStandardResultShapes) { + SQLHSTMT stmt = AllocStatement(); + ASSERT_NE(stmt, nullptr); + const char* table = "/local/odbc_core_conformance_catalog"; + SQLExecDirect( + stmt, + reinterpret_cast(const_cast("DROP TABLE IF EXISTS odbc_core_conformance_catalog")), + SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + ASSERT_TRUE(OdbcSuccess( + SQLExecDirect( + stmt, + reinterpret_cast(const_cast( + "CREATE TABLE odbc_core_conformance_catalog (id Int32, value Text, PRIMARY KEY (id))")), + SQL_NTS), + stmt, + SQL_HANDLE_STMT)); + SQLFreeStmt(stmt, SQL_CLOSE); + + auto expectColumns = [&](SQLRETURN rc, SQLSMALLINT expected, const char* name) { + ASSERT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; + SQLSMALLINT actual = 0; + ASSERT_TRUE(OdbcSuccess(SQLNumResultCols(stmt, &actual), stmt, SQL_HANDLE_STMT)) << name; + EXPECT_EQ(actual, expected) << name; + SQLFreeStmt(stmt, SQL_CLOSE); + }; + + expectColumns( + SQLTables(stmt, nullptr, 0, nullptr, 0, + reinterpret_cast(const_cast(table)), SQL_NTS, + reinterpret_cast(const_cast("TABLE")), SQL_NTS), + 5, + "SQLTables"); + expectColumns( + SQLColumns(stmt, nullptr, 0, nullptr, 0, + reinterpret_cast(const_cast(table)), SQL_NTS, + nullptr, 0), + 18, + "SQLColumns"); + expectColumns(SQLGetTypeInfo(stmt, SQL_ALL_TYPES), 19, "SQLGetTypeInfo"); + expectColumns( + SQLStatistics(stmt, nullptr, 0, nullptr, 0, + reinterpret_cast(const_cast(table)), SQL_NTS, + SQL_INDEX_ALL, SQL_ENSURE), + 13, + "SQLStatistics"); + expectColumns( + SQLSpecialColumns(stmt, SQL_BEST_ROWID, nullptr, 0, nullptr, 0, + reinterpret_cast(const_cast(table)), SQL_NTS, + SQL_SCOPE_SESSION, SQL_NULLABLE), + 8, + "SQLSpecialColumns"); + + SQLExecDirect( + stmt, + reinterpret_cast(const_cast("DROP TABLE odbc_core_conformance_catalog")), + SQL_NTS); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} diff --git a/odbc/tests/integration/core_api_it.cpp b/odbc/tests/integration/core_api_it.cpp index d66971490f7..3a2a263a01d 100644 --- a/odbc/tests/integration/core_api_it.cpp +++ b/odbc/tests/integration/core_api_it.cpp @@ -304,17 +304,21 @@ TEST(CoreApi, SQLParamDataPutData) { CHECK_ODBC_OK(SQLPrepare(stmt, (SQLCHAR*)"UPSERT INTO test_at_exec (id, val) VALUES (1, ?)", SQL_NTS), stmt, SQL_HANDLE_STMT); SQLLEN atExec = SQL_DATA_AT_EXEC; + SQLPOINTER parameterToken = &atExec; CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 32, 0, - nullptr, 0, &atExec), stmt, SQL_HANDLE_STMT); + parameterToken, 0, &atExec), stmt, SQL_HANDLE_STMT); ASSERT_EQ(SQLExecute(stmt), SQL_NEED_DATA); SQLPOINTER token = nullptr; ASSERT_EQ(SQLParamData(stmt, &token), SQL_NEED_DATA); + EXPECT_EQ(token, parameterToken); const char part1[] = "hel"; CHECK_ODBC_OK(SQLPutData(stmt, (SQLPOINTER)part1, sizeof(part1) - 1), stmt, SQL_HANDLE_STMT); const char part2[] = "lo"; CHECK_ODBC_OK(SQLPutData(stmt, (SQLPOINTER)part2, sizeof(part2) - 1), stmt, SQL_HANDLE_STMT); CHECK_ODBC_OK(SQLPutData(stmt, nullptr, 0), stmt, SQL_HANDLE_STMT); CHECK_ODBC_OK(SQLParamData(stmt, &token), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(SQLExecute(stmt), SQL_NEED_DATA); + CHECK_ODBC_OK(SQLCancel(stmt), stmt, SQL_HANDLE_STMT); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); diff --git a/odbc/tests/unit/convert_ut.cpp b/odbc/tests/unit/convert_ut.cpp index 86b0d3d5be3..eb7863ff219 100644 --- a/odbc/tests/unit/convert_ut.cpp +++ b/odbc/tests/unit/convert_ut.cpp @@ -41,7 +41,7 @@ TEST(OdbcConvert, Int64ToYdb) { CheckProto(value->GetProto(), "int64_value: 42\n"); } -TEST(OdbcConvert, Uint64ToYdb) { +TEST(OdbcConvert, UnsignedCToSignedSqlBigint) { SQLUBIGINT v = 123; TBoundParam param{ 1, SQL_PARAM_INPUT, SQL_C_UBIGINT, SQL_BIGINT, 0, 0, &v, sizeof(v), nullptr @@ -51,8 +51,8 @@ TEST(OdbcConvert, Uint64ToYdb) { auto params = paramsBuilder.Build(); auto value = params.GetValue("$p1"); ASSERT_TRUE(value); - CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: UINT64\n }\n}\n"); - CheckProto(value->GetProto(), "uint64_value: 123\n"); + CheckProto(value->GetType().GetProto(), "optional_type {\n item {\n type_id: INT64\n }\n}\n"); + CheckProto(value->GetProto(), "int64_value: 123\n"); } TEST(OdbcConvert, DoubleToYdb) { From cc59fa87752df63a378e6b0ae2904dd2021d8e22 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Tue, 28 Jul 2026 12:25:39 +0300 Subject: [PATCH 43/55] work in progress --- odbc/README.md | 18 +- odbc/proposal.md | 205 ++++ odbc/tests/conformance/CMakeLists.txt | 232 +++- odbc/tests/conformance/README.md | 56 - .../tests/conformance/core_conformance_it.cpp | 1036 ----------------- .../conformance/soci_odbc_test_context.cpp | 111 ++ 6 files changed, 554 insertions(+), 1104 deletions(-) create mode 100644 odbc/proposal.md delete mode 100644 odbc/tests/conformance/README.md delete mode 100644 odbc/tests/conformance/core_conformance_it.cpp create mode 100644 odbc/tests/conformance/soci_odbc_test_context.cpp diff --git a/odbc/README.md b/odbc/README.md index 626eec86ccb..089b460c93e 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -102,12 +102,20 @@ SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT `?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` from `SQLBindParameter` types. YDB-native `$pN` syntax also works. -## Core conformance tests +## Conformance and client-framework tests -The Linux/headless ODBC 3.x Core contract audit and the open-source test-suite -evaluation are documented in [tests/conformance](tests/conformance/README.md). -With a local YDB server running, build `odbc-core-conformance_it` and run -`ctest --test-dir build -L core-conformance --output-on-failure`. +The test build fetches pinned unixODBC-Test and SOCI revisions. The SOCI gate +runs 51 unchanged upstream framework cases through its Core-only ODBC backend; +the repository contains only the schema/capability adapter needed by the +upstream harness. With a local server running, build the test preset and run +either suite: + +```bash +cmake --preset release-test-clang +cmake --build build -j$(nproc) +ctest --test-dir build -L core-conformance --output-on-failure +ctest --test-dir build -L core-framework --output-on-failure +``` ## License diff --git a/odbc/proposal.md b/odbc/proposal.md new file mode 100644 index 00000000000..b330ded20a7 --- /dev/null +++ b/odbc/proposal.md @@ -0,0 +1,205 @@ +# Odbc driver +Odbc is a database connection layer, which gives users the opportunity to execute sql and interact with a database using a standardised C ABI. This document regulates how should the odbc driver for YDB be implemented, which functionality it is supposed to cover, performance issues and the acceptance criteria. + +## Goal + +The driver should provide useful YDB access to as many programming languages as possible through their established ODBC libraries. Select one representative framework or binding per language, run its upstream database tests when they exist, maintain an explicit YDB compatibility patch series for those tests, and provide a runnable example application in every selected language. + +Languages with a maintained native YDB SDK are excluded because their native SDK is the preferred integration. The current exclusions are C++, Go, Java, Python, C#/.NET, JavaScript/TypeScript and Rust. PHP is the explicit exception because its native SDK is planned for deprecation. The exclusion list must be checked against the current [YDB SDK installation page](https://ydb.tech/docs/en/reference/ydb-sdk/install) whenever the framework matrix is updated. + +## Acceptance criteria + +- Every selected language has exactly one primary ODBC framework or binding, a pinned upstream revision, a reproducible YDB patch series, an Allure test result set and a repository-owned example application. +- Every upstream database test that exists is either executed, patched with a documented YDB-specific reason, or listed explicitly as not applicable; tests must never disappear silently. +- Framework patches may adapt database assumptions to YDB, but they must not change the framework implementation or weaken assertions for ODBC behavior that the driver advertises. +- The existing driver unit, integration and conformance suites continue to pass. +- Multiple independent ODBC connections can target different YDB databases and hosts without sharing sessions, transactions, credentials or catalog state. +- PHP PDO_ODBC performance does not regress relative to the pinned native PHP SDK. The target is for PDO_ODBC to outperform the deprecated SDK on equivalent operations. + +## Language and framework matrix + +The matrix is intentionally open-ended. A new language should be added whenever an installable Linux ODBC binding can connect through unixODBC, execute parameterized statements and fetch results. A small or old upstream suite is not a reason to reject a language; it means the shared contract and example application carry more of its coverage. + +| Tier | Language | Selected framework or binding | Upstream tests to run | Required example | +|---|---|---|---|---| +| Core | Erlang | [OTP `odbc`](https://github.com/erlang/otp/tree/master/lib/odbc) | All YDB-applicable Common Test cases in `lib/odbc/test` | OTP application using `odbc:param_query` and transactions | +| Core | PHP | [PDO_ODBC](https://github.com/php/php-src/tree/master/ext/pdo_odbc) | PDO_ODBC PHPT tests and generic PDO tests selected by the existing harness | CLI application using `PDO`, prepared statements and transactions | +| Core | Haskell | [HDBC-odbc](https://github.com/hdbc/HDBC-odbc) | HDBC/HUnit database tests | Cabal application using prepared statements and `withTransaction` | +| Core | Ruby | [ruby-odbc](https://github.com/larskanis/ruby-odbc) | All upstream database-independent test scripts | Ruby application using prepared statements, iteration and rollback | +| Core | Lua | [LuaSQL ODBC](https://github.com/lunarmodules/luasql) | Common LuaSQL tests and ODBC-specific parameter tests | Lua application using environment, connection and cursor objects | +| Core | Perl | [DBD::ODBC](https://github.com/perl5-dbi/DBD-ODBC) | Generic TAP tests under `t/` | DBI application using binding, fetch hashes and transactions | +| Core | R | [`odbc` with DBI](https://github.com/r-dbi/odbc) | Package `testthat` tests and DBItest compliance groups | R script returning a typed data frame through DBI | +| Core | Julia | [ODBC.jl](https://github.com/JuliaDatabases/ODBC.jl) | ODBC.jl, DBInterface and Tables-compatible test sets | Julia application using `DBInterface.execute` and Tables rows | +| Core | Tcl | [`tdbc::odbc`](https://core.tcl-lang.org/tdbc) | `tcltest` suites for the ODBC backend | Tcl application using prepared statements and result-set iteration | +| Expansion | Raku | [DBDish::ODBC](https://github.com/salortiz/DBDish-ODBC) | Upstream `t/` tests and DBIish common tests supported by the adapter | Raku application using DBIish connection and statement handles | +| Expansion | Crystal | [crystal-odbc](https://github.com/naqvis/crystal-odbc) | Complete `crystal spec` suite | Crystal application using the `crystal-db` API | +| Expansion | Dart | [`dart_odbc`](https://pub.dev/packages/dart_odbc) | Complete `dart test` suite | Dart CLI application using prepared execution and typed rows | +| Expansion | D | [`odbc`](https://github.com/singingbush/odbc) | Upstream unit tests and integration-test executable | D application using the package connection and result APIs | +| Expansion | OCaml | [`ocaml-odbc`](https://opam.ocaml.org/packages/odbc/) | Upstream database tests when present; otherwise the shared contract | Dune application using prepared execution and row conversion | +| Expansion | Common Lisp | [CLSQL ODBC](https://github.com/sharplispers/clsql) | ODBC-applicable ASDF test systems when present; otherwise the shared contract | SBCL application using CLSQL query and transaction APIs | +| Expansion | COBOL | [GixSQL ODBC](https://github.com/mridoni/gixsql) | ODBC-capable GixSQL regression cases and examples | GnuCOBOL application using embedded SQL, a cursor and commit/rollback | +| Expansion | Pascal | [Free Pascal SQLDB ODBC](https://gitlab.com/freepascal.org/fpc/source/-/tree/main/packages/fcl-db) | FPCUnit SQLDB connector tests | Free Pascal application using `TODBCConnection`, `TSQLQuery` and `TSQLTransaction` | +| Expansion | Smalltalk | [Pharo-ODBC](https://github.com/pharo-rdbms/Pharo-ODBC) | Upstream SUnit tests | Headless Pharo example using connection, statement and result objects | +| Expansion | Fortran | [`odbc.f`](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests when present; otherwise the shared contract | Fortran application using connection, result-set and column-set objects | + +Core jobs run on every pull request once their baseline is stable. Expansion jobs run nightly while being onboarded and are promoted to the pull-request matrix after they produce deterministic results. Adding a new language requires one matrix row, not a second framework for a language already represented. + +## YDB compatibility policy + +YDB requires a primary key for every table. The ODBC driver must not synthesize keys, inject hidden columns, rewrite application DML to maintain hidden values or hide physical columns from metadata. Applications using YDB are responsible for providing a sound key, and framework test fixtures should be patched to do the same. + +YDB and YQL also differ from other relational databases in namespace structure, supported types, DDL, common-table-expression syntax, stored procedures, identity columns, result-set capabilities and transaction modes. These differences should be handled either by a semantics-preserving driver feature or by an explicit framework test patch when the test encodes a database-specific assumption rather than an ODBC requirement. + +## Implementation details + +### Driver architecture + +The exported ODBC C ABI should remain in `src/odbc_driver.cpp`, while connection state, statement execution, descriptors, diagnostics and result-set handling remain in the existing handle classes. Compatibility behavior should be implemented in a new internal `src/compatibility/` module instead of being distributed across exported API functions. + +`TStatement::ExecuteQuery()` should use the following pipeline: + +1. Apply the existing ODBC escape translation unless `SQL_NOSCAN` is enabled. +2. Tokenize the statement while preserving string literals, quoted identifiers, comments and parameter markers. +3. Apply semantics-preserving YDB compatibility rewrites for common table expressions, identifiers and namespace resolution; do not change table keys or application data. +4. Apply the existing `?` to `$pN` rewrite and generate typed `DECLARE` statements from the bound ODBC parameters. +5. Add the compatibility pragmas required by the statement and apply the current catalog with `TConnection::WrapQueryForCurrentCatalog()`. +6. Execute the final YQL through the Query Service and translate YDB status and result metadata back to ODBC diagnostics and types. + +`SQLNativeSql` should run the same translation pipeline without executing the statement. This makes the API useful for diagnosing the exact YQL that the driver will submit and prevents it from disagreeing with `SQLPrepare` and `SQLExecDirect`. + +### Primary-key behavior + +The driver should submit `CREATE TABLE` statements without inventing a physical schema. If a table has no primary key, it should return the YDB failure through the normal ODBC diagnostic chain with an appropriate SQLSTATE and the native YDB issue text. Driver tests should verify correct diagnostics, while every framework fixture patch should add a deterministic key and update its inserts and expected metadata consistently. + +### WITH-clause translation + +The compatibility parser should translate each non-recursive CTE to a collision-free YQL named expression. It must first collect every `$identifier` in the complete query, allocate a deterministic unused name such as `$_odbc_cte_s0_n0_`, and maintain a scope-aware mapping from the ANSI relation name to that generated expression. Multiple CTEs must be emitted in dependency order, table references must be rewritten only in the correct scope, and existing declared parameters or global YQL named expressions must remain unchanged. + +The initial implementation must cover chained CTEs, multiple references to one CTE, nested subqueries, CTE column aliases, quoted identifiers and statements containing ODBC parameters. `WITH RECURSIVE`, data-modifying CTEs and unsupported materialization modifiers should remain explicit driver gaps until they have a semantics-preserving implementation. + +Tests must include keywords inside strings and comments, nested and shadowed CTE names, an existing `$cte` variable, multiple CTEs and failure diagnostics for unsupported recursive syntax. + +### Catalog and directory mapping + +The implementation should build on the current catalog support rather than introduce an independent schema model. An ODBC catalog is a normalized absolute YDB database or directory path, the schema component is empty, `/` is the catalog separator, and `SQL_ATTR_CURRENT_CATALOG` changes the path used by `PRAGMA TablePathPrefix`. + +Qualified-name resolution and metadata filters must use one shared normalizer. It must handle quoted path components, absolute and current-catalog-relative table names, repeated separators and attempts to traverse above the configured database root. `SQLTables`, `SQLColumns`, `SQLPrimaryKeys`, `SQLStatistics` and query execution must resolve the same logical name to the same physical YDB path. + +If an upstream suite proves that a non-empty schema is required, schema support should be implemented as a tested directory alias layer on top of this mapping; returning inconsistent schema values only to satisfy metadata assertions is not acceptable. + +### Multiple databases and hosts + +Each `SQLHDBC` must own an endpoint, database path, credentials, TLS settings, clients, sessions, transaction and current catalog. A single `SQLHENV` may contain many independently configured connection handles targeting different databases on the same host or databases on different hosts. Statements always execute through their parent connection, and disconnecting or failing one connection must not affect another. + +One connection string identifies one YDB discovery endpoint and one database. A multi-node YDB database should use its discovery or load-balancer endpoint rather than exposing a host list through ODBC. Cross-connection transactions are not atomic: `SQLEndTran(SQL_HANDLE_ENV, ...)` may iterate over connections, but it must not be described as a distributed transaction. + +Integration tests must cover two databases on one endpoint, two endpoints, concurrent queries, independent commit/rollback, isolated credentials and catalog state, failure of one endpoint, and driver-manager pooling without returning a connection for the wrong endpoint/database pair. + +### YDB, YQL and driver boundaries + +The required physical primary key and hierarchical object namespace are YDB constraints. YQL named expressions, parameter declarations, identifier quoting and unsupported ANSI constructs are language-level constraints. Patching framework fixtures to use a valid YDB schema, implementing semantics-preserving SQL translation, mapping catalogs, preserving ODBC transaction semantics and returning correct SQLSTATE diagnostics are project responsibilities. + +Every failing framework test should be assigned to one of these boundaries in Allure. A portable ODBC behavior should be implemented in the driver when it can be provided without falsifying YDB semantics. A database-specific fixture or assertion should be patched or marked not applicable with a precise reason. A case may be classified as server-blocked only when no sound driver implementation or fixture adaptation exists, and it must remain visible with the exact server limitation documented. + +### Capability reporting + +`SQLGetInfo` and `SQLGetFunctions` must describe implemented behavior, not intended behavior. Each compatibility feature should therefore land with both execution tests and capability-reporting tests. Unsupported procedures, multiple result sets, scrollable cursors, asynchronous execution and batch operations must continue to report unsupported until their complete API behavior is implemented. + +## Framework test implementation + +### Repository layout + +The repository should keep one self-contained integration directory per language: + +```text +odbc/tests/frameworks/ + registry.yaml + / + upstream.lock + patches/ + test-manifest.yaml + run-tests + convert-results + example/ +odbc/tests/reporting/ +odbc/tests/performance/php/ +``` + +`registry.yaml` is the source of truth for the CI matrix and records the language, framework, tier, runtime image, upstream URL, revision, archive checksum, patch directory, test command, native result format and example command. `upstream.lock` repeats the immutable source identity inside each integration directory so a language can be reproduced independently. + +### Patch policy + +Upstream framework and binding implementation code must remain unchanged. Test files, test fixtures and test-only configuration may be patched when necessary to make the suite sound for YDB. Patches should be stored as ordered files under `/patches/` and applied to a clean pinned checkout during the CI job; the repository must not maintain an opaque fork. + +Allowed patches include: + +- Add explicit primary keys to fixture DDL and update fixture inserts and expected key metadata consistently. +- Replace another database's vendor-specific setup SQL with equivalent YDB/YQL setup. +- Use YDB-supported types where the original type is vendor-specific and the test is not testing that exact ODBC type. +- Map flat schemas, temporary database names or database creation steps to isolated YDB directories. +- Adapt expected database-specific error text while preserving the expected SQLSTATE class and operation outcome. +- Mark a test not applicable when it requires a database feature YDB does not provide and the driver accurately reports that capability as unsupported. + +Patches must not modify the framework or binding implementation, remove tests without a manifest entry, weaken assertions for advertised ODBC behavior, replace framework calls with direct YDB calls, turn a crash/hang/data corruption failure into an expected failure, or hide a driver regression behind a YDB limitation. + +Every patch file must have a matching manifest record containing a stable patch ID, affected upstream test IDs, category (`YDB_PRIMARY_KEY`, `YDB_NAMESPACE`, `YQL_SYNTAX`, `YDB_TYPE`, `UNSUPPORTED_CAPABILITY` or `VENDOR_SPECIFIC`), rationale and link to the relevant YDB/YQL limitation. CI must verify the upstream checksum, run `git apply --check`, apply the ordered series and publish both the patch manifest and resulting tree hash. + +### Shared test contract + +Frameworks with an upstream database suite run that suite after applying the reviewed patches. Frameworks without a useful upstream integration suite run a repository-owned shared contract through the public API of the selected binding. The shared contract is not a replacement for upstream tests when upstream tests exist. + +The shared contract covers connection and disconnection, invalid connection diagnostics, multiple connections, direct execution, preparation and rebinding, scalar and tabular results, `NULL`, integer, floating-point, decimal, UTF-8, binary and date/time values, metadata, affected-row counts, commit, rollback, autocommit, concurrent independent connections, cleanup after errors and resource finalization. Cases for optional ODBC features run only when capability discovery reports them as supported. + +### Example applications + +Every language directory must contain a small executable example and a README with exact dependency installation and run commands. The example accepts `YDB_ODBC_DSN` or `YDB_ODBC_CONNECTION_STRING`, creates an isolated table with an explicit primary key, performs a parameterized insert, reads and prints typed rows, demonstrates commit and rollback, and removes its table. It must use only the selected language framework's public API and must run in CI after the tests. + +Examples should share the same logical `people(id, name, score, created_at)` schema while remaining idiomatic for their language. They are product artifacts, not test patches, and should be suitable for copying into user documentation. + +### Native result and Allure contract + +Every launcher must preserve the framework's native output and produce a normalized result containing the stable upstream test identifier, status, duration, stdout, stderr and setup/teardown phase. Native formats such as Common Test logs, PHPT output, TAP, JUnit/XML, FPCUnit XML or framework-specific text should be converted externally rather than by editing the upstream runner. + +Allure results should use the hierarchy `ODBC / / / ` and include the framework version, runtime version, driver commit, YDB version, endpoint/database mode, upstream checksum, patched-tree hash and applied patch IDs. The history identifier must be derived from the language, pinned upstream revision and original upstream test identifier. + +Failing tests should attach the SQLSTATE chain, native YDB issue text, translated YQL from `SQLNativeSql`, framework output and relevant server logs with secrets removed. Missing tests, an empty suite, patch-application failure, infrastructure failure or an unexpected skip should produce synthetic broken results and fail the job so a reduced test count cannot look like progress. + +### CI workflow + +A framework workflow should run for pull requests targeting `odbc-driver-feature`, pushes to that branch, nightly schedules and manual dispatches. It should build the driver once and generate its matrix from `registry.yaml`. Core languages run on every pull request; all Core and Expansion languages run nightly and on manual full-matrix requests. + +Each job should start the same pinned YDB version, wait for readiness, create an isolated database prefix, register the driver in a job-local `odbcinst.ini`, fetch and verify upstream source, apply the reviewed patch series, run upstream tests, run the shared contract when required, run the example, and upload native plus Allure results even on failure. A final `if: always()` job validates manifests, merges results, builds the HTML report and publishes the raw results, rendered report, patch manifests and example logs. + +The baseline may contain known failures while support is being implemented, but each pull request must satisfy an incremental gate: no passing test or example regresses, no test disappears, the targeted behavior becomes passing, and upstream or patch changes are explicit. Once a language is green, its gate switches to zero failed, broken, missing or unexpected skipped cases. + +The development loop is: inspect aggregated Allure failures, decide whether each failure is a driver defect or an unsound database assumption, add a focused driver regression test or a documented test patch, rerun the affected language and existing ODBC suites, then merge into `odbc-driver-feature`. The branch must be rebased and the applicable matrix rerun when its base advances. + +## Performance test implementation + +The PHP comparison must use one repository-owned workload implementation with two thin backends: native PHP SDK `ExecuteQuery` and PDO_ODBC. Both backends must use the same PHP runtime, YDB server, schema, seed data, query text, parameter values, connection lifetime, concurrency schedule, retry policy and result validation. + +The benchmark should separate operation types rather than compare unlike APIs: + +- Result-returning statements: native `ExecuteQuery` versus PDO `prepare`/`execute` plus the same row fetch and decoding work. +- Non-result statements: native `ExecuteQuery` versus PDO `exec` or prepared execution with identical transaction semantics. +- Connection setup: measured separately from steady-state execution so pooling and driver initialization costs remain visible. + +The workload should include point reads, parameterized range reads, inserts, updates and an explicit transaction. It should run a warm-up phase followed by at least five paired 600-second samples in alternating backend order on the same isolated runner. Rate limiting, concurrency and data-set size must be explicit inputs and recorded in the result. + +The workflow should publish requests per second, p50/p95/p99/p99.9 latency, error rate, retry count, CPU time per operation and peak resident memory. Each operation must validate returned row counts or affected-row counts so a faster error or empty result cannot be reported as an improvement. + +The regression gate should compare paired samples with confidence intervals. PHP PDO_ODBC passes when throughput is not lower and p95/p99 latency is not higher than the pinned native PHP SDK by more than the agreed tolerance; the performance target is for ODBC to outperform the deprecated SDK, but correctness and a statistically stable no-regression gate come first. Any intentional change to the SDK version, YDB version, runner class or workload invalidates the stored baseline and requires a new reviewed baseline. + +## Delivery order + +1. Add `registry.yaml`, the framework directory template, source verification, patch verification, native-result conversion and Allure aggregation. +2. Onboard the Core languages with pinned upstream suites, reviewed YDB patches, test manifests and runnable examples. +3. Add the shared contract for bindings with incomplete upstream integration coverage. +4. Extract the shared SQL compatibility pipeline, make `SQLNativeSql` use it and implement collision-safe non-recursive WITH translation. +5. Finish catalog normalization, per-connection authentication/TLS configuration and multiple-host/multiple-database isolation tests. +6. Onboard Expansion languages one at a time and promote each stable job into the pull-request matrix. +7. Resolve remaining Allure failures as driver fixes or reviewed database-specific test patches, with a focused regression test or patch rationale for every change. +8. Add the paired PHP performance workflow and establish the reviewed native-SDK baseline. +9. Enable zero-regression gates for every stable language and the PHP performance gate on `odbc-driver-feature`. + +The final acceptance evidence is a commit-specific Allure report for every registered language, the original and patched upstream source identities, patch manifests, example logs, existing ODBC unit/integration/conformance results, multiple-endpoint results and the paired PHP performance report. A result is not acceptable if implementation code in a selected framework was modified, a test was omitted without a manifest record, a patch masks advertised ODBC behavior, capability reporting overstates the driver, an example does not run, or the benchmark compares different semantics. diff --git a/odbc/tests/conformance/CMakeLists.txt b/odbc/tests/conformance/CMakeLists.txt index 9b336ba4265..7e66679baee 100644 --- a/odbc/tests/conformance/CMakeLists.txt +++ b/odbc/tests/conformance/CMakeLists.txt @@ -1,8 +1,226 @@ -add_odbc_test(NAME odbc-core-conformance_it - SOURCES - core_conformance_it.cpp - LABELS - odbc - core-conformance - linux +include(FetchContent) + +set(_odbc_external_test_environment + "ODBCINI=${YDB_ODBC_TEST_CONFIG_DIR}/odbc.ini" + "ODBCSYSINI=${YDB_ODBC_TEST_CONFIG_DIR}" ) + +# unixODBC-Test is the public ODBC API conformance corpus. Its build files +# require qmake and the interactive ODBCTest host, so compile the headless +# TestFarm programs directly without changing their sources. +FetchContent_Declare( + unixodbc_test + SVN_REPOSITORY https://svn.code.sf.net/p/unixodbc-test/code + SVN_REVISION -r19 + UPDATE_DISCONNECTED ON +) +FetchContent_MakeAvailable(unixodbc_test) + +add_library(unixodbc-test-v3-common STATIC + "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/tst.c" + "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/o3util.c" + "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/o3utdesc.c" + "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/uniutil.c" +) +target_include_directories(unixodbc-test-v3-common + PUBLIC + "${unixodbc_test_SOURCE_DIR}/TestFarm/common" + "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common" +) +target_compile_definitions(unixodbc-test-v3-common + PUBLIC + # Removed from modern 64-bit unixODBC headers, but used by this ODBC 3.x + # corpus as the row-offset-sized integer type. + SQLROWOFFSET=SQLLEN +) +target_link_libraries(unixodbc-test-v3-common PUBLIC ODBC::ODBC) +set_target_properties(unixodbc-test-v3-common PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED ON +) +if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(unixodbc-test-v3-common PRIVATE -w) +endif() + +function(add_unixodbc_testfarm_test name source) + set(target_name "unixodbc-test-${name}") + add_executable(${target_name} "${unixodbc_test_SOURCE_DIR}/${source}") + target_link_libraries(${target_name} + PRIVATE + unixodbc-test-v3-common + ODBC::ODBC + ) + set_target_properties(${target_name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED ON + ) + if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(${target_name} PRIVATE -w) + endif() + add_dependencies(${target_name} ydb-odbc) + + add_test(NAME "unixodbc-test.${name}" + COMMAND $ ${ARGN} + ) + set_tests_properties("unixodbc-test.${name}" PROPERTIES + ENVIRONMENT "${_odbc_external_test_environment}" + LABELS "integration;odbc;core-conformance;unixodbc-test" + TIMEOUT 60 + ) +endfunction() + +# The upstream TestFarm's formal Core directory contains the two allocation +# tests. The remaining programs are its Core-relevant, SQL-dialect-independent +# conformance cases from the historical Other directory. +add_unixodbc_testfarm_test(alloc-handle-env + "TestFarm/v3/conformance/Core/AllocHandle-ENV/test.c") +add_unixodbc_testfarm_test(alloc-handle-dbc + "TestFarm/v3/conformance/Core/AllocHandle-DBC/test.c") +add_unixodbc_testfarm_test(connect + "TestFarm/v3/conformance/Other/connect/connect.c" YDB) +add_unixodbc_testfarm_test(get-functions + "TestFarm/v3/conformance/Other/getfunc/getfunc.c" YDB) +add_unixodbc_testfarm_test(get-info + "TestFarm/v3/conformance/Other/getinfo/getinfo.c" YDB) +add_unixodbc_testfarm_test(get-type-info + "TestFarm/v3/conformance/Other/gettinf/gettinf.c" YDB) +add_unixodbc_testfarm_test(native-sql + "TestFarm/v3/conformance/Other/nativsql/nativsql.c" YDB) +add_unixodbc_testfarm_test(connection-attributes + "TestFarm/v3/conformance/Other/getconop/getconop.c" YDB) +add_unixodbc_testfarm_test(attribute-roundtrip + "TestFarm/v3/conformance/Other/getset/getset.c" YDB) + +# SOCI's ODBC backend uses only Core-classified ODBC entry points. Compile its +# upstream common test sources unchanged; the local translation unit supplies +# only the database schema and capability adapter required by SOCI's harness. +set(SOCI_SHARED OFF CACHE BOOL "" FORCE) +set(SOCI_TESTS OFF CACHE BOOL "" FORCE) +set(SOCI_LTO OFF CACHE BOOL "" FORCE) +set(WITH_BOOST OFF CACHE BOOL "" FORCE) +set(SOCI_DB2 OFF CACHE STRING "" FORCE) +set(SOCI_EMPTY OFF CACHE STRING "" FORCE) +set(SOCI_FIREBIRD OFF CACHE STRING "" FORCE) +set(SOCI_MYSQL OFF CACHE STRING "" FORCE) +set(SOCI_ODBC ON CACHE STRING "" FORCE) +set(SOCI_ORACLE OFF CACHE STRING "" FORCE) +set(SOCI_POSTGRESQL OFF CACHE STRING "" FORCE) +set(SOCI_SQLITE3 OFF CACHE STRING "" FORCE) +FetchContent_Declare( + soci + GIT_REPOSITORY https://github.com/SOCI/soci.git + GIT_TAG 4c1578d09ebbe324fbe1f00661146b483de88be2 + UPDATE_DISCONNECTED ON +) +FetchContent_MakeAvailable(soci) + +add_executable(soci-odbc-core-tests + "${soci_SOURCE_DIR}/tests/common/test-boost.cpp" + "${soci_SOURCE_DIR}/tests/common/test-common.cpp" + "${soci_SOURCE_DIR}/tests/common/test-connparams.cpp" + "${soci_SOURCE_DIR}/tests/common/test-custom.cpp" + "${soci_SOURCE_DIR}/tests/common/test-dynamic.cpp" + "${soci_SOURCE_DIR}/tests/common/test-lob.cpp" + "${soci_SOURCE_DIR}/tests/common/test-main.cpp" + "${soci_SOURCE_DIR}/tests/common/test-manual.cpp" + "${soci_SOURCE_DIR}/tests/common/test-rowset.cpp" + "${soci_SOURCE_DIR}/tests/common/test-unicode.cpp" + soci_odbc_test_context.cpp +) +target_include_directories(soci-odbc-core-tests + PRIVATE + "${soci_SOURCE_DIR}/include/private" + "${soci_SOURCE_DIR}/tests" +) +target_compile_definitions(soci-odbc-core-tests + PRIVATE + CATCH_CONFIG_CPP11_NO_SHUFFLE +) +target_compile_features(soci-odbc-core-tests PRIVATE cxx_std_14) +target_link_libraries(soci-odbc-core-tests + PRIVATE + SOCI::Core + SOCI::ODBC +) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(soci-odbc-core-tests PRIVATE -Wno-deprecated-declarations) +endif() +add_dependencies(soci-odbc-core-tests ydb-odbc) + +set(_soci_odbc_connection_string + "Driver=$;Server=${YDB_ODBC_DSN_SERVER};Database=${YDB_ODBC_DSN_DATABASE}" +) +set(_soci_odbc_core_test_cases + "timegm implementation" + "Exception on not connected" + "Basic functionality" + "Use and into" + "Repeated and bulk fetch" + "Indicators" + "Indicators vector" + "Use type conversion" + "Multiple use and into" + "Use vector" + "Named parameters" + "Named parameters with similar names" + "Use with indicators" + "Numeric round trip" + "Bulk fetch with single use" + "Basic logging support" + "soci_error is nothrow" + "NULL with std optional" + "Connection and reconnection" + "Connection pool" + "Query transformation" + "Query transformation with connection pool" + "Backend memory leak" + "Bind memory leak" + "std::tm timestamp problem with DST" + "Truncation error" + "Select without table" + "String length" + "Logger" + "Connection string parsing" + "connection_parameters::extract_option" + "connection_parameters::build_string_from_options" + "Use vector of custom type objects" + "Into vector of custom type objects" + "Prepared insert with ORM" + "Partial match with ORM" + "Dynamic row binding" + "Dynamic row binding 2" + "Dynamic row binding 3" + "Dynamic binding with type conversions" + "Dynamic row binding 4" + "Dynamic binding with rowset" + "CLOB" + "CLOB vector" + "Rowset creation and copying" + "Rowset iteration" + "Reading rows from rowset" + "Reading ints from rowset" + "Reading strings from rowset" + "Rowset expected exception" + "NULL expected exception" +) + +# Deliberately not registered: upstream cases which the SOCI harness would +# capability-skip (identity, affected rows, XML, and BLOB), plus cases requiring +# SQL_AUTOCOMMIT_OFF/rollback. Core-level ODBC drivers need not support true +# transactions, so those are not valid tests of a Core-only framework path. + +foreach(test_case IN LISTS _soci_odbc_core_test_cases) + string(MAKE_C_IDENTIFIER "${test_case}" test_id) + string(TOLOWER "${test_id}" test_id) + add_test(NAME "soci-odbc.${test_id}" + COMMAND $ + "${_soci_odbc_connection_string}" + "${test_case}" + ) + set_tests_properties("soci-odbc.${test_id}" PROPERTIES + ENVIRONMENT "${_odbc_external_test_environment}" + LABELS "integration;odbc;core-framework;framework-compatibility;soci" + RESOURCE_LOCK soci_odbc_test_table + TIMEOUT 120 + ) +endforeach() diff --git a/odbc/tests/conformance/README.md b/odbc/tests/conformance/README.md deleted file mode 100644 index a5b19de0ffc..00000000000 --- a/odbc/tests/conformance/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# ODBC Core conformance audit - -This directory contains a Linux/headless audit for the ODBC 3.x **Core interface -conformance** contract. It deliberately tests only Core requirements; claiming -Level 1 or Level 2 is outside its scope. It is a regression and gap-finding -suite, not an official certification program. - -The executable talks through unixODBC, not through driver internals. It checks: - -- the declared interface level and the complete mandatory function bitmap; -- Driver Manager enumeration entry points; -- Core environment, connection, and statement attributes; -- parameter/row arrays, bind offsets, and conservative array capabilities; -- application descriptor assignment, binding, lifetime, and mandatory fields; -- pre-`SQLExecDirect` bindings and scalar data-at-execution sequencing; -- numeric/text conversion, partial `SQLGetData`, and diagnostic lifetime; -- transaction completion edge semantics; -- standard result shapes for the Core catalog functions. - -The existing integration tests under `../integration` provide deeper behavioral -coverage for connection lifecycle, statement execution, binding and fetching, -data-at-execution, transactions, diagnostics, metadata, and cursor operations. - -## Running - -A YDB server must be available at `localhost:2136`, with database `/local`. - -```bash -cmake --preset release-test-clang -cmake --build build --target odbc-core-conformance_it -j$(nproc) -ctest --test-dir build -L core-conformance --output-on-failure -``` - -Failures are intentional evidence of missing Core behavior. Do not convert a -mandatory failure into a skip unless the cited ODBC conformance contract says -the feature is optional or belongs to a higher level. - -## Open-source solutions evaluated - -Research was refreshed on 2026-07-14. - -| Project | Evaluated revision | Useful parts | Why it was not vendored | -|---|---:|---|---| -| [unixODBC-Test](https://sourceforge.net/projects/unixodbc-test/) | SVN r19 (2018-03-08) | The closest generic corpus: AutoTests, TestFarm, and a `MyODBC3/funccore` API set | GPLv2; the broad AutoTest corpus is ODBC 2-era, requires Qt plus the ODBCTest GUI/gtrtst library, and is not a headless CTest suite. TestFarm's ODBC 3 Core directory contains only handle-allocation tests. | -| [Microsoft ODBCTest](https://github.com/microsoft/ODBCTest/tree/0d629c7e4ff7b01398a5ac71d20c43362d0f43bf) | `0d629c7e` | MIT-licensed interactive API exerciser and reference implementation | Visual Studio/Windows GUI application, interactive rather than an automated Linux conformance corpus. | -| [pyodbc](https://github.com/mkleehammer/pyodbc/tree/9fd386c370288d802ddec58e5580fe1ec9adeade) | `9fd386c3` | Mature real-client compatibility tests | Tests the Python DB-API wrapper and database-specific SQL, not raw driver Core conformance. | -| [nanodbc](https://github.com/nanodbc/nanodbc/tree/fd9b4f551b0f03780168c4b2ba880dcb5777aad4) | `fd9b4f55` | Portable C++ client smoke and type-conversion tests | Tests the nanodbc wrapper and is configured around a SQLite ODBC data source. | - -The audit is implemented locally instead of copying those sources. That keeps -the repository Apache-2.0-only, avoids a GUI/runtime dependency, and makes every -assertion traceable to the current ODBC 3.x Core contract: - -- [Core Interface Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/core-interface-conformance) -- [Function Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/function-conformance) -- [Attribute Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/attribute-conformance) -- [Descriptor Field Conformance](https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/descriptor-field-conformance) diff --git a/odbc/tests/conformance/core_conformance_it.cpp b/odbc/tests/conformance/core_conformance_it.cpp deleted file mode 100644 index 3dc3c31dc02..00000000000 --- a/odbc/tests/conformance/core_conformance_it.cpp +++ /dev/null @@ -1,1036 +0,0 @@ -#include "../integration/test_utils.h" - -#include -#include -#include -#include -#include -#include - -namespace { - -bool IsOdbcSuccess(SQLRETURN rc) { - return rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO; -} - -::testing::AssertionResult OdbcSuccess( - SQLRETURN rc, - SQLHANDLE handle, - SQLSMALLINT handleType) { - if (IsOdbcSuccess(rc)) { - return ::testing::AssertionSuccess(); - } - return ::testing::AssertionFailure() - << "ODBC return code " << rc << ": " << GetOdbcError(handle, handleType); -} - -struct TCoreFunction { - SQLUSMALLINT Id; - const char* Name; -}; - -// ODBC 3.x Core functions from the Microsoft/Open Group conformance table. -// SQLDataSources and SQLDrivers are Driver Manager functions and are exercised -// separately; all entries below must be advertised by the connected driver. -constexpr std::array CoreDriverFunctions{{ - {SQL_API_SQLALLOCHANDLE, "SQLAllocHandle"}, - {SQL_API_SQLBINDCOL, "SQLBindCol"}, - {SQL_API_SQLBINDPARAMETER, "SQLBindParameter"}, - {SQL_API_SQLCANCEL, "SQLCancel"}, - {SQL_API_SQLCLOSECURSOR, "SQLCloseCursor"}, - {SQL_API_SQLCOLATTRIBUTE, "SQLColAttribute"}, - {SQL_API_SQLCOLUMNS, "SQLColumns"}, - {SQL_API_SQLCONNECT, "SQLConnect"}, - {SQL_API_SQLCOPYDESC, "SQLCopyDesc"}, - {SQL_API_SQLDESCRIBECOL, "SQLDescribeCol"}, - {SQL_API_SQLDISCONNECT, "SQLDisconnect"}, - {SQL_API_SQLDRIVERCONNECT, "SQLDriverConnect"}, - {SQL_API_SQLENDTRAN, "SQLEndTran"}, - {SQL_API_SQLEXECDIRECT, "SQLExecDirect"}, - {SQL_API_SQLEXECUTE, "SQLExecute"}, - {SQL_API_SQLFETCH, "SQLFetch"}, - {SQL_API_SQLFETCHSCROLL, "SQLFetchScroll"}, - {SQL_API_SQLFREEHANDLE, "SQLFreeHandle"}, - {SQL_API_SQLFREESTMT, "SQLFreeStmt"}, - {SQL_API_SQLGETCONNECTATTR, "SQLGetConnectAttr"}, - {SQL_API_SQLGETCURSORNAME, "SQLGetCursorName"}, - {SQL_API_SQLGETDATA, "SQLGetData"}, - {SQL_API_SQLGETDESCFIELD, "SQLGetDescField"}, - {SQL_API_SQLGETDESCREC, "SQLGetDescRec"}, - {SQL_API_SQLGETDIAGFIELD, "SQLGetDiagField"}, - {SQL_API_SQLGETDIAGREC, "SQLGetDiagRec"}, - {SQL_API_SQLGETENVATTR, "SQLGetEnvAttr"}, - {SQL_API_SQLGETFUNCTIONS, "SQLGetFunctions"}, - {SQL_API_SQLGETINFO, "SQLGetInfo"}, - {SQL_API_SQLGETSTMTATTR, "SQLGetStmtAttr"}, - {SQL_API_SQLGETTYPEINFO, "SQLGetTypeInfo"}, - {SQL_API_SQLNATIVESQL, "SQLNativeSql"}, - {SQL_API_SQLNUMPARAMS, "SQLNumParams"}, - {SQL_API_SQLNUMRESULTCOLS, "SQLNumResultCols"}, - {SQL_API_SQLPARAMDATA, "SQLParamData"}, - {SQL_API_SQLPREPARE, "SQLPrepare"}, - {SQL_API_SQLPUTDATA, "SQLPutData"}, - {SQL_API_SQLROWCOUNT, "SQLRowCount"}, - {SQL_API_SQLSETCONNECTATTR, "SQLSetConnectAttr"}, - {SQL_API_SQLSETCURSORNAME, "SQLSetCursorName"}, - {SQL_API_SQLSETDESCFIELD, "SQLSetDescField"}, - {SQL_API_SQLSETDESCREC, "SQLSetDescRec"}, - {SQL_API_SQLSETENVATTR, "SQLSetEnvAttr"}, - {SQL_API_SQLSETSTMTATTR, "SQLSetStmtAttr"}, - {SQL_API_SQLSPECIALCOLUMNS, "SQLSpecialColumns"}, - {SQL_API_SQLSTATISTICS, "SQLStatistics"}, - {SQL_API_SQLTABLES, "SQLTables"}, -}}; - -void ExpectDescriptorField( - SQLHDESC desc, - SQLSMALLINT record, - SQLSMALLINT field, - const char* name, - bool characterField = false) { - alignas(std::max_align_t) std::array storage{}; - SQLINTEGER length = 0; - const SQLRETURN rc = SQLGetDescField( - desc, - record, - field, - storage.data(), - characterField ? static_cast(storage.size()) : 0, - &length); - EXPECT_TRUE(OdbcSuccess(rc, desc, SQL_HANDLE_DESC)) << "field " << name; -} - -class OdbcCoreConformance : public ::testing::Test { -protected: - void SetUp() override { - AllocEnvAndConnect(&Env_, &Dbc_); - } - - void TearDown() override { - if (Dbc_ != SQL_NULL_HDBC) { - SQLDisconnect(Dbc_); - SQLFreeHandle(SQL_HANDLE_DBC, Dbc_); - } - if (Env_ != SQL_NULL_HENV) { - SQLFreeHandle(SQL_HANDLE_ENV, Env_); - } - } - - SQLHSTMT AllocStatement() { - SQLHSTMT stmt = SQL_NULL_HSTMT; - EXPECT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &stmt), SQL_SUCCESS); - return stmt; - } - - SQLHENV Env_ = SQL_NULL_HENV; - SQLHDBC Dbc_ = SQL_NULL_HDBC; -}; - -} // namespace - -TEST_F(OdbcCoreConformance, DeclaresAtLeastCoreInterfaceConformance) { - // SQL_ATTR_ODBC_VERSION is the only Core-level environment attribute. - SQLINTEGER version = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetEnvAttr(Env_, SQL_ATTR_ODBC_VERSION, &version, sizeof(version), nullptr), - Env_, - SQL_HANDLE_ENV)); - EXPECT_EQ(version, SQL_OV_ODBC3); - - SQLUINTEGER level = 0; - SQLSMALLINT length = 0; - const SQLRETURN rc = SQLGetInfo( - Dbc_, SQL_ODBC_INTERFACE_CONFORMANCE, &level, sizeof(level), &length); - ASSERT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)); - EXPECT_GE(level, static_cast(SQL_OIC_CORE)); - EXPECT_EQ(length, sizeof(level)); -} - -TEST_F(OdbcCoreConformance, AdvertisesEveryMandatoryDriverFunction) { - std::array bitmap{}; - ASSERT_TRUE(OdbcSuccess( - SQLGetFunctions(Dbc_, SQL_API_ODBC3_ALL_FUNCTIONS, bitmap.data()), - Dbc_, - SQL_HANDLE_DBC)); - - for (const auto& function : CoreDriverFunctions) { - SQLUSMALLINT supported = SQL_FALSE; - const SQLRETURN rc = SQLGetFunctions(Dbc_, function.Id, &supported); - EXPECT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)) << function.Name; - EXPECT_EQ(supported, SQL_TRUE) << function.Name; - EXPECT_EQ(SQL_FUNC_EXISTS(bitmap.data(), function.Id), SQL_TRUE) - << function.Name << " missing from SQL_API_ODBC3_ALL_FUNCTIONS"; - } -} - -TEST_F(OdbcCoreConformance, DriverManagerEnumerationFunctionsAreUsable) { - SQLCHAR description[256] = {}; - SQLCHAR attributes[512] = {}; - SQLSMALLINT descriptionLength = 0; - SQLSMALLINT attributesLength = 0; - SQLRETURN rc = SQLDrivers( - Env_, - SQL_FETCH_FIRST, - description, - sizeof(description), - &descriptionLength, - attributes, - sizeof(attributes), - &attributesLength); - EXPECT_TRUE(IsOdbcSuccess(rc) || rc == SQL_NO_DATA) << "SQLDrivers returned " << rc; - - SQLCHAR dsn[256] = {}; - SQLCHAR dsnDescription[256] = {}; - SQLSMALLINT dsnLength = 0; - SQLSMALLINT dsnDescriptionLength = 0; - rc = SQLDataSources( - Env_, - SQL_FETCH_FIRST, - dsn, - sizeof(dsn), - &dsnLength, - dsnDescription, - sizeof(dsnDescription), - &dsnDescriptionLength); - EXPECT_TRUE(IsOdbcSuccess(rc) || rc == SQL_NO_DATA) << "SQLDataSources returned " << rc; -} - -TEST_F(OdbcCoreConformance, CoreConnectionAttributesFollowUnsetAndRoundTripSemantics) { - const std::array, 3> integerAttributes{{ - {SQL_ATTR_ACCESS_MODE, "SQL_ATTR_ACCESS_MODE"}, - {SQL_ATTR_ODBC_CURSORS, "SQL_ATTR_ODBC_CURSORS"}, - {SQL_ATTR_TRACE, "SQL_ATTR_TRACE"}, - }}; - for (const auto& [attribute, name] : integerAttributes) { - SQLULEN value = 0; - SQLINTEGER length = 0; - const SQLRETURN rc = SQLGetConnectAttr( - Dbc_, attribute, &value, sizeof(value), &length); - EXPECT_TRUE(OdbcSuccess(rc, Dbc_, SQL_HANDLE_DBC)) << name; - } - - SQLCHAR traceFile[512] = {}; - SQLINTEGER traceFileLength = 0; - EXPECT_TRUE(OdbcSuccess( - SQLGetConnectAttr( - Dbc_, SQL_ATTR_TRACEFILE, traceFile, sizeof(traceFile), &traceFileLength), - Dbc_, SQL_HANDLE_DBC)); - - SQLPOINTER quietMode = reinterpret_cast(uintptr_t{1}); - EXPECT_EQ( - SQLGetConnectAttr(Dbc_, SQL_ATTR_QUIET_MODE, &quietMode, sizeof(quietMode), nullptr), - SQL_NO_DATA); - SQLUINTEGER translateOption = 0; - EXPECT_EQ( - SQLGetConnectAttr( - Dbc_, SQL_ATTR_TRANSLATE_OPTION, &translateOption, sizeof(translateOption), nullptr), - SQL_NO_DATA); - SQLCHAR translateLib[32] = {}; - EXPECT_EQ( - SQLGetConnectAttr( - Dbc_, SQL_ATTR_TRANSLATE_LIB, translateLib, sizeof(translateLib), nullptr), - SQL_NO_DATA); - - quietMode = reinterpret_cast(uintptr_t{42}); - ASSERT_TRUE(OdbcSuccess( - SQLSetConnectAttr(Dbc_, SQL_ATTR_QUIET_MODE, quietMode, 0), - Dbc_, SQL_HANDLE_DBC)); - SQLPOINTER actualQuietMode = nullptr; - SQLINTEGER quietModeLength = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetConnectAttr( - Dbc_, SQL_ATTR_QUIET_MODE, &actualQuietMode, sizeof(actualQuietMode), &quietModeLength), - Dbc_, SQL_HANDLE_DBC)); - EXPECT_EQ(actualQuietMode, quietMode); - EXPECT_EQ(quietModeLength, static_cast(sizeof(SQLPOINTER))); - - constexpr SQLUINTEGER ExpectedTranslateOption = 17; - ASSERT_TRUE(OdbcSuccess( - SQLSetConnectAttr( - Dbc_, SQL_ATTR_TRANSLATE_OPTION, - reinterpret_cast(static_cast(ExpectedTranslateOption)), 0), - Dbc_, SQL_HANDLE_DBC)); - SQLINTEGER translateOptionLength = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetConnectAttr( - Dbc_, SQL_ATTR_TRANSLATE_OPTION, &translateOption, sizeof(translateOption), - &translateOptionLength), - Dbc_, SQL_HANDLE_DBC)); - EXPECT_EQ(translateOption, ExpectedTranslateOption); - EXPECT_EQ(translateOptionLength, static_cast(sizeof(SQLUINTEGER))); - - ASSERT_TRUE(OdbcSuccess( - SQLSetConnectAttr( - Dbc_, SQL_ATTR_ACCESS_MODE, reinterpret_cast(SQL_MODE_READ_ONLY), 0), - Dbc_, - SQL_HANDLE_DBC)); - SQLULEN accessMode = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetConnectAttr(Dbc_, SQL_ATTR_ACCESS_MODE, &accessMode, sizeof(accessMode), nullptr), - Dbc_, - SQL_HANDLE_DBC)); - EXPECT_EQ(accessMode, static_cast(SQL_MODE_READ_ONLY)); -} - -TEST_F(OdbcCoreConformance, CoreStatementAttributeDefaultsAreReadable) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - - const std::array, 7> integerAttributes{{ - {SQL_ATTR_CURSOR_TYPE, SQL_CURSOR_FORWARD_ONLY, "SQL_ATTR_CURSOR_TYPE"}, - {SQL_ATTR_METADATA_ID, SQL_FALSE, "SQL_ATTR_METADATA_ID"}, - {SQL_ATTR_NOSCAN, SQL_NOSCAN_OFF, "SQL_ATTR_NOSCAN"}, - {SQL_ATTR_PARAM_BIND_TYPE, SQL_PARAM_BIND_BY_COLUMN, "SQL_ATTR_PARAM_BIND_TYPE"}, - {SQL_ATTR_PARAMSET_SIZE, 1, "SQL_ATTR_PARAMSET_SIZE"}, - {SQL_ATTR_ROW_ARRAY_SIZE, 1, "SQL_ATTR_ROW_ARRAY_SIZE"}, - {SQL_ATTR_ROW_BIND_TYPE, SQL_BIND_BY_COLUMN, "SQL_ATTR_ROW_BIND_TYPE"}, - }}; - for (const auto& [attribute, expected, name] : integerAttributes) { - SQLULEN value = 0; - const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &value, sizeof(value), nullptr); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - if (IsOdbcSuccess(rc)) { - EXPECT_EQ(value, expected) << name; - } - } - - const std::array, 7> pointerAttributes{{ - {SQL_ATTR_PARAM_BIND_OFFSET_PTR, "SQL_ATTR_PARAM_BIND_OFFSET_PTR"}, - {SQL_ATTR_PARAM_OPERATION_PTR, "SQL_ATTR_PARAM_OPERATION_PTR"}, - {SQL_ATTR_PARAM_STATUS_PTR, "SQL_ATTR_PARAM_STATUS_PTR"}, - {SQL_ATTR_PARAMS_PROCESSED_PTR, "SQL_ATTR_PARAMS_PROCESSED_PTR"}, - {SQL_ATTR_ROW_BIND_OFFSET_PTR, "SQL_ATTR_ROW_BIND_OFFSET_PTR"}, - {SQL_ATTR_ROW_STATUS_PTR, "SQL_ATTR_ROW_STATUS_PTR"}, - {SQL_ATTR_ROWS_FETCHED_PTR, "SQL_ATTR_ROWS_FETCHED_PTR"}, - }}; - for (const auto& [attribute, name] : pointerAttributes) { - SQLPOINTER value = reinterpret_cast(uintptr_t{1}); - const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &value, sizeof(value), nullptr); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - if (IsOdbcSuccess(rc)) { - EXPECT_EQ(value, nullptr) << name; - } - } - - for (const auto& [attribute, name] : std::array, 4>{{ - {SQL_ATTR_APP_PARAM_DESC, "SQL_ATTR_APP_PARAM_DESC"}, - {SQL_ATTR_APP_ROW_DESC, "SQL_ATTR_APP_ROW_DESC"}, - {SQL_ATTR_IMP_PARAM_DESC, "SQL_ATTR_IMP_PARAM_DESC"}, - {SQL_ATTR_IMP_ROW_DESC, "SQL_ATTR_IMP_ROW_DESC"}, - }}) { - SQLHDESC desc = SQL_NULL_HDESC; - const SQLRETURN rc = SQLGetStmtAttr(stmt, attribute, &desc, sizeof(desc), nullptr); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - if (IsOdbcSuccess(rc)) { - EXPECT_NE(desc, nullptr) << name; - } - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, CoreStatementArrayAttributesCanBeConfigured) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - - auto setAndGetInteger = [&](SQLINTEGER attribute, SQLULEN expected, const char* name) { - SQLRETURN rc = SQLSetStmtAttr( - stmt, attribute, reinterpret_cast(static_cast(expected)), 0); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - if (!IsOdbcSuccess(rc)) { - return; - } - SQLULEN actual = 0; - rc = SQLGetStmtAttr(stmt, attribute, &actual, sizeof(actual), nullptr); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - EXPECT_EQ(actual, expected) << name; - }; - - setAndGetInteger(SQL_ATTR_METADATA_ID, SQL_TRUE, "SQL_ATTR_METADATA_ID"); - setAndGetInteger(SQL_ATTR_NOSCAN, SQL_NOSCAN_ON, "SQL_ATTR_NOSCAN"); - setAndGetInteger(SQL_ATTR_PARAM_BIND_TYPE, 32, "SQL_ATTR_PARAM_BIND_TYPE"); - setAndGetInteger(SQL_ATTR_PARAMSET_SIZE, 2, "SQL_ATTR_PARAMSET_SIZE"); - setAndGetInteger(SQL_ATTR_ROW_ARRAY_SIZE, 2, "SQL_ATTR_ROW_ARRAY_SIZE"); - setAndGetInteger(SQL_ATTR_ROW_BIND_TYPE, 32, "SQL_ATTR_ROW_BIND_TYPE"); - - SQLULEN offset = 8; - SQLUSMALLINT operation[2] = {SQL_PARAM_PROCEED, SQL_PARAM_PROCEED}; - SQLUSMALLINT status[2] = {}; - SQLULEN processed = 0; - SQLUSMALLINT rowStatus[2] = {}; - SQLULEN rowsFetched = 0; - const std::array, 7> pointerAttributes{{ - {SQL_ATTR_PARAM_BIND_OFFSET_PTR, &offset, "SQL_ATTR_PARAM_BIND_OFFSET_PTR"}, - {SQL_ATTR_PARAM_OPERATION_PTR, operation, "SQL_ATTR_PARAM_OPERATION_PTR"}, - {SQL_ATTR_PARAM_STATUS_PTR, status, "SQL_ATTR_PARAM_STATUS_PTR"}, - {SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, "SQL_ATTR_PARAMS_PROCESSED_PTR"}, - {SQL_ATTR_ROW_BIND_OFFSET_PTR, &offset, "SQL_ATTR_ROW_BIND_OFFSET_PTR"}, - {SQL_ATTR_ROW_STATUS_PTR, rowStatus, "SQL_ATTR_ROW_STATUS_PTR"}, - {SQL_ATTR_ROWS_FETCHED_PTR, &rowsFetched, "SQL_ATTR_ROWS_FETCHED_PTR"}, - }}; - for (const auto& [attribute, expected, name] : pointerAttributes) { - SQLRETURN rc = SQLSetStmtAttr(stmt, attribute, expected, 0); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - if (!IsOdbcSuccess(rc)) { - continue; - } - SQLPOINTER actual = nullptr; - rc = SQLGetStmtAttr(stmt, attribute, &actual, sizeof(actual), nullptr); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - EXPECT_EQ(actual, expected) << name; - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, ApplicationDescriptorsCanBeAssigned) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - SQLHDESC ard = SQL_NULL_HDESC; - SQLHDESC apd = SQL_NULL_HDESC; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &ard), SQL_SUCCESS); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &apd), SQL_SUCCESS); - - SQLRETURN rc = SQLSetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, ard, 0); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << "SQL_ATTR_APP_ROW_DESC"; - SQLHDESC actual = SQL_NULL_HDESC; - if (IsOdbcSuccess(rc)) { - ASSERT_TRUE(OdbcSuccess( - SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &actual, sizeof(actual), nullptr), - stmt, - SQL_HANDLE_STMT)); - EXPECT_EQ(actual, ard); - } - - rc = SQLSetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, apd, 0); - EXPECT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << "SQL_ATTR_APP_PARAM_DESC"; - actual = SQL_NULL_HDESC; - if (IsOdbcSuccess(rc)) { - ASSERT_TRUE(OdbcSuccess( - SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &actual, sizeof(actual), nullptr), - stmt, - SQL_HANDLE_STMT)); - EXPECT_EQ(actual, apd); - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLFreeHandle(SQL_HANDLE_DESC, ard); - SQLFreeHandle(SQL_HANDLE_DESC, apd); -} - -TEST_F(OdbcCoreConformance, ApplicationDescriptorsDriveBindingsAndDetachSafely) { - SQLHSTMT first = AllocStatement(); - SQLHSTMT second = AllocStatement(); - ASSERT_NE(first, nullptr); - ASSERT_NE(second, nullptr); - SQLHDESC apd = SQL_NULL_HDESC; - SQLHDESC ard = SQL_NULL_HDESC; - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &apd), SQL_SUCCESS); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DESC, Dbc_, &ard), SQL_SUCCESS); - - SQLINTEGER fallbackParameter = 1; - ASSERT_TRUE(OdbcSuccess( - SQLPrepare(first, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), - first, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLBindParameter(first, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, - &fallbackParameter, 0, nullptr), - first, SQL_HANDLE_STMT)); - SQLINTEGER descriptorParameter = 42; - ASSERT_TRUE(OdbcSuccess( - SQLSetDescRec(apd, 1, SQL_C_LONG, 0, sizeof(descriptorParameter), 0, 0, - &descriptorParameter, nullptr, nullptr), - apd, SQL_HANDLE_DESC)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(first, SQL_ATTR_APP_PARAM_DESC, apd, 0), first, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecute(first), first, SQL_HANDLE_STMT)); - - SQLINTEGER fallbackResult = 0; - SQLLEN fallbackIndicator = 0; - ASSERT_TRUE(OdbcSuccess( - SQLBindCol(first, 1, SQL_C_LONG, &fallbackResult, 0, &fallbackIndicator), - first, SQL_HANDLE_STMT)); - SQLINTEGER descriptorResult = 0; - SQLLEN descriptorLength = -1; - SQLLEN descriptorIndicator = -1; - ASSERT_TRUE(OdbcSuccess( - SQLSetDescRec(ard, 1, SQL_C_LONG, 0, 0, 0, 0, - &descriptorResult, &descriptorLength, &descriptorIndicator), - ard, SQL_HANDLE_DESC)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(first, SQL_ATTR_APP_ROW_DESC, ard, 0), first, SQL_HANDLE_STMT)); - ASSERT_EQ(SQLFetch(first), SQL_SUCCESS); - EXPECT_EQ(descriptorResult, 42); - EXPECT_EQ(descriptorLength, static_cast(sizeof(descriptorResult))); - EXPECT_EQ(descriptorIndicator, 0); - EXPECT_EQ(fallbackResult, 0); - - ASSERT_TRUE(OdbcSuccess( - SQLSetDescField( - apd, 0, SQL_DESC_ARRAY_SIZE, reinterpret_cast(uintptr_t{2}), 0), - apd, SQL_HANDLE_DESC)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(second, SQL_ATTR_APP_PARAM_DESC, apd, 0), second, SQL_HANDLE_STMT)); - SQLULEN size = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetStmtAttr(second, SQL_ATTR_PARAMSET_SIZE, &size, sizeof(size), nullptr), - second, SQL_HANDLE_STMT)); - EXPECT_EQ(size, 2u); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr( - second, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{3}), 0), - second, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLGetDescField(apd, 0, SQL_DESC_ARRAY_SIZE, &size, 0, nullptr), - apd, SQL_HANDLE_DESC)); - EXPECT_EQ(size, 3u); - - ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_DESC, apd), SQL_SUCCESS); - ASSERT_TRUE(OdbcSuccess( - SQLGetStmtAttr(second, SQL_ATTR_PARAMSET_SIZE, &size, sizeof(size), nullptr), - second, SQL_HANDLE_STMT)); - EXPECT_EQ(size, 1u); - ASSERT_EQ(SQLFreeHandle(SQL_HANDLE_DESC, ard), SQL_SUCCESS); - - SQLFreeHandle(SQL_HANDLE_STMT, first); - SQLFreeHandle(SQL_HANDLE_STMT, second); -} - -TEST_F(OdbcCoreConformance, ParameterArraysRespectBindOffset) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "DROP TABLE IF EXISTS odbc_core_parameter_array")), - SQL_NTS); - SQLFreeStmt(stmt, SQL_CLOSE); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "CREATE TABLE odbc_core_parameter_array (id Int32, PRIMARY KEY (id))")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLFreeStmt(stmt, SQL_CLOSE); - - struct TParameterRow { - SQLINTEGER Id; - SQLLEN Indicator; - }; - std::array parameters{{ - {0, 0}, // deliberately skipped by SQL_ATTR_PARAM_BIND_OFFSET_PTR - {101, 0}, - {102, 0}, - }}; - SQLULEN bindOffset = sizeof(TParameterRow); - SQLUSMALLINT operations[2] = {SQL_PARAM_PROCEED, SQL_PARAM_PROCEED}; - SQLUSMALLINT statuses[2] = {}; - SQLULEN processed = 0; - - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr( - stmt, - SQL_ATTR_PARAM_BIND_TYPE, - reinterpret_cast(sizeof(TParameterRow)), - 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, &bindOffset, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr( - stmt, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{2}), 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_OPERATION_PTR, operations, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_STATUS_PTR, statuses, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLPrepare( - stmt, - reinterpret_cast(const_cast( - "UPSERT INTO odbc_core_parameter_array (id) VALUES (?)")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLBindParameter( - stmt, - 1, - SQL_PARAM_INPUT, - SQL_C_LONG, - SQL_INTEGER, - 0, - 0, - ¶meters[0].Id, - 0, - ¶meters[0].Indicator), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); - EXPECT_EQ(processed, 2u); - for (SQLUSMALLINT status : statuses) { - EXPECT_TRUE(status == SQL_PARAM_SUCCESS || status == SQL_PARAM_SUCCESS_WITH_INFO) - << "parameter status " << status; - } - - SQLFreeStmt(stmt, SQL_CLOSE); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "SELECT COUNT(*) FROM odbc_core_parameter_array")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLBIGINT count = 0; - ASSERT_TRUE(OdbcSuccess( - SQLGetData(stmt, 1, SQL_C_SBIGINT, &count, sizeof(count), nullptr), - stmt, - SQL_HANDLE_STMT)); - EXPECT_EQ(count, 2); - - SQLFreeStmt(stmt, SQL_CLOSE); - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "DROP TABLE odbc_core_parameter_array")), - SQL_NTS); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, RowArraysRespectBindOffset) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "DROP TABLE IF EXISTS odbc_core_row_array")), - SQL_NTS); - SQLFreeStmt(stmt, SQL_CLOSE); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "CREATE TABLE odbc_core_row_array (id Int32, PRIMARY KEY (id))")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLFreeStmt(stmt, SQL_CLOSE); - for (const char* sql : { - "UPSERT INTO odbc_core_row_array (id) VALUES (1)", - "UPSERT INTO odbc_core_row_array (id) VALUES (2)", - "UPSERT INTO odbc_core_row_array (id) VALUES (3)", - }) { - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect(stmt, reinterpret_cast(const_cast(sql)), SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLFreeStmt(stmt, SQL_CLOSE); - } - - struct TRow { - SQLINTEGER Id; - SQLLEN Indicator; - }; - std::array rows{}; - SQLULEN bindOffset = sizeof(TRow); - SQLUSMALLINT statuses[2] = {}; - SQLULEN fetched = 0; - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr( - stmt, - SQL_ATTR_ROW_BIND_TYPE, - reinterpret_cast(sizeof(TRow)), - 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_ROW_BIND_OFFSET_PTR, &bindOffset, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr( - stmt, SQL_ATTR_ROW_ARRAY_SIZE, reinterpret_cast(uintptr_t{2}), 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_ROW_STATUS_PTR, statuses, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLSetStmtAttr(stmt, SQL_ATTR_ROWS_FETCHED_PTR, &fetched, 0), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "SELECT id FROM odbc_core_row_array ORDER BY id")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLBindCol(stmt, 1, SQL_C_LONG, &rows[0].Id, 0, &rows[0].Indicator), - stmt, - SQL_HANDLE_STMT)); - - ASSERT_TRUE(OdbcSuccess(SQLFetchScroll(stmt, SQL_FETCH_NEXT, 0), stmt, SQL_HANDLE_STMT)); - ASSERT_EQ(fetched, 2u); - EXPECT_EQ(rows[1].Id, 1); - EXPECT_EQ(rows[2].Id, 2); - rows[1].Id = 0; - ASSERT_TRUE(OdbcSuccess(SQLFetchScroll(stmt, SQL_FETCH_NEXT, 0), stmt, SQL_HANDLE_STMT)); - ASSERT_EQ(fetched, 1u); - EXPECT_EQ(rows[1].Id, 3); - - SQLFreeStmt(stmt, SQL_CLOSE); - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "DROP TABLE odbc_core_row_array")), - SQL_NTS); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, CoreDescriptorHeaderFieldsAreReadable) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - ASSERT_TRUE(OdbcSuccess( - SQLPrepare(stmt, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLINTEGER parameter = 7; - ASSERT_TRUE(OdbcSuccess( - SQLBindParameter( - stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, ¶meter, 0, nullptr), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); - SQLINTEGER result = 0; - SQLLEN indicator = 0; - ASSERT_TRUE(OdbcSuccess( - SQLBindCol(stmt, 1, SQL_C_LONG, &result, 0, &indicator), - stmt, - SQL_HANDLE_STMT)); - - SQLHDESC ard = SQL_NULL_HDESC; - SQLHDESC apd = SQL_NULL_HDESC; - SQLHDESC ird = SQL_NULL_HDESC; - SQLHDESC ipd = SQL_NULL_HDESC; - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, sizeof(ard), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &apd, sizeof(apd), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_PARAM_DESC, &ipd, sizeof(ipd), nullptr), stmt, SQL_HANDLE_STMT)); - - for (SQLHDESC desc : {ard, apd, ird, ipd}) { - ExpectDescriptorField(desc, 0, SQL_DESC_ALLOC_TYPE, "SQL_DESC_ALLOC_TYPE"); - ExpectDescriptorField(desc, 0, SQL_DESC_COUNT, "SQL_DESC_COUNT"); - } - for (SQLHDESC desc : {ard, apd}) { - ExpectDescriptorField(desc, 0, SQL_DESC_ARRAY_SIZE, "SQL_DESC_ARRAY_SIZE"); - ExpectDescriptorField(desc, 0, SQL_DESC_BIND_OFFSET_PTR, "SQL_DESC_BIND_OFFSET_PTR"); - ExpectDescriptorField(desc, 0, SQL_DESC_BIND_TYPE, "SQL_DESC_BIND_TYPE"); - } - for (SQLHDESC desc : {apd, ipd, ird}) { - ExpectDescriptorField(desc, 0, SQL_DESC_ARRAY_STATUS_PTR, "SQL_DESC_ARRAY_STATUS_PTR"); - } - for (SQLHDESC desc : {ipd, ird}) { - ExpectDescriptorField(desc, 0, SQL_DESC_ROWS_PROCESSED_PTR, "SQL_DESC_ROWS_PROCESSED_PTR"); - } - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, CoreDescriptorRecordFieldsAreReadable) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - ASSERT_TRUE(OdbcSuccess( - SQLPrepare(stmt, reinterpret_cast(const_cast("SELECT ? AS value")), SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLINTEGER parameter = 7; - SQLLEN parameterIndicator = 0; - ASSERT_TRUE(OdbcSuccess( - SQLBindParameter( - stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, - ¶meter, 0, ¶meterIndicator), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecute(stmt), stmt, SQL_HANDLE_STMT)); - SQLINTEGER result = 0; - SQLLEN resultIndicator = 0; - ASSERT_TRUE(OdbcSuccess( - SQLBindCol(stmt, 1, SQL_C_LONG, &result, 0, &resultIndicator), - stmt, - SQL_HANDLE_STMT)); - - SQLHDESC ard = SQL_NULL_HDESC; - SQLHDESC apd = SQL_NULL_HDESC; - SQLHDESC ird = SQL_NULL_HDESC; - SQLHDESC ipd = SQL_NULL_HDESC; - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, sizeof(ard), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &apd, sizeof(apd), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLGetStmtAttr(stmt, SQL_ATTR_IMP_PARAM_DESC, &ipd, sizeof(ipd), nullptr), stmt, SQL_HANDLE_STMT)); - - for (const auto& [field, name] : std::array, 6>{{ - {SQL_DESC_BASE_COLUMN_NAME, "SQL_DESC_BASE_COLUMN_NAME"}, - {SQL_DESC_LITERAL_PREFIX, "SQL_DESC_LITERAL_PREFIX"}, - {SQL_DESC_LITERAL_SUFFIX, "SQL_DESC_LITERAL_SUFFIX"}, - {SQL_DESC_LOCAL_TYPE_NAME, "SQL_DESC_LOCAL_TYPE_NAME"}, - {SQL_DESC_NAME, "SQL_DESC_NAME"}, - {SQL_DESC_TYPE_NAME, "SQL_DESC_TYPE_NAME"}, - }}) { - ExpectDescriptorField(ird, 1, field, name, true); - } - for (const auto& [field, name] : std::array, 14>{{ - {SQL_DESC_CASE_SENSITIVE, "SQL_DESC_CASE_SENSITIVE"}, - {SQL_DESC_CONCISE_TYPE, "SQL_DESC_CONCISE_TYPE"}, - {SQL_DESC_DISPLAY_SIZE, "SQL_DESC_DISPLAY_SIZE"}, - {SQL_DESC_FIXED_PREC_SCALE, "SQL_DESC_FIXED_PREC_SCALE"}, - {SQL_DESC_LENGTH, "SQL_DESC_LENGTH"}, - {SQL_DESC_NULLABLE, "SQL_DESC_NULLABLE"}, - {SQL_DESC_OCTET_LENGTH, "SQL_DESC_OCTET_LENGTH"}, - {SQL_DESC_PRECISION, "SQL_DESC_PRECISION"}, - {SQL_DESC_SCALE, "SQL_DESC_SCALE"}, - {SQL_DESC_SEARCHABLE, "SQL_DESC_SEARCHABLE"}, - {SQL_DESC_TYPE, "SQL_DESC_TYPE"}, - {SQL_DESC_UNNAMED, "SQL_DESC_UNNAMED"}, - {SQL_DESC_UNSIGNED, "SQL_DESC_UNSIGNED"}, - {SQL_DESC_UPDATABLE, "SQL_DESC_UPDATABLE"}, - }}) { - ExpectDescriptorField(ird, 1, field, name); - } - - for (const auto& [field, name] : std::array, 3>{{ - {SQL_DESC_DATA_PTR, "SQL_DESC_DATA_PTR"}, - {SQL_DESC_INDICATOR_PTR, "SQL_DESC_INDICATOR_PTR"}, - {SQL_DESC_OCTET_LENGTH_PTR, "SQL_DESC_OCTET_LENGTH_PTR"}, - }}) { - ExpectDescriptorField(ard, 1, field, name); - ExpectDescriptorField(apd, 1, field, name); - } - ExpectDescriptorField(ipd, 1, SQL_DESC_PARAMETER_TYPE, "SQL_DESC_PARAMETER_TYPE"); - - SQLFreeStmt(stmt, SQL_CLOSE); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "SELECT CAST('2024-06-15T14:30:00Z' AS Datetime) AS value")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess( - SQLGetStmtAttr(stmt, SQL_ATTR_IMP_ROW_DESC, &ird, sizeof(ird), nullptr), - stmt, - SQL_HANDLE_STMT)); - ExpectDescriptorField( - ird, - 1, - SQL_DESC_DATETIME_INTERVAL_CODE, - "SQL_DESC_DATETIME_INTERVAL_CODE"); - - SQLSMALLINT conciseType = 0; - EXPECT_TRUE(OdbcSuccess( - SQLGetDescRec(ird, 1, nullptr, 0, nullptr, &conciseType, nullptr, - nullptr, nullptr, nullptr, nullptr), - ird, SQL_HANDLE_DESC)); - EXPECT_EQ(SQLGetDescField( - ird, 2, SQL_DESC_CONCISE_TYPE, &conciseType, 0, nullptr), SQL_NO_DATA); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, ReportsConservativeCapabilitiesAndTransactionSemantics) { - SQLUINTEGER value = 0; - ASSERT_TRUE(OdbcSuccess(SQLGetInfo( - Dbc_, SQL_PARAM_ARRAY_ROW_COUNTS, &value, sizeof(value), nullptr), Dbc_, SQL_HANDLE_DBC)); - EXPECT_EQ(value, static_cast(SQL_PARC_NO_BATCH)); - ASSERT_TRUE(OdbcSuccess(SQLGetInfo( - Dbc_, SQL_PARAM_ARRAY_SELECTS, &value, sizeof(value), nullptr), Dbc_, SQL_HANDLE_DBC)); - EXPECT_EQ(value, static_cast(SQL_PAS_NO_SELECT)); - - SQLUSMALLINT txnCapable = 0; - ASSERT_TRUE(OdbcSuccess(SQLGetInfo( - Dbc_, SQL_TXN_CAPABLE, &txnCapable, sizeof(txnCapable), nullptr), Dbc_, SQL_HANDLE_DBC)); - EXPECT_EQ(txnCapable, static_cast(SQL_TC_DML)); - EXPECT_EQ(SQLEndTran(SQL_HANDLE_DBC, Dbc_, SQL_COMMIT), SQL_SUCCESS); - EXPECT_EQ(SQLEndTran(SQL_HANDLE_DBC, Dbc_, 99), SQL_ERROR); - SQLCHAR state[6] = {}; - ASSERT_TRUE(IsOdbcSuccess(SQLGetDiagRec( - SQL_HANDLE_DBC, Dbc_, 1, state, nullptr, nullptr, 0, nullptr))); - EXPECT_STREQ(reinterpret_cast(state), "HY012"); -} - -TEST_F(OdbcCoreConformance, RejectsSelectParameterArrays) { - SQLHSTMT stmt = AllocStatement(); - SQLINTEGER values[2] = {1, 2}; - SQLLEN indicators[2] = {0, 0}; - ASSERT_TRUE(OdbcSuccess(SQLSetStmtAttr( - stmt, SQL_ATTR_PARAMSET_SIZE, reinterpret_cast(uintptr_t{2}), 0), - stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLPrepare( - stmt, reinterpret_cast(const_cast("SELECT ?")), SQL_NTS), - stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLBindParameter( - stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, - values, 0, indicators), stmt, SQL_HANDLE_STMT)); - EXPECT_EQ(SQLExecute(stmt), SQL_ERROR); - EXPECT_NE(GetOdbcError(stmt, SQL_HANDLE_STMT).find("HYC00"), std::string::npos); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, BindingsMayPrecedeExecDirect) { - SQLHSTMT stmt = AllocStatement(); - SQLINTEGER parameter = 77; - ASSERT_TRUE(OdbcSuccess(SQLBindParameter( - stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, - ¶meter, 0, nullptr), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecDirect( - stmt, reinterpret_cast(const_cast("SELECT ?")), SQL_NTS), - stmt, SQL_HANDLE_STMT)); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLINTEGER result = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_LONG, &result, 0, nullptr), SQL_SUCCESS); - EXPECT_EQ(result, parameter); - - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_RESET_PARAMS), SQL_SUCCESS); - SQLLEN nullIndicator = SQL_NULL_DATA; - ASSERT_TRUE(OdbcSuccess(SQLBindParameter( - stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, - nullptr, 0, &nullIndicator), stmt, SQL_HANDLE_STMT)); - ASSERT_TRUE(OdbcSuccess(SQLExecDirect( - stmt, reinterpret_cast(const_cast("SELECT ? IS NULL")), SQL_NTS), - stmt, SQL_HANDLE_STMT)); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - SQLCHAR isNull = 0; - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_BIT, &isNull, 0, nullptr), SQL_SUCCESS); - EXPECT_EQ(isNull, 1); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, GetDataConvertsNumbersAndContinuesText) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_TRUE(OdbcSuccess(SQLExecDirect( - stmt, reinterpret_cast(const_cast( - "SELECT 123 AS number, 'abcdef' AS text")), SQL_NTS), - stmt, SQL_HANDLE_STMT)); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - - char numeric[16] = {}; - SQLLEN length = 0; - EXPECT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, numeric, -1, &length), SQL_ERROR); - EXPECT_NE(GetOdbcError(stmt, SQL_HANDLE_STMT).find("HY090"), std::string::npos); - ASSERT_EQ(SQLGetData(stmt, 1, SQL_C_CHAR, numeric, sizeof(numeric), &length), SQL_SUCCESS); - EXPECT_STREQ(numeric, "123"); - - char chunk[4] = {}; - EXPECT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_SUCCESS_WITH_INFO); - EXPECT_STREQ(chunk, "abc"); - EXPECT_EQ(length, 6); - ASSERT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_SUCCESS); - EXPECT_STREQ(chunk, "def"); - EXPECT_EQ(length, 3); - EXPECT_EQ(SQLGetData(stmt, 2, SQL_C_CHAR, chunk, sizeof(chunk), &length), SQL_NO_DATA); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, DiagnosticReadsAreStableAndSuccessClearsThem) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_EQ(SQLExecDirect( - stmt, reinterpret_cast(const_cast("invalid syntax")), SQL_NTS), SQL_ERROR); - SQLCHAR first[6] = {}; - SQLCHAR second[6] = {}; - ASSERT_EQ(SQLGetDiagRec( - SQL_HANDLE_STMT, stmt, 1, first, nullptr, nullptr, 0, nullptr), SQL_SUCCESS); - ASSERT_EQ(SQLGetDiagRec( - SQL_HANDLE_STMT, stmt, 1, second, nullptr, nullptr, 0, nullptr), SQL_SUCCESS); - EXPECT_STREQ(reinterpret_cast(first), reinterpret_cast(second)); - - SQLRETURN prior = SQL_SUCCESS; - ASSERT_EQ(SQLGetDiagField( - SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_RETURNCODE, &prior, 0, nullptr), SQL_SUCCESS); - EXPECT_EQ(prior, SQL_ERROR); - EXPECT_EQ(SQLPrepare( - stmt, reinterpret_cast(const_cast("SELECT 1")), SQL_NTS), SQL_SUCCESS); - EXPECT_EQ(SQLGetDiagRec( - SQL_HANDLE_STMT, stmt, 1, first, nullptr, nullptr, 0, nullptr), SQL_NO_DATA); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} - -TEST_F(OdbcCoreConformance, CoreCatalogFunctionsReturnStandardResultShapes) { - SQLHSTMT stmt = AllocStatement(); - ASSERT_NE(stmt, nullptr); - const char* table = "/local/odbc_core_conformance_catalog"; - SQLExecDirect( - stmt, - reinterpret_cast(const_cast("DROP TABLE IF EXISTS odbc_core_conformance_catalog")), - SQL_NTS); - SQLFreeStmt(stmt, SQL_CLOSE); - ASSERT_TRUE(OdbcSuccess( - SQLExecDirect( - stmt, - reinterpret_cast(const_cast( - "CREATE TABLE odbc_core_conformance_catalog (id Int32, value Text, PRIMARY KEY (id))")), - SQL_NTS), - stmt, - SQL_HANDLE_STMT)); - SQLFreeStmt(stmt, SQL_CLOSE); - - auto expectColumns = [&](SQLRETURN rc, SQLSMALLINT expected, const char* name) { - ASSERT_TRUE(OdbcSuccess(rc, stmt, SQL_HANDLE_STMT)) << name; - SQLSMALLINT actual = 0; - ASSERT_TRUE(OdbcSuccess(SQLNumResultCols(stmt, &actual), stmt, SQL_HANDLE_STMT)) << name; - EXPECT_EQ(actual, expected) << name; - SQLFreeStmt(stmt, SQL_CLOSE); - }; - - expectColumns( - SQLTables(stmt, nullptr, 0, nullptr, 0, - reinterpret_cast(const_cast(table)), SQL_NTS, - reinterpret_cast(const_cast("TABLE")), SQL_NTS), - 5, - "SQLTables"); - expectColumns( - SQLColumns(stmt, nullptr, 0, nullptr, 0, - reinterpret_cast(const_cast(table)), SQL_NTS, - nullptr, 0), - 18, - "SQLColumns"); - expectColumns(SQLGetTypeInfo(stmt, SQL_ALL_TYPES), 19, "SQLGetTypeInfo"); - expectColumns( - SQLStatistics(stmt, nullptr, 0, nullptr, 0, - reinterpret_cast(const_cast(table)), SQL_NTS, - SQL_INDEX_ALL, SQL_ENSURE), - 13, - "SQLStatistics"); - expectColumns( - SQLSpecialColumns(stmt, SQL_BEST_ROWID, nullptr, 0, nullptr, 0, - reinterpret_cast(const_cast(table)), SQL_NTS, - SQL_SCOPE_SESSION, SQL_NULLABLE), - 8, - "SQLSpecialColumns"); - - SQLExecDirect( - stmt, - reinterpret_cast(const_cast("DROP TABLE odbc_core_conformance_catalog")), - SQL_NTS); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); -} diff --git a/odbc/tests/conformance/soci_odbc_test_context.cpp b/odbc/tests/conformance/soci_odbc_test_context.cpp new file mode 100644 index 00000000000..dee272cbf6c --- /dev/null +++ b/odbc/tests/conformance/soci_odbc_test_context.cpp @@ -0,0 +1,111 @@ +// Database setup adapter for SOCI's unchanged common ODBC test suite. + +#include "soci/odbc/soci-odbc.h" +#include "soci/soci.h" +#include "test-context.h" + +#include + +using namespace soci; +using namespace soci::tests; + +std::string connectString; +backend_factory const& backEnd = *soci::factory_odbc(); + +namespace { + +struct table_creator_one final : table_creator_base +{ + explicit table_creator_one(session& sql) + : table_creator_base(sql) + { + sql << "create table soci_test(" + "id Int32, val Int32, c Utf8, str Utf8, sh Int16, ll Int64, " + "ul Uint64, d Double, num76 Decimal(7, 6), tm Timestamp, " + "i1 Int32, i2 Int32, i3 Int32, name Utf8, primary key(id))"; + } +}; + +struct table_creator_two final : table_creator_base +{ + explicit table_creator_two(session& sql) + : table_creator_base(sql) + { + sql << "create table soci_test(" + "num_float Double, num_int Int32, name Utf8, " + "sometime Timestamp, chr Utf8, primary key(name))"; + } +}; + +struct table_creator_three final : table_creator_base +{ + explicit table_creator_three(session& sql) + : table_creator_base(sql) + { + sql << "create table soci_test(" + "name Utf8 not null, phone Utf8, primary key(name))"; + } +}; + +struct clob_table_creator final : table_creator_base +{ + explicit clob_table_creator(session& sql) + : table_creator_base(sql) + { + sql << "create table soci_test(" + "id Int32, s Text, primary key(id))"; + } +}; + +class test_context final : public test_context_common +{ +public: + std::string get_backend_name() const override + { + return "odbc"; + } + + std::string to_date_time(std::string const& value) const override + { + return "{ts '" + value + "'}"; + } + + table_creator_base* table_creator_1(session& sql) const override + { + return new table_creator_one(sql); + } + + table_creator_base* table_creator_2(session& sql) const override + { + return new table_creator_two(sql); + } + + table_creator_base* table_creator_3(session& sql) const override + { + return new table_creator_three(sql); + } + + table_creator_base* table_creator_4(session&) const override + { + return nullptr; + } + + table_creator_base* table_creator_clob(session& sql) const override + { + return new clob_table_creator(sql); + } + + bool has_transactions_support(session&) const override + { + return false; + } + + std::string sql_length(std::string const& value) const override + { + return "length(" + value + ")"; + } +}; + +test_context context; + +} // namespace From 92789a76812310c2c8db2f8ec72f815b2d201f3e Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Wed, 29 Jul 2026 15:25:11 +0300 Subject: [PATCH 44/55] performance is not an acceptance criteria --- odbc/README.md | 15 -- odbc/proposal.md | 108 ++++----- odbc/tests/CMakeLists.txt | 1 - odbc/tests/conformance/CMakeLists.txt | 226 ------------------ .../conformance/soci_odbc_test_context.cpp | 111 --------- 5 files changed, 52 insertions(+), 409 deletions(-) delete mode 100644 odbc/tests/conformance/CMakeLists.txt delete mode 100644 odbc/tests/conformance/soci_odbc_test_context.cpp diff --git a/odbc/README.md b/odbc/README.md index 089b460c93e..13ad517fd6d 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -102,21 +102,6 @@ SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT `?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` from `SQLBindParameter` types. YDB-native `$pN` syntax also works. -## Conformance and client-framework tests - -The test build fetches pinned unixODBC-Test and SOCI revisions. The SOCI gate -runs 51 unchanged upstream framework cases through its Core-only ODBC backend; -the repository contains only the schema/capability adapter needed by the -upstream harness. With a local server running, build the test preset and run -either suite: - -```bash -cmake --preset release-test-clang -cmake --build build -j$(nproc) -ctest --test-dir build -L core-conformance --output-on-failure -ctest --test-dir build -L core-framework --output-on-failure -``` - ## License Apache License 2.0 diff --git a/odbc/proposal.md b/odbc/proposal.md index b330ded20a7..a1a5a8c8478 100644 --- a/odbc/proposal.md +++ b/odbc/proposal.md @@ -1,5 +1,5 @@ # Odbc driver -Odbc is a database connection layer, which gives users the opportunity to execute sql and interact with a database using a standardised C ABI. This document regulates how should the odbc driver for YDB be implemented, which functionality it is supposed to cover, performance issues and the acceptance criteria. +Odbc is a database connection layer, which gives users the opportunity to execute sql and interact with a database using a standardised C ABI. This document regulates how should the odbc driver for YDB be implemented, which functionality it is supposed to cover and the acceptance criteria. ## Goal @@ -11,10 +11,10 @@ Languages with a maintained native YDB SDK are excluded because their native SDK - Every selected language has exactly one primary ODBC framework or binding, a pinned upstream revision, a reproducible YDB patch series, an Allure test result set and a repository-owned example application. - Every upstream database test that exists is either executed, patched with a documented YDB-specific reason, or listed explicitly as not applicable; tests must never disappear silently. -- Framework patches may adapt database assumptions to YDB, but they must not change the framework implementation or weaken assertions for ODBC behavior that the driver advertises. -- The existing driver unit, integration and conformance suites continue to pass. +- Framework implementation sources remain identical to the pinned upstream revision; YDB-specific changes are confined to test setup, fixtures and database adapters while preserving the original ODBC assertions. +- The existing driver unit and integration suites continue to pass. +- Driver-owned forward-only and static cursors provide standard ODBC fetch, scrolling, rowset binding and chunked-data behavior over YDB query results. - Multiple independent ODBC connections can target different YDB databases and hosts without sharing sessions, transactions, credentials or catalog state. -- PHP PDO_ODBC performance does not regress relative to the pinned native PHP SDK. The target is for PDO_ODBC to outperform the deprecated SDK on equivalent operations. ## Language and framework matrix @@ -42,13 +42,11 @@ The matrix is intentionally open-ended. A new language should be added whenever | Expansion | Smalltalk | [Pharo-ODBC](https://github.com/pharo-rdbms/Pharo-ODBC) | Upstream SUnit tests | Headless Pharo example using connection, statement and result objects | | Expansion | Fortran | [`odbc.f`](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests when present; otherwise the shared contract | Fortran application using connection, result-set and column-set objects | -Core jobs run on every pull request once their baseline is stable. Expansion jobs run nightly while being onboarded and are promoted to the pull-request matrix after they produce deterministic results. Adding a new language requires one matrix row, not a second framework for a language already represented. +Core and Expansion jobs run only after changes are merged into `odbc-driver-feature` and when a tag is pushed. They do not run for pull requests or on a nightly schedule. Adding a new language requires one matrix row, not a second framework for a language already represented. ## YDB compatibility policy -YDB requires a primary key for every table. The ODBC driver must not synthesize keys, inject hidden columns, rewrite application DML to maintain hidden values or hide physical columns from metadata. Applications using YDB are responsible for providing a sound key, and framework test fixtures should be patched to do the same. - -YDB and YQL also differ from other relational databases in namespace structure, supported types, DDL, common-table-expression syntax, stored procedures, identity columns, result-set capabilities and transaction modes. These differences should be handled either by a semantics-preserving driver feature or by an explicit framework test patch when the test encodes a database-specific assumption rather than an ODBC requirement. +The driver presents standard ODBC behavior and translates it to YDB semantics. This compatibility layer covers required table keys, namespace structure, supported types, DDL, common-table-expression syntax, identity behavior, result cursors and transaction modes. Applications and ODBC frameworks use their normal public APIs without YDB-specific source changes. ## Implementation details @@ -60,22 +58,38 @@ The exported ODBC C ABI should remain in `src/odbc_driver.cpp`, while connection 1. Apply the existing ODBC escape translation unless `SQL_NOSCAN` is enabled. 2. Tokenize the statement while preserving string literals, quoted identifiers, comments and parameter markers. -3. Apply semantics-preserving YDB compatibility rewrites for common table expressions, identifiers and namespace resolution; do not change table keys or application data. +3. Apply semantics-preserving YDB compatibility rewrites for table keys, common table expressions, identifiers and namespace resolution while preserving application-visible data. 4. Apply the existing `?` to `$pN` rewrite and generate typed `DECLARE` statements from the bound ODBC parameters. 5. Add the compatibility pragmas required by the statement and apply the current catalog with `TConnection::WrapQueryForCurrentCatalog()`. 6. Execute the final YQL through the Query Service and translate YDB status and result metadata back to ODBC diagnostics and types. `SQLNativeSql` should run the same translation pipeline without executing the statement. This makes the API useful for diagnosing the exact YQL that the driver will submit and prevents it from disagreeing with `SQLPrepare` and `SQLExecDirect`. -### Primary-key behavior +### Primary-key emulation + +For `CREATE TABLE`, the compatibility parser should preserve an explicit primary key. When the statement has no primary key, it should promote a declared non-null unique constraint or unique index to the YDB primary key. If no suitable unique key exists, it should add a collision-free UUID column such as `_ydb_odbc_row_id` as the physical YDB primary key. + +The generated UUID column is an internal storage detail. The driver should populate it for inserts, preserve it for updates, use it to identify rows for deletes and omit it from `SELECT *`, `SQLColumns`, `SQLPrimaryKeys`, `SQLStatistics` and result metadata. Explicit column lists, parameter counts, ordinal positions and affected-row counts remain those of the application-visible schema. One shared table-mapping record should describe the logical columns, physical columns, selected key strategy and generated-column name so DDL rewriting, DML rewriting and metadata always agree. + +The mapping should be recovered from YDB schema metadata and a driver-owned metadata table, allowing a new process or pooled connection to use tables created by an earlier connection. Tests should cover explicit keys, promoted composite unique keys, generated UUID keys, inserts with and without column lists, updates, deletes, `SELECT *`, aliases, metadata, reconnects and concurrent writers. + +### Cursor emulation + +YDB returns query results rather than server-side ODBC cursors. Each executed statement should therefore create a driver-owned cursor over the returned typed rows and column metadata. The cursor state machine consists of `before first`, `on row or rowset`, `after last` and `closed`; statement re-execution replaces the previous cursor, and statement close, cancellation and connection close release its resources. + +The Core path should implement `SQLFetch` and `SQLFetchScroll(SQL_FETCH_NEXT)` as forward movement through that cursor. A static scrollable cursor should materialize a result snapshot and implement `SQL_FETCH_FIRST`, `LAST`, `PRIOR`, `ABSOLUTE` and `RELATIVE` by changing a logical row position. `SQL_ATTR_ROW_ARRAY_SIZE`, row-wise and column-wise binding, `SQL_ATTR_ROWS_FETCHED_PTR` and row-status arrays should operate on consecutive rows beginning at that position. + +Rows should remain in YDB's typed representation until `SQLBindCol` or `SQLGetData` requests an ODBC C type. The cursor keeps a separate `SQLGetData` byte offset for every column of the current row, resets those offsets whenever the position changes and preserves the row until all chunked reads are complete. Cursor movement should produce the standard ODBC outcomes and diagnostics, including `SQL_NO_DATA`, `24000`, `HY010`, `HY106`, `01004` and conversion SQLSTATEs. -The driver should submit `CREATE TABLE` statements without inventing a physical schema. If a table has no primary key, it should return the YDB failure through the normal ODBC diagnostic chain with an appropriate SQLSTATE and the native YDB issue text. Driver tests should verify correct diagnostics, while every framework fixture patch should add a deterministic key and update its inserts and expected metadata consistently. +Forward-only cursors should consume rows incrementally. Static cursors should use a bounded in-memory row store with a statement-local spill file and an index of row offsets after the memory threshold is reached. `SQL_ATTR_MAX_ROWS` limits population of either store. Explicit commit or rollback closes open cursors consistently with the advertised `SQL_CB_CLOSE` behavior. + +The initial advertised cursor types should be `SQL_CURSOR_FORWARD_ONLY` and read-only `SQL_CURSOR_STATIC`. `SQLSetCursorName` and `SQLGetCursorName` maintain the statement-local ODBC name. Capability reporting should be derived from the implemented fetch orientations, cursor attributes and concurrency mode. Integration tests should exercise empty and single-row results, large spilled results, every supported orientation and offset, row arrays, bound columns, chunked `SQLGetData`, truncation, nulls, re-execution, cancellation, transaction completion and multiple simultaneous statement cursors. ### WITH-clause translation The compatibility parser should translate each non-recursive CTE to a collision-free YQL named expression. It must first collect every `$identifier` in the complete query, allocate a deterministic unused name such as `$_odbc_cte_s0_n0_`, and maintain a scope-aware mapping from the ANSI relation name to that generated expression. Multiple CTEs must be emitted in dependency order, table references must be rewritten only in the correct scope, and existing declared parameters or global YQL named expressions must remain unchanged. -The initial implementation must cover chained CTEs, multiple references to one CTE, nested subqueries, CTE column aliases, quoted identifiers and statements containing ODBC parameters. `WITH RECURSIVE`, data-modifying CTEs and unsupported materialization modifiers should remain explicit driver gaps until they have a semantics-preserving implementation. +The initial implementation must cover chained CTEs, multiple references to one CTE, nested subqueries, CTE column aliases, quoted identifiers and statements containing ODBC parameters. Recursive and data-modifying CTEs form the next compatibility milestone. Tests must include keywords inside strings and comments, nested and shadowed CTE names, an existing `$cte` variable, multiple CTEs and failure diagnostics for unsupported recursive syntax. @@ -85,25 +99,25 @@ The implementation should build on the current catalog support rather than intro Qualified-name resolution and metadata filters must use one shared normalizer. It must handle quoted path components, absolute and current-catalog-relative table names, repeated separators and attempts to traverse above the configured database root. `SQLTables`, `SQLColumns`, `SQLPrimaryKeys`, `SQLStatistics` and query execution must resolve the same logical name to the same physical YDB path. -If an upstream suite proves that a non-empty schema is required, schema support should be implemented as a tested directory alias layer on top of this mapping; returning inconsistent schema values only to satisfy metadata assertions is not acceptable. +Schema support should use a tested directory alias layer on top of this mapping whenever a framework requires a non-empty schema. ### Multiple databases and hosts -Each `SQLHDBC` must own an endpoint, database path, credentials, TLS settings, clients, sessions, transaction and current catalog. A single `SQLHENV` may contain many independently configured connection handles targeting different databases on the same host or databases on different hosts. Statements always execute through their parent connection, and disconnecting or failing one connection must not affect another. +Each `SQLHDBC` must own an endpoint, database path, credentials, TLS settings, clients, sessions, transaction and current catalog. A single `SQLHENV` may contain many independently configured connection handles targeting different databases on the same host or databases on different hosts. Statements always execute through their parent connection, and each connection has an independent lifecycle and failure boundary. -One connection string identifies one YDB discovery endpoint and one database. A multi-node YDB database should use its discovery or load-balancer endpoint rather than exposing a host list through ODBC. Cross-connection transactions are not atomic: `SQLEndTran(SQL_HANDLE_ENV, ...)` may iterate over connections, but it must not be described as a distributed transaction. +One connection string identifies one YDB discovery or load-balancer endpoint and one database. `SQLEndTran(SQL_HANDLE_ENV, ...)` applies commit or rollback independently to every connected `SQLHDBC` and reports the per-connection diagnostic chain. -Integration tests must cover two databases on one endpoint, two endpoints, concurrent queries, independent commit/rollback, isolated credentials and catalog state, failure of one endpoint, and driver-manager pooling without returning a connection for the wrong endpoint/database pair. +Integration tests must cover two databases on one endpoint, two endpoints, concurrent queries, independent commit/rollback, isolated credentials and catalog state, failure of one endpoint, and driver-manager pooling keyed by the complete endpoint/database/credential identity. ### YDB, YQL and driver boundaries -The required physical primary key and hierarchical object namespace are YDB constraints. YQL named expressions, parameter declarations, identifier quoting and unsupported ANSI constructs are language-level constraints. Patching framework fixtures to use a valid YDB schema, implementing semantics-preserving SQL translation, mapping catalogs, preserving ODBC transaction semantics and returning correct SQLSTATE diagnostics are project responsibilities. +The required physical primary key and hierarchical object namespace are YDB constraints. YQL named expressions, parameter declarations, identifier quoting and ANSI translation are language-level constraints. The driver compatibility layer owns key emulation, SQL translation, catalog mapping, cursor emulation, transaction behavior and SQLSTATE diagnostics. -Every failing framework test should be assigned to one of these boundaries in Allure. A portable ODBC behavior should be implemented in the driver when it can be provided without falsifying YDB semantics. A database-specific fixture or assertion should be patched or marked not applicable with a precise reason. A case may be classified as server-blocked only when no sound driver implementation or fixture adaptation exists, and it must remain visible with the exact server limitation documented. +Every failing framework test should be assigned to one of these boundaries in Allure. Portable ODBC behavior belongs in the driver compatibility layer. Database-specific setup belongs in the test fixture or database adapter. Server limitations remain visible with the exact affected behavior and YDB issue documented. ### Capability reporting -`SQLGetInfo` and `SQLGetFunctions` must describe implemented behavior, not intended behavior. Each compatibility feature should therefore land with both execution tests and capability-reporting tests. Unsupported procedures, multiple result sets, scrollable cursors, asynchronous execution and batch operations must continue to report unsupported until their complete API behavior is implemented. +`SQLGetInfo` and `SQLGetFunctions` should be generated from a tested capability registry shared with the implementation. Each compatibility feature lands with execution tests, capability-reporting tests and the corresponding registry entry. ## Framework test implementation @@ -122,37 +136,35 @@ odbc/tests/frameworks/ convert-results example/ odbc/tests/reporting/ -odbc/tests/performance/php/ ``` `registry.yaml` is the source of truth for the CI matrix and records the language, framework, tier, runtime image, upstream URL, revision, archive checksum, patch directory, test command, native result format and example command. `upstream.lock` repeats the immutable source identity inside each integration directory so a language can be reproduced independently. ### Patch policy -Upstream framework and binding implementation code must remain unchanged. Test files, test fixtures and test-only configuration may be patched when necessary to make the suite sound for YDB. Patches should be stored as ordered files under `/patches/` and applied to a clean pinned checkout during the CI job; the repository must not maintain an opaque fork. +Framework and binding implementation files are verified against the pinned upstream revision. YDB-specific patches are limited by path to test files, fixtures and test-only configuration, stored as ordered files under `/patches/` and applied to a clean pinned checkout during the CI job. Allowed patches include: -- Add explicit primary keys to fixture DDL and update fixture inserts and expected key metadata consistently. - Replace another database's vendor-specific setup SQL with equivalent YDB/YQL setup. - Use YDB-supported types where the original type is vendor-specific and the test is not testing that exact ODBC type. - Map flat schemas, temporary database names or database creation steps to isolated YDB directories. - Adapt expected database-specific error text while preserving the expected SQLSTATE class and operation outcome. - Mark a test not applicable when it requires a database feature YDB does not provide and the driver accurately reports that capability as unsupported. -Patches must not modify the framework or binding implementation, remove tests without a manifest entry, weaken assertions for advertised ODBC behavior, replace framework calls with direct YDB calls, turn a crash/hang/data corruption failure into an expected failure, or hide a driver regression behind a YDB limitation. +CI enforces the allowed patch paths, the pinned implementation-source checksum, the original assertion count and a manifest entry for every changed or inapplicable test. Crash, hang and data-corruption outcomes remain failures. -Every patch file must have a matching manifest record containing a stable patch ID, affected upstream test IDs, category (`YDB_PRIMARY_KEY`, `YDB_NAMESPACE`, `YQL_SYNTAX`, `YDB_TYPE`, `UNSUPPORTED_CAPABILITY` or `VENDOR_SPECIFIC`), rationale and link to the relevant YDB/YQL limitation. CI must verify the upstream checksum, run `git apply --check`, apply the ordered series and publish both the patch manifest and resulting tree hash. +Every patch file must have a matching manifest record containing a stable patch ID, affected upstream test IDs, category (`YDB_NAMESPACE`, `YQL_SYNTAX`, `YDB_TYPE`, `UNSUPPORTED_CAPABILITY` or `VENDOR_SPECIFIC`), rationale and link to the relevant YDB/YQL limitation. CI must verify the upstream checksum, run `git apply --check`, apply the ordered series and publish both the patch manifest and resulting tree hash. ### Shared test contract -Frameworks with an upstream database suite run that suite after applying the reviewed patches. Frameworks without a useful upstream integration suite run a repository-owned shared contract through the public API of the selected binding. The shared contract is not a replacement for upstream tests when upstream tests exist. +Every framework runs its upstream database suite when one exists. Bindings with incomplete upstream integration coverage additionally run a repository-owned shared contract through the selected binding's public API. -The shared contract covers connection and disconnection, invalid connection diagnostics, multiple connections, direct execution, preparation and rebinding, scalar and tabular results, `NULL`, integer, floating-point, decimal, UTF-8, binary and date/time values, metadata, affected-row counts, commit, rollback, autocommit, concurrent independent connections, cleanup after errors and resource finalization. Cases for optional ODBC features run only when capability discovery reports them as supported. +The shared contract covers connection and disconnection, invalid connection diagnostics, multiple connections, direct execution, preparation and rebinding, scalar and tabular results, forward and static cursor movement, rowset binding, chunked reads, `NULL`, integer, floating-point, decimal, UTF-8, binary and date/time values, metadata, affected-row counts, commit, rollback, autocommit, concurrent independent connections, cleanup after errors and resource finalization. Cases for optional ODBC features run when capability discovery reports them as supported. ### Example applications -Every language directory must contain a small executable example and a README with exact dependency installation and run commands. The example accepts `YDB_ODBC_DSN` or `YDB_ODBC_CONNECTION_STRING`, creates an isolated table with an explicit primary key, performs a parameterized insert, reads and prints typed rows, demonstrates commit and rollback, and removes its table. It must use only the selected language framework's public API and must run in CI after the tests. +Every language directory must contain a small executable example and a README with exact dependency installation and run commands. The example accepts `YDB_ODBC_DSN` or `YDB_ODBC_CONNECTION_STRING`, creates an isolated table with a standard non-null unique `id`, performs a parameterized insert, reads and prints typed rows through the framework's cursor, demonstrates commit and rollback, and removes its table. It must use only the selected language framework's public API and must run in CI after the tests. Examples should share the same logical `people(id, name, score, created_at)` schema while remaining idiomatic for their language. They are product artifacts, not test patches, and should be suitable for copying into user documentation. @@ -166,40 +178,24 @@ Failing tests should attach the SQLSTATE chain, native YDB issue text, translate ### CI workflow -A framework workflow should run for pull requests targeting `odbc-driver-feature`, pushes to that branch, nightly schedules and manual dispatches. It should build the driver once and generate its matrix from `registry.yaml`. Core languages run on every pull request; all Core and Expansion languages run nightly and on manual full-matrix requests. +A framework workflow should run only after changes are merged into `odbc-driver-feature` and when a tag is pushed. It must not run for pull requests, direct non-merge pushes, nightly or other scheduled events, or manual dispatches. Both triggers run the complete Core and Expansion matrix generated from `registry.yaml`, with the driver built once for the workflow. Each job should start the same pinned YDB version, wait for readiness, create an isolated database prefix, register the driver in a job-local `odbcinst.ini`, fetch and verify upstream source, apply the reviewed patch series, run upstream tests, run the shared contract when required, run the example, and upload native plus Allure results even on failure. A final `if: always()` job validates manifests, merges results, builds the HTML report and publishes the raw results, rendered report, patch manifests and example logs. -The baseline may contain known failures while support is being implemented, but each pull request must satisfy an incremental gate: no passing test or example regresses, no test disappears, the targeted behavior becomes passing, and upstream or patch changes are explicit. Once a language is green, its gate switches to zero failed, broken, missing or unexpected skipped cases. - -The development loop is: inspect aggregated Allure failures, decide whether each failure is a driver defect or an unsound database assumption, add a focused driver regression test or a documented test patch, rerun the affected language and existing ODBC suites, then merge into `odbc-driver-feature`. The branch must be rebased and the applicable matrix rerun when its base advances. - -## Performance test implementation - -The PHP comparison must use one repository-owned workload implementation with two thin backends: native PHP SDK `ExecuteQuery` and PDO_ODBC. Both backends must use the same PHP runtime, YDB server, schema, seed data, query text, parameter values, connection lifetime, concurrency schedule, retry policy and result validation. - -The benchmark should separate operation types rather than compare unlike APIs: - -- Result-returning statements: native `ExecuteQuery` versus PDO `prepare`/`execute` plus the same row fetch and decoding work. -- Non-result statements: native `ExecuteQuery` versus PDO `exec` or prepared execution with identical transaction semantics. -- Connection setup: measured separately from steady-state execution so pooling and driver initialization costs remain visible. - -The workload should include point reads, parameterized range reads, inserts, updates and an explicit transaction. It should run a warm-up phase followed by at least five paired 600-second samples in alternating backend order on the same isolated runner. Rate limiting, concurrency and data-set size must be explicit inputs and recorded in the result. - -The workflow should publish requests per second, p50/p95/p99/p99.9 latency, error rate, retry count, CPU time per operation and peak resident memory. Each operation must validate returned row counts or affected-row counts so a faster error or empty result cannot be reported as an improvement. +The baseline may contain known failures while support is being implemented, but each post-merge or tag run must satisfy an incremental gate: no passing test or example regresses, no test disappears, the targeted behavior becomes passing, and upstream or patch changes are explicit. Once a language is green, its gate switches to zero failed, broken, missing or unexpected skipped cases. -The regression gate should compare paired samples with confidence intervals. PHP PDO_ODBC passes when throughput is not lower and p95/p99 latency is not higher than the pinned native PHP SDK by more than the agreed tolerance; the performance target is for ODBC to outperform the deprecated SDK, but correctness and a statistically stable no-regression gate come first. Any intentional change to the SDK version, YDB version, runner class or workload invalidates the stored baseline and requires a new reviewed baseline. +The development loop is: inspect aggregated Allure failures, decide whether each failure is a driver defect or an unsound database assumption, add a focused driver regression test or a documented test patch, and rerun the affected language and existing ODBC suites locally before merging into `odbc-driver-feature`. The full matrix runs after that merge. Tag creation runs the same matrix against the tagged revision. ## Delivery order 1. Add `registry.yaml`, the framework directory template, source verification, patch verification, native-result conversion and Allure aggregation. -2. Onboard the Core languages with pinned upstream suites, reviewed YDB patches, test manifests and runnable examples. -3. Add the shared contract for bindings with incomplete upstream integration coverage. -4. Extract the shared SQL compatibility pipeline, make `SQLNativeSql` use it and implement collision-safe non-recursive WITH translation. -5. Finish catalog normalization, per-connection authentication/TLS configuration and multiple-host/multiple-database isolation tests. -6. Onboard Expansion languages one at a time and promote each stable job into the pull-request matrix. -7. Resolve remaining Allure failures as driver fixes or reviewed database-specific test patches, with a focused regression test or patch rationale for every change. -8. Add the paired PHP performance workflow and establish the reviewed native-SDK baseline. -9. Enable zero-regression gates for every stable language and the PHP performance gate on `odbc-driver-feature`. - -The final acceptance evidence is a commit-specific Allure report for every registered language, the original and patched upstream source identities, patch manifests, example logs, existing ODBC unit/integration/conformance results, multiple-endpoint results and the paired PHP performance report. A result is not acceptable if implementation code in a selected framework was modified, a test was omitted without a manifest record, a patch masks advertised ODBC behavior, capability reporting overstates the driver, an example does not run, or the benchmark compares different semantics. +2. Extract the shared SQL compatibility pipeline and implement primary-key emulation, persistent logical-to-physical table mappings, `SQLNativeSql` translation and collision-safe non-recursive WITH translation. +3. Implement driver-owned forward-only and static cursors, bounded buffering and spill, all declared fetch orientations, rowset binding and chunked `SQLGetData`. +4. Onboard the Core languages with pinned upstream suites, reviewed YDB patches, test manifests and runnable examples. +5. Add the shared contract for bindings with incomplete upstream integration coverage. +6. Finish catalog normalization, per-connection authentication/TLS configuration and multiple-host/multiple-database isolation tests. +7. Onboard Expansion languages one at a time and add each stable job to the post-merge and tag matrix. +8. Resolve remaining Allure failures as driver fixes or reviewed database-specific test patches, with a focused regression test or patch rationale for every change. +9. Enable zero-regression gates for every stable language in the post-merge and tag workflow. + +The final acceptance evidence is a commit-specific Allure report for every registered language, the original and patched upstream source identities, patch manifests, example logs, existing ODBC unit and integration results and multiple-endpoint results. diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 160517d941e..8abcd081832 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -18,5 +18,4 @@ Database=${YDB_ODBC_DSN_DATABASE} ") add_subdirectory(integration) -add_subdirectory(conformance) add_subdirectory(unit) diff --git a/odbc/tests/conformance/CMakeLists.txt b/odbc/tests/conformance/CMakeLists.txt deleted file mode 100644 index 7e66679baee..00000000000 --- a/odbc/tests/conformance/CMakeLists.txt +++ /dev/null @@ -1,226 +0,0 @@ -include(FetchContent) - -set(_odbc_external_test_environment - "ODBCINI=${YDB_ODBC_TEST_CONFIG_DIR}/odbc.ini" - "ODBCSYSINI=${YDB_ODBC_TEST_CONFIG_DIR}" -) - -# unixODBC-Test is the public ODBC API conformance corpus. Its build files -# require qmake and the interactive ODBCTest host, so compile the headless -# TestFarm programs directly without changing their sources. -FetchContent_Declare( - unixodbc_test - SVN_REPOSITORY https://svn.code.sf.net/p/unixodbc-test/code - SVN_REVISION -r19 - UPDATE_DISCONNECTED ON -) -FetchContent_MakeAvailable(unixodbc_test) - -add_library(unixodbc-test-v3-common STATIC - "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/tst.c" - "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/o3util.c" - "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/o3utdesc.c" - "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common/uniutil.c" -) -target_include_directories(unixodbc-test-v3-common - PUBLIC - "${unixodbc_test_SOURCE_DIR}/TestFarm/common" - "${unixodbc_test_SOURCE_DIR}/TestFarm/v3/common" -) -target_compile_definitions(unixodbc-test-v3-common - PUBLIC - # Removed from modern 64-bit unixODBC headers, but used by this ODBC 3.x - # corpus as the row-offset-sized integer type. - SQLROWOFFSET=SQLLEN -) -target_link_libraries(unixodbc-test-v3-common PUBLIC ODBC::ODBC) -set_target_properties(unixodbc-test-v3-common PROPERTIES - C_STANDARD 11 - C_STANDARD_REQUIRED ON -) -if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(unixodbc-test-v3-common PRIVATE -w) -endif() - -function(add_unixodbc_testfarm_test name source) - set(target_name "unixodbc-test-${name}") - add_executable(${target_name} "${unixodbc_test_SOURCE_DIR}/${source}") - target_link_libraries(${target_name} - PRIVATE - unixodbc-test-v3-common - ODBC::ODBC - ) - set_target_properties(${target_name} PROPERTIES - C_STANDARD 11 - C_STANDARD_REQUIRED ON - ) - if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(${target_name} PRIVATE -w) - endif() - add_dependencies(${target_name} ydb-odbc) - - add_test(NAME "unixodbc-test.${name}" - COMMAND $ ${ARGN} - ) - set_tests_properties("unixodbc-test.${name}" PROPERTIES - ENVIRONMENT "${_odbc_external_test_environment}" - LABELS "integration;odbc;core-conformance;unixodbc-test" - TIMEOUT 60 - ) -endfunction() - -# The upstream TestFarm's formal Core directory contains the two allocation -# tests. The remaining programs are its Core-relevant, SQL-dialect-independent -# conformance cases from the historical Other directory. -add_unixodbc_testfarm_test(alloc-handle-env - "TestFarm/v3/conformance/Core/AllocHandle-ENV/test.c") -add_unixodbc_testfarm_test(alloc-handle-dbc - "TestFarm/v3/conformance/Core/AllocHandle-DBC/test.c") -add_unixodbc_testfarm_test(connect - "TestFarm/v3/conformance/Other/connect/connect.c" YDB) -add_unixodbc_testfarm_test(get-functions - "TestFarm/v3/conformance/Other/getfunc/getfunc.c" YDB) -add_unixodbc_testfarm_test(get-info - "TestFarm/v3/conformance/Other/getinfo/getinfo.c" YDB) -add_unixodbc_testfarm_test(get-type-info - "TestFarm/v3/conformance/Other/gettinf/gettinf.c" YDB) -add_unixodbc_testfarm_test(native-sql - "TestFarm/v3/conformance/Other/nativsql/nativsql.c" YDB) -add_unixodbc_testfarm_test(connection-attributes - "TestFarm/v3/conformance/Other/getconop/getconop.c" YDB) -add_unixodbc_testfarm_test(attribute-roundtrip - "TestFarm/v3/conformance/Other/getset/getset.c" YDB) - -# SOCI's ODBC backend uses only Core-classified ODBC entry points. Compile its -# upstream common test sources unchanged; the local translation unit supplies -# only the database schema and capability adapter required by SOCI's harness. -set(SOCI_SHARED OFF CACHE BOOL "" FORCE) -set(SOCI_TESTS OFF CACHE BOOL "" FORCE) -set(SOCI_LTO OFF CACHE BOOL "" FORCE) -set(WITH_BOOST OFF CACHE BOOL "" FORCE) -set(SOCI_DB2 OFF CACHE STRING "" FORCE) -set(SOCI_EMPTY OFF CACHE STRING "" FORCE) -set(SOCI_FIREBIRD OFF CACHE STRING "" FORCE) -set(SOCI_MYSQL OFF CACHE STRING "" FORCE) -set(SOCI_ODBC ON CACHE STRING "" FORCE) -set(SOCI_ORACLE OFF CACHE STRING "" FORCE) -set(SOCI_POSTGRESQL OFF CACHE STRING "" FORCE) -set(SOCI_SQLITE3 OFF CACHE STRING "" FORCE) -FetchContent_Declare( - soci - GIT_REPOSITORY https://github.com/SOCI/soci.git - GIT_TAG 4c1578d09ebbe324fbe1f00661146b483de88be2 - UPDATE_DISCONNECTED ON -) -FetchContent_MakeAvailable(soci) - -add_executable(soci-odbc-core-tests - "${soci_SOURCE_DIR}/tests/common/test-boost.cpp" - "${soci_SOURCE_DIR}/tests/common/test-common.cpp" - "${soci_SOURCE_DIR}/tests/common/test-connparams.cpp" - "${soci_SOURCE_DIR}/tests/common/test-custom.cpp" - "${soci_SOURCE_DIR}/tests/common/test-dynamic.cpp" - "${soci_SOURCE_DIR}/tests/common/test-lob.cpp" - "${soci_SOURCE_DIR}/tests/common/test-main.cpp" - "${soci_SOURCE_DIR}/tests/common/test-manual.cpp" - "${soci_SOURCE_DIR}/tests/common/test-rowset.cpp" - "${soci_SOURCE_DIR}/tests/common/test-unicode.cpp" - soci_odbc_test_context.cpp -) -target_include_directories(soci-odbc-core-tests - PRIVATE - "${soci_SOURCE_DIR}/include/private" - "${soci_SOURCE_DIR}/tests" -) -target_compile_definitions(soci-odbc-core-tests - PRIVATE - CATCH_CONFIG_CPP11_NO_SHUFFLE -) -target_compile_features(soci-odbc-core-tests PRIVATE cxx_std_14) -target_link_libraries(soci-odbc-core-tests - PRIVATE - SOCI::Core - SOCI::ODBC -) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(soci-odbc-core-tests PRIVATE -Wno-deprecated-declarations) -endif() -add_dependencies(soci-odbc-core-tests ydb-odbc) - -set(_soci_odbc_connection_string - "Driver=$;Server=${YDB_ODBC_DSN_SERVER};Database=${YDB_ODBC_DSN_DATABASE}" -) -set(_soci_odbc_core_test_cases - "timegm implementation" - "Exception on not connected" - "Basic functionality" - "Use and into" - "Repeated and bulk fetch" - "Indicators" - "Indicators vector" - "Use type conversion" - "Multiple use and into" - "Use vector" - "Named parameters" - "Named parameters with similar names" - "Use with indicators" - "Numeric round trip" - "Bulk fetch with single use" - "Basic logging support" - "soci_error is nothrow" - "NULL with std optional" - "Connection and reconnection" - "Connection pool" - "Query transformation" - "Query transformation with connection pool" - "Backend memory leak" - "Bind memory leak" - "std::tm timestamp problem with DST" - "Truncation error" - "Select without table" - "String length" - "Logger" - "Connection string parsing" - "connection_parameters::extract_option" - "connection_parameters::build_string_from_options" - "Use vector of custom type objects" - "Into vector of custom type objects" - "Prepared insert with ORM" - "Partial match with ORM" - "Dynamic row binding" - "Dynamic row binding 2" - "Dynamic row binding 3" - "Dynamic binding with type conversions" - "Dynamic row binding 4" - "Dynamic binding with rowset" - "CLOB" - "CLOB vector" - "Rowset creation and copying" - "Rowset iteration" - "Reading rows from rowset" - "Reading ints from rowset" - "Reading strings from rowset" - "Rowset expected exception" - "NULL expected exception" -) - -# Deliberately not registered: upstream cases which the SOCI harness would -# capability-skip (identity, affected rows, XML, and BLOB), plus cases requiring -# SQL_AUTOCOMMIT_OFF/rollback. Core-level ODBC drivers need not support true -# transactions, so those are not valid tests of a Core-only framework path. - -foreach(test_case IN LISTS _soci_odbc_core_test_cases) - string(MAKE_C_IDENTIFIER "${test_case}" test_id) - string(TOLOWER "${test_id}" test_id) - add_test(NAME "soci-odbc.${test_id}" - COMMAND $ - "${_soci_odbc_connection_string}" - "${test_case}" - ) - set_tests_properties("soci-odbc.${test_id}" PROPERTIES - ENVIRONMENT "${_odbc_external_test_environment}" - LABELS "integration;odbc;core-framework;framework-compatibility;soci" - RESOURCE_LOCK soci_odbc_test_table - TIMEOUT 120 - ) -endforeach() diff --git a/odbc/tests/conformance/soci_odbc_test_context.cpp b/odbc/tests/conformance/soci_odbc_test_context.cpp deleted file mode 100644 index dee272cbf6c..00000000000 --- a/odbc/tests/conformance/soci_odbc_test_context.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Database setup adapter for SOCI's unchanged common ODBC test suite. - -#include "soci/odbc/soci-odbc.h" -#include "soci/soci.h" -#include "test-context.h" - -#include - -using namespace soci; -using namespace soci::tests; - -std::string connectString; -backend_factory const& backEnd = *soci::factory_odbc(); - -namespace { - -struct table_creator_one final : table_creator_base -{ - explicit table_creator_one(session& sql) - : table_creator_base(sql) - { - sql << "create table soci_test(" - "id Int32, val Int32, c Utf8, str Utf8, sh Int16, ll Int64, " - "ul Uint64, d Double, num76 Decimal(7, 6), tm Timestamp, " - "i1 Int32, i2 Int32, i3 Int32, name Utf8, primary key(id))"; - } -}; - -struct table_creator_two final : table_creator_base -{ - explicit table_creator_two(session& sql) - : table_creator_base(sql) - { - sql << "create table soci_test(" - "num_float Double, num_int Int32, name Utf8, " - "sometime Timestamp, chr Utf8, primary key(name))"; - } -}; - -struct table_creator_three final : table_creator_base -{ - explicit table_creator_three(session& sql) - : table_creator_base(sql) - { - sql << "create table soci_test(" - "name Utf8 not null, phone Utf8, primary key(name))"; - } -}; - -struct clob_table_creator final : table_creator_base -{ - explicit clob_table_creator(session& sql) - : table_creator_base(sql) - { - sql << "create table soci_test(" - "id Int32, s Text, primary key(id))"; - } -}; - -class test_context final : public test_context_common -{ -public: - std::string get_backend_name() const override - { - return "odbc"; - } - - std::string to_date_time(std::string const& value) const override - { - return "{ts '" + value + "'}"; - } - - table_creator_base* table_creator_1(session& sql) const override - { - return new table_creator_one(sql); - } - - table_creator_base* table_creator_2(session& sql) const override - { - return new table_creator_two(sql); - } - - table_creator_base* table_creator_3(session& sql) const override - { - return new table_creator_three(sql); - } - - table_creator_base* table_creator_4(session&) const override - { - return nullptr; - } - - table_creator_base* table_creator_clob(session& sql) const override - { - return new clob_table_creator(sql); - } - - bool has_transactions_support(session&) const override - { - return false; - } - - std::string sql_length(std::string const& value) const override - { - return "length(" + value + ")"; - } -}; - -test_context context; - -} // namespace From bd070572403b3138cd2f1da58538de4f43d33e77 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Thu, 30 Jul 2026 14:18:50 +0300 Subject: [PATCH 45/55] document actual odbc limitations --- odbc/proposal.md | 310 ++++++++++++++++++----------------------- odbc/src/statement.cpp | 1 + 2 files changed, 136 insertions(+), 175 deletions(-) diff --git a/odbc/proposal.md b/odbc/proposal.md index a1a5a8c8478..b4a8117ed5c 100644 --- a/odbc/proposal.md +++ b/odbc/proposal.md @@ -1,201 +1,161 @@ -# Odbc driver -Odbc is a database connection layer, which gives users the opportunity to execute sql and interact with a database using a standardised C ABI. This document regulates how should the odbc driver for YDB be implemented, which functionality it is supposed to cover and the acceptance criteria. +# ODBC driver bindings ## Goal -The driver should provide useful YDB access to as many programming languages as possible through their established ODBC libraries. Select one representative framework or binding per language, run its upstream database tests when they exist, maintain an explicit YDB compatibility patch series for those tests, and provide a runnable example application in every selected language. +Validate the YDB ODBC driver through established ODBC bindings. Applications use the binding's public API and select YDB through a DSN or connection string. Binding implementation code is not patched. -Languages with a maintained native YDB SDK are excluded because their native SDK is the preferred integration. The current exclusions are C++, Go, Java, Python, C#/.NET, JavaScript/TypeScript and Rust. PHP is the explicit exception because its native SDK is planned for deprecation. The exclusion list must be checked against the current [YDB SDK installation page](https://ydb.tech/docs/en/reference/ydb-sdk/install) whenever the framework matrix is updated. - -## Acceptance criteria - -- Every selected language has exactly one primary ODBC framework or binding, a pinned upstream revision, a reproducible YDB patch series, an Allure test result set and a repository-owned example application. -- Every upstream database test that exists is either executed, patched with a documented YDB-specific reason, or listed explicitly as not applicable; tests must never disappear silently. -- Framework implementation sources remain identical to the pinned upstream revision; YDB-specific changes are confined to test setup, fixtures and database adapters while preserving the original ODBC assertions. -- The existing driver unit and integration suites continue to pass. -- Driver-owned forward-only and static cursors provide standard ODBC fetch, scrolling, rowset binding and chunked-data behavior over YDB query results. -- Multiple independent ODBC connections can target different YDB databases and hosts without sharing sessions, transactions, credentials or catalog state. - -## Language and framework matrix - -The matrix is intentionally open-ended. A new language should be added whenever an installable Linux ODBC binding can connect through unixODBC, execute parameterized statements and fetch results. A small or old upstream suite is not a reason to reject a language; it means the shared contract and example application carry more of its coverage. - -| Tier | Language | Selected framework or binding | Upstream tests to run | Required example | -|---|---|---|---|---| -| Core | Erlang | [OTP `odbc`](https://github.com/erlang/otp/tree/master/lib/odbc) | All YDB-applicable Common Test cases in `lib/odbc/test` | OTP application using `odbc:param_query` and transactions | -| Core | PHP | [PDO_ODBC](https://github.com/php/php-src/tree/master/ext/pdo_odbc) | PDO_ODBC PHPT tests and generic PDO tests selected by the existing harness | CLI application using `PDO`, prepared statements and transactions | -| Core | Haskell | [HDBC-odbc](https://github.com/hdbc/HDBC-odbc) | HDBC/HUnit database tests | Cabal application using prepared statements and `withTransaction` | -| Core | Ruby | [ruby-odbc](https://github.com/larskanis/ruby-odbc) | All upstream database-independent test scripts | Ruby application using prepared statements, iteration and rollback | -| Core | Lua | [LuaSQL ODBC](https://github.com/lunarmodules/luasql) | Common LuaSQL tests and ODBC-specific parameter tests | Lua application using environment, connection and cursor objects | -| Core | Perl | [DBD::ODBC](https://github.com/perl5-dbi/DBD-ODBC) | Generic TAP tests under `t/` | DBI application using binding, fetch hashes and transactions | -| Core | R | [`odbc` with DBI](https://github.com/r-dbi/odbc) | Package `testthat` tests and DBItest compliance groups | R script returning a typed data frame through DBI | -| Core | Julia | [ODBC.jl](https://github.com/JuliaDatabases/ODBC.jl) | ODBC.jl, DBInterface and Tables-compatible test sets | Julia application using `DBInterface.execute` and Tables rows | -| Core | Tcl | [`tdbc::odbc`](https://core.tcl-lang.org/tdbc) | `tcltest` suites for the ODBC backend | Tcl application using prepared statements and result-set iteration | -| Expansion | Raku | [DBDish::ODBC](https://github.com/salortiz/DBDish-ODBC) | Upstream `t/` tests and DBIish common tests supported by the adapter | Raku application using DBIish connection and statement handles | -| Expansion | Crystal | [crystal-odbc](https://github.com/naqvis/crystal-odbc) | Complete `crystal spec` suite | Crystal application using the `crystal-db` API | -| Expansion | Dart | [`dart_odbc`](https://pub.dev/packages/dart_odbc) | Complete `dart test` suite | Dart CLI application using prepared execution and typed rows | -| Expansion | D | [`odbc`](https://github.com/singingbush/odbc) | Upstream unit tests and integration-test executable | D application using the package connection and result APIs | -| Expansion | OCaml | [`ocaml-odbc`](https://opam.ocaml.org/packages/odbc/) | Upstream database tests when present; otherwise the shared contract | Dune application using prepared execution and row conversion | -| Expansion | Common Lisp | [CLSQL ODBC](https://github.com/sharplispers/clsql) | ODBC-applicable ASDF test systems when present; otherwise the shared contract | SBCL application using CLSQL query and transaction APIs | -| Expansion | COBOL | [GixSQL ODBC](https://github.com/mridoni/gixsql) | ODBC-capable GixSQL regression cases and examples | GnuCOBOL application using embedded SQL, a cursor and commit/rollback | -| Expansion | Pascal | [Free Pascal SQLDB ODBC](https://gitlab.com/freepascal.org/fpc/source/-/tree/main/packages/fcl-db) | FPCUnit SQLDB connector tests | Free Pascal application using `TODBCConnection`, `TSQLQuery` and `TSQLTransaction` | -| Expansion | Smalltalk | [Pharo-ODBC](https://github.com/pharo-rdbms/Pharo-ODBC) | Upstream SUnit tests | Headless Pharo example using connection, statement and result objects | -| Expansion | Fortran | [`odbc.f`](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests when present; otherwise the shared contract | Fortran application using connection, result-set and column-set objects | - -Core and Expansion jobs run only after changes are merged into `odbc-driver-feature` and when a tag is pushed. They do not run for pull requests or on a nightly schedule. Adding a new language requires one matrix row, not a second framework for a language already represented. - -## YDB compatibility policy - -The driver presents standard ODBC behavior and translates it to YDB semantics. This compatibility layer covers required table keys, namespace structure, supported types, DDL, common-table-expression syntax, identity behavior, result cursors and transaction modes. Applications and ODBC frameworks use their normal public APIs without YDB-specific source changes. - -## Implementation details - -### Driver architecture - -The exported ODBC C ABI should remain in `src/odbc_driver.cpp`, while connection state, statement execution, descriptors, diagnostics and result-set handling remain in the existing handle classes. Compatibility behavior should be implemented in a new internal `src/compatibility/` module instead of being distributed across exported API functions. - -`TStatement::ExecuteQuery()` should use the following pipeline: - -1. Apply the existing ODBC escape translation unless `SQL_NOSCAN` is enabled. -2. Tokenize the statement while preserving string literals, quoted identifiers, comments and parameter markers. -3. Apply semantics-preserving YDB compatibility rewrites for table keys, common table expressions, identifiers and namespace resolution while preserving application-visible data. -4. Apply the existing `?` to `$pN` rewrite and generate typed `DECLARE` statements from the bound ODBC parameters. -5. Add the compatibility pragmas required by the statement and apply the current catalog with `TConnection::WrapQueryForCurrentCatalog()`. -6. Execute the final YQL through the Query Service and translate YDB status and result metadata back to ODBC diagnostics and types. - -`SQLNativeSql` should run the same translation pipeline without executing the statement. This makes the API useful for diagnosing the exact YQL that the driver will submit and prevents it from disagreeing with `SQLPrepare` and `SQLExecDirect`. - -### Primary-key emulation - -For `CREATE TABLE`, the compatibility parser should preserve an explicit primary key. When the statement has no primary key, it should promote a declared non-null unique constraint or unique index to the YDB primary key. If no suitable unique key exists, it should add a collision-free UUID column such as `_ydb_odbc_row_id` as the physical YDB primary key. - -The generated UUID column is an internal storage detail. The driver should populate it for inserts, preserve it for updates, use it to identify rows for deletes and omit it from `SELECT *`, `SQLColumns`, `SQLPrimaryKeys`, `SQLStatistics` and result metadata. Explicit column lists, parameter counts, ordinal positions and affected-row counts remain those of the application-visible schema. One shared table-mapping record should describe the logical columns, physical columns, selected key strategy and generated-column name so DDL rewriting, DML rewriting and metadata always agree. - -The mapping should be recovered from YDB schema metadata and a driver-owned metadata table, allowing a new process or pooled connection to use tables created by an earlier connection. Tests should cover explicit keys, promoted composite unique keys, generated UUID keys, inserts with and without column lists, updates, deletes, `SELECT *`, aliases, metadata, reconnects and concurrent writers. - -### Cursor emulation - -YDB returns query results rather than server-side ODBC cursors. Each executed statement should therefore create a driver-owned cursor over the returned typed rows and column metadata. The cursor state machine consists of `before first`, `on row or rowset`, `after last` and `closed`; statement re-execution replaces the previous cursor, and statement close, cancellation and connection close release its resources. - -The Core path should implement `SQLFetch` and `SQLFetchScroll(SQL_FETCH_NEXT)` as forward movement through that cursor. A static scrollable cursor should materialize a result snapshot and implement `SQL_FETCH_FIRST`, `LAST`, `PRIOR`, `ABSOLUTE` and `RELATIVE` by changing a logical row position. `SQL_ATTR_ROW_ARRAY_SIZE`, row-wise and column-wise binding, `SQL_ATTR_ROWS_FETCHED_PTR` and row-status arrays should operate on consecutive rows beginning at that position. - -Rows should remain in YDB's typed representation until `SQLBindCol` or `SQLGetData` requests an ODBC C type. The cursor keeps a separate `SQLGetData` byte offset for every column of the current row, resets those offsets whenever the position changes and preserves the row until all chunked reads are complete. Cursor movement should produce the standard ODBC outcomes and diagnostics, including `SQL_NO_DATA`, `24000`, `HY010`, `HY106`, `01004` and conversion SQLSTATEs. - -Forward-only cursors should consume rows incrementally. Static cursors should use a bounded in-memory row store with a statement-local spill file and an index of row offsets after the memory threshold is reached. `SQL_ATTR_MAX_ROWS` limits population of either store. Explicit commit or rollback closes open cursors consistently with the advertised `SQL_CB_CLOSE` behavior. - -The initial advertised cursor types should be `SQL_CURSOR_FORWARD_ONLY` and read-only `SQL_CURSOR_STATIC`. `SQLSetCursorName` and `SQLGetCursorName` maintain the statement-local ODBC name. Capability reporting should be derived from the implemented fetch orientations, cursor attributes and concurrency mode. Integration tests should exercise empty and single-row results, large spilled results, every supported orientation and offset, row arrays, bound columns, chunked `SQLGetData`, truncation, nulls, re-execution, cancellation, transaction completion and multiple simultaneous statement cursors. - -### WITH-clause translation - -The compatibility parser should translate each non-recursive CTE to a collision-free YQL named expression. It must first collect every `$identifier` in the complete query, allocate a deterministic unused name such as `$_odbc_cte_s0_n0_`, and maintain a scope-aware mapping from the ANSI relation name to that generated expression. Multiple CTEs must be emitted in dependency order, table references must be rewritten only in the correct scope, and existing declared parameters or global YQL named expressions must remain unchanged. - -The initial implementation must cover chained CTEs, multiple references to one CTE, nested subqueries, CTE column aliases, quoted identifiers and statements containing ODBC parameters. Recursive and data-modifying CTEs form the next compatibility milestone. - -Tests must include keywords inside strings and comments, nested and shadowed CTE names, an existing `$cte` variable, multiple CTEs and failure diagnostics for unsupported recursive syntax. - -### Catalog and directory mapping - -The implementation should build on the current catalog support rather than introduce an independent schema model. An ODBC catalog is a normalized absolute YDB database or directory path, the schema component is empty, `/` is the catalog separator, and `SQL_ATTR_CURRENT_CATALOG` changes the path used by `PRAGMA TablePathPrefix`. - -Qualified-name resolution and metadata filters must use one shared normalizer. It must handle quoted path components, absolute and current-catalog-relative table names, repeated separators and attempts to traverse above the configured database root. `SQLTables`, `SQLColumns`, `SQLPrimaryKeys`, `SQLStatistics` and query execution must resolve the same logical name to the same physical YDB path. - -Schema support should use a tested directory alias layer on top of this mapping whenever a framework requires a non-empty schema. - -### Multiple databases and hosts - -Each `SQLHDBC` must own an endpoint, database path, credentials, TLS settings, clients, sessions, transaction and current catalog. A single `SQLHENV` may contain many independently configured connection handles targeting different databases on the same host or databases on different hosts. Statements always execute through their parent connection, and each connection has an independent lifecycle and failure boundary. - -One connection string identifies one YDB discovery or load-balancer endpoint and one database. `SQLEndTran(SQL_HANDLE_ENV, ...)` applies commit or rollback independently to every connected `SQLHDBC` and reports the per-connection diagnostic chain. - -Integration tests must cover two databases on one endpoint, two endpoints, concurrent queries, independent commit/rollback, isolated credentials and catalog state, failure of one endpoint, and driver-manager pooling keyed by the complete endpoint/database/credential identity. - -### YDB, YQL and driver boundaries - -The required physical primary key and hierarchical object namespace are YDB constraints. YQL named expressions, parameter declarations, identifier quoting and ANSI translation are language-level constraints. The driver compatibility layer owns key emulation, SQL translation, catalog mapping, cursor emulation, transaction behavior and SQLSTATE diagnostics. - -Every failing framework test should be assigned to one of these boundaries in Allure. Portable ODBC behavior belongs in the driver compatibility layer. Database-specific setup belongs in the test fixture or database adapter. Server limitations remain visible with the exact affected behavior and YDB issue documented. - -### Capability reporting - -`SQLGetInfo` and `SQLGetFunctions` should be generated from a tested capability registry shared with the implementation. Each compatibility feature lands with execution tests, capability-reporting tests and the corresponding registry entry. - -## Framework test implementation - -### Repository layout - -The repository should keep one self-contained integration directory per language: +Languages with a maintained native YDB SDK are out of scope. PHP remains in scope because its native SDK is planned for deprecation. + +## Binding matrix + +Each language has one binding, one pinned upstream revision, its upstream database tests, and one runnable example. + +| Tier | Language | Binding | Tests | +|---|---|---|---| +| Core | Erlang | [OTP `odbc`](https://github.com/erlang/otp/tree/master/lib/odbc) | `lib/odbc/test` Common Test cases | +| Core | PHP | [PDO_ODBC](https://github.com/php/php-src/tree/master/ext/pdo_odbc) | PDO_ODBC and generic PDO PHPT tests | +| Core | Haskell | [HDBC-odbc](https://github.com/hdbc/HDBC-odbc) | HDBC/HUnit database tests | +| Core | Ruby | [ruby-odbc](https://github.com/larskanis/ruby-odbc) | Upstream test scripts | +| Core | Lua | [LuaSQL ODBC](https://github.com/lunarmodules/luasql) | Common LuaSQL and ODBC parameter tests | +| Core | Perl | [DBD::ODBC](https://github.com/perl5-dbi/DBD-ODBC) | Upstream TAP tests | +| Core | R | [`odbc`](https://github.com/r-dbi/odbc) | `testthat` and DBItest | +| Core | Julia | [ODBC.jl](https://github.com/JuliaDatabases/ODBC.jl) | ODBC.jl, DBInterface, and Tables tests | +| Core | Tcl | [`tdbc::odbc`](https://core.tcl-lang.org/tdbc) | ODBC backend `tcltest` suite | +| Expansion | Raku | [DBDish::ODBC](https://github.com/salortiz/DBDish-ODBC) | Upstream and DBIish tests | +| Expansion | Crystal | [crystal-odbc](https://github.com/naqvis/crystal-odbc) | `crystal spec` | +| Expansion | Dart | [`dart_odbc`](https://pub.dev/packages/dart_odbc) | `dart test` | +| Expansion | D | [`odbc`](https://github.com/singingbush/odbc) | Upstream unit and integration tests | +| Expansion | OCaml | [`ocaml-odbc`](https://opam.ocaml.org/packages/odbc/) | Upstream tests | +| Expansion | Common Lisp | [CLSQL ODBC](https://github.com/sharplispers/clsql) | ODBC ASDF tests | +| Expansion | COBOL | [GixSQL ODBC](https://github.com/mridoni/gixsql) | ODBC regression tests | +| Expansion | Pascal | [Free Pascal SQLDB ODBC](https://gitlab.com/freepascal.org/fpc/source/-/tree/main/packages/fcl-db) | SQLDB connector tests | +| Expansion | Smalltalk | [Pharo-ODBC](https://github.com/pharo-rdbms/Pharo-ODBC) | SUnit tests | +| Expansion | Fortran | [`odbc.f`](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests | + +## Required driver behavior + +### Connections + +An ODBC connection is one endpoint/database pair. YDB requires both values for routing ([connection parameters](https://ydb.tech/docs/en/concepts/connect)). + +Each `SQLHDBC` owns: + +- one SDK `TDriver` configured with its endpoint and database; +- its query, table, and scheme clients; +- its query session and active transaction; +- its current catalog, credentials, and diagnostics. + +The current implementation follows this model: `SQLAllocHandle(SQL_HANDLE_DBC)` creates a `TConnection`, and `TConnection::TYdbState` owns the SDK driver and clients. `SQLHENV` only tracks connection handles. Connections in the same environment therefore keep endpoint, database, session, transaction, and catalog state separate. + +`SQL_ATTR_CURRENT_CATALOG` uses a path below the connected database as `TablePathPrefix`. Setting it to another database path recreates only that connection's SDK state. + +Required tests: + +- two database paths on one endpoint; +- two endpoints; +- simultaneous queries; +- independent transactions and catalogs; +- failure and disconnect of one connection while the other remains usable; +- driver-manager pooling keyed by endpoint, database, credentials, and TLS settings. + +### Cursors + +YDB returns result sets, not server-side ODBC cursors. The driver owns cursor state for each statement. + +The cursor states are `before first`, `on row or rowset`, `after last`, and `closed`. `SQLFetch` and `SQLFetchScroll(SQL_FETCH_NEXT)` advance a forward cursor. A static cursor stores a result snapshot and implements `FIRST`, `LAST`, `PRIOR`, `ABSOLUTE`, and `RELATIVE`. + +The cursor also owns: + +- typed YDB rows and column metadata; +- row-wise and column-wise bindings; +- row-array status and processed-row counters; +- a separate chunk offset for each `SQLGetData` column; +- bounded memory with a statement-local spill file for static cursors. + +Re-execution replaces the cursor. Close, cancel, commit, rollback, and disconnect release it according to the advertised cursor behavior. + +### Binding contract + +The driver must support the operations used by the Core bindings: + +- DSN and connection-string connection; +- prepare, bind, execute, and data-at-execution parameters; +- scalar and rowset fetch; +- `NULL`, integer, floating-point, decimal, text, binary, date, time, and timestamp conversion; +- column, table, key, index, type, and result metadata; +- autocommit and explicit transactions; +- diagnostics through SQLSTATE and native YDB issues; +- independent statements and connections; +- deterministic cleanup after errors. + +## Current limitations + +| Area | Limitation | +|---|---| +| SQL dialect | Statements are YQL. The driver rewrites ODBC escapes and `?` parameters; it is not a general ANSI SQL translator. | +| Authentication | Connection strings currently configure only endpoint, database, and DSN. User/password, token, service-account, metadata credentials, TLS certificates, and custom IAM settings are not wired into `TDriverConfig`. | +| Retry classification | The driver cannot infer whether arbitrary SQL is idempotent. Autocommit statement retries use `TRetryOperationSettings::Idempotent(false)`. This enables only retries safe for a non-idempotent operation and may return an error with an unknown execution outcome. See [YDB retry settings](https://ydb.tech/docs/en/recipes/ydb-sdk/retry) and [error handling](https://ydb.tech/docs/en/reference/ydb-sdk/error_handling). | +| Explicit transactions | An ODBC transaction is not retried by the driver. Conflicts, node failures, maintenance, and network failures can abort it, including at commit. The application must open a new transaction and replay the entire unit of work in a retry loop. Retrying only the failed statement is incorrect ([query execution](https://ydb.tech/docs/en/concepts/query_execution/), [transactions](https://ydb.tech/docs/en/concepts/transactions)). | +| Transaction isolation | Read-write connections support serializable and snapshot read-write modes. ODBC read-committed and read-uncommitted requests are rejected. | +| Transaction scope | `SQLEndTran(SQL_HANDLE_ENV, ...)` completes each connection independently. It is not an atomic transaction across databases or endpoints. | +| Connection concurrency | An explicit transaction uses one SDK session. YDB sessions execute one query at a time, so concurrent statements on the same transaction connection require application serialization ([YDB errors FAQ](https://ydb.tech/docs/en/faq/errors)). | +| Cursor support | The current implementation is forward-only. `SQLFetchScroll` accepts only `SQL_FETCH_NEXT`; static scrolling and spill are planned. | +| Result sets | Only the first result set is exposed. `SQLMoreResults` returns `SQL_NO_DATA`. | +| Result buffering | Query execution uses the non-streaming SDK result and keeps it for cursor fetches. Large results can consume memory proportional to the result size. | +| Row counts | `SQLRowCount` returns `-1`; affected-row counts are not extracted from YDB query statistics. | +| Prepare | `SQLPrepare` stores the query and counts client-side parameter markers. It does not create a persistent server-side prepared statement. | +| Batches | Parameter arrays are accepted only for data-modification statements and execute sequentially. Earlier parameter sets may already be committed when a later set fails. | +| Cancellation | Execution is synchronous. `SQLCancel` clears local cursor and parameter state but does not interrupt an in-flight SDK request. | +| Metadata namespace | YDB paths are exposed as catalogs. Schemas are empty. | +| DDL | Autocommit DDL uses `NoTx`. DDL executed while autocommit is off is sent through the active transaction and may be rejected by YDB. | +| Optional ODBC features | Multiple result sets, stored procedures, output parameters, positioned updates, bookmarks, asynchronous execution, and ODBC batch operations are not implemented. | +| Thread safety | Handle state is mutable and has no internal locking. Applications must serialize access to the same ODBC handle. | + +## Test repository ```text odbc/tests/frameworks/ registry.yaml / upstream.lock - patches/ - test-manifest.yaml run-tests convert-results example/ odbc/tests/reporting/ ``` -`registry.yaml` is the source of truth for the CI matrix and records the language, framework, tier, runtime image, upstream URL, revision, archive checksum, patch directory, test command, native result format and example command. `upstream.lock` repeats the immutable source identity inside each integration directory so a language can be reproduced independently. - -### Patch policy - -Framework and binding implementation files are verified against the pinned upstream revision. YDB-specific patches are limited by path to test files, fixtures and test-only configuration, stored as ordered files under `/patches/` and applied to a clean pinned checkout during the CI job. - -Allowed patches include: - -- Replace another database's vendor-specific setup SQL with equivalent YDB/YQL setup. -- Use YDB-supported types where the original type is vendor-specific and the test is not testing that exact ODBC type. -- Map flat schemas, temporary database names or database creation steps to isolated YDB directories. -- Adapt expected database-specific error text while preserving the expected SQLSTATE class and operation outcome. -- Mark a test not applicable when it requires a database feature YDB does not provide and the driver accurately reports that capability as unsupported. - -CI enforces the allowed patch paths, the pinned implementation-source checksum, the original assertion count and a manifest entry for every changed or inapplicable test. Crash, hang and data-corruption outcomes remain failures. - -Every patch file must have a matching manifest record containing a stable patch ID, affected upstream test IDs, category (`YDB_NAMESPACE`, `YQL_SYNTAX`, `YDB_TYPE`, `UNSUPPORTED_CAPABILITY` or `VENDOR_SPECIFIC`), rationale and link to the relevant YDB/YQL limitation. CI must verify the upstream checksum, run `git apply --check`, apply the ordered series and publish both the patch manifest and resulting tree hash. - -### Shared test contract - -Every framework runs its upstream database suite when one exists. Bindings with incomplete upstream integration coverage additionally run a repository-owned shared contract through the selected binding's public API. - -The shared contract covers connection and disconnection, invalid connection diagnostics, multiple connections, direct execution, preparation and rebinding, scalar and tabular results, forward and static cursor movement, rowset binding, chunked reads, `NULL`, integer, floating-point, decimal, UTF-8, binary and date/time values, metadata, affected-row counts, commit, rollback, autocommit, concurrent independent connections, cleanup after errors and resource finalization. Cases for optional ODBC features run when capability discovery reports them as supported. - -### Example applications - -Every language directory must contain a small executable example and a README with exact dependency installation and run commands. The example accepts `YDB_ODBC_DSN` or `YDB_ODBC_CONNECTION_STRING`, creates an isolated table with a standard non-null unique `id`, performs a parameterized insert, reads and prints typed rows through the framework's cursor, demonstrates commit and rollback, and removes its table. It must use only the selected language framework's public API and must run in CI after the tests. - -Examples should share the same logical `people(id, name, score, created_at)` schema while remaining idiomatic for their language. They are product artifacts, not test patches, and should be suitable for copying into user documentation. - -### Native result and Allure contract - -Every launcher must preserve the framework's native output and produce a normalized result containing the stable upstream test identifier, status, duration, stdout, stderr and setup/teardown phase. Native formats such as Common Test logs, PHPT output, TAP, JUnit/XML, FPCUnit XML or framework-specific text should be converted externally rather than by editing the upstream runner. +`registry.yaml` records the language, tier, runtime image, upstream URL and revision, archive checksum, test command, result format, and example command. CI verifies the checksum before running the unchanged upstream binding. -Allure results should use the hierarchy `ODBC / / / ` and include the framework version, runtime version, driver commit, YDB version, endpoint/database mode, upstream checksum, patched-tree hash and applied patch IDs. The history identifier must be derived from the language, pinned upstream revision and original upstream test identifier. +Each example uses only the binding's public API. It accepts `YDB_ODBC_DSN` or `YDB_ODBC_CONNECTION_STRING`, creates isolated test data, executes bound statements, iterates a cursor, demonstrates commit and rollback, and cleans up. -Failing tests should attach the SQLSTATE chain, native YDB issue text, translated YQL from `SQLNativeSql`, framework output and relevant server logs with secrets removed. Missing tests, an empty suite, patch-application failure, infrastructure failure or an unexpected skip should produce synthetic broken results and fail the job so a reduced test count cannot look like progress. +Test output is converted to Allure without changing the upstream runner. Reports include the binding version, runtime version, driver commit, YDB version, endpoint/database mode, and upstream checksum. Missing tests, an empty suite, infrastructure failure, and unexpected skips fail the job. -### CI workflow +## CI -A framework workflow should run only after changes are merged into `odbc-driver-feature` and when a tag is pushed. It must not run for pull requests, direct non-merge pushes, nightly or other scheduled events, or manual dispatches. Both triggers run the complete Core and Expansion matrix generated from `registry.yaml`, with the driver built once for the workflow. +The complete matrix runs only: -Each job should start the same pinned YDB version, wait for readiness, create an isolated database prefix, register the driver in a job-local `odbcinst.ini`, fetch and verify upstream source, apply the reviewed patch series, run upstream tests, run the shared contract when required, run the example, and upload native plus Allure results even on failure. A final `if: always()` job validates manifests, merges results, builds the HTML report and publishes the raw results, rendered report, patch manifests and example logs. +- after a merge into `odbc-driver-feature`; +- for a pushed tag. -The baseline may contain known failures while support is being implemented, but each post-merge or tag run must satisfy an incremental gate: no passing test or example regresses, no test disappears, the targeted behavior becomes passing, and upstream or patch changes are explicit. Once a language is green, its gate switches to zero failed, broken, missing or unexpected skipped cases. +It does not run for pull requests, direct non-merge pushes, schedules, or manual dispatches. -The development loop is: inspect aggregated Allure failures, decide whether each failure is a driver defect or an unsound database assumption, add a focused driver regression test or a documented test patch, and rerun the affected language and existing ODBC suites locally before merging into `odbc-driver-feature`. The full matrix runs after that merge. Tag creation runs the same matrix against the tagged revision. +Each job starts a pinned YDB version, registers the driver in a job-local `odbcinst.ini`, verifies the upstream source, runs the upstream tests and example, and uploads native and Allure results. ## Delivery order -1. Add `registry.yaml`, the framework directory template, source verification, patch verification, native-result conversion and Allure aggregation. -2. Extract the shared SQL compatibility pipeline and implement primary-key emulation, persistent logical-to-physical table mappings, `SQLNativeSql` translation and collision-safe non-recursive WITH translation. -3. Implement driver-owned forward-only and static cursors, bounded buffering and spill, all declared fetch orientations, rowset binding and chunked `SQLGetData`. -4. Onboard the Core languages with pinned upstream suites, reviewed YDB patches, test manifests and runnable examples. -5. Add the shared contract for bindings with incomplete upstream integration coverage. -6. Finish catalog normalization, per-connection authentication/TLS configuration and multiple-host/multiple-database isolation tests. -7. Onboard Expansion languages one at a time and add each stable job to the post-merge and tag matrix. -8. Resolve remaining Allure failures as driver fixes or reviewed database-specific test patches, with a focused regression test or patch rationale for every change. -9. Enable zero-regression gates for every stable language in the post-merge and tag workflow. - -The final acceptance evidence is a commit-specific Allure report for every registered language, the original and patched upstream source identities, patch manifests, example logs, existing ODBC unit and integration results and multiple-endpoint results. +1. Add the registry, framework runner template, source verification, result conversion, and report aggregation. +2. Add static cursor emulation and cursor integration tests. +3. Add two-endpoint and two-database isolation tests. +4. Onboard Core bindings. +5. Onboard Expansion bindings. +6. Enable a zero-regression gate for stable bindings. + +## Acceptance + +- Every selected binding runs from a pinned, verified upstream source. +- Binding implementation code is unchanged. +- Every upstream database test is executed or reported as unsupported with its original test identifier. +- Every binding has a runnable example. +- Unit and integration tests pass. +- Endpoint/database isolation tests pass. +- Reports contain no missing or unexpected skipped tests. diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 8fbb821ffbf..e56425524ee 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -281,6 +281,7 @@ SQLUSMALLINT TStatement::FindNextNeedDataParam() const { NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() { NYdb::NRetry::TRetryOperationSettings settings; + settings.Idempotent(false); SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { const TDuration deadline = TDuration::Seconds(queryTimeoutSec); From 8b5831b965864aa7e528cd8efa1b8dadfa970af0 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Fri, 31 Jul 2026 13:15:14 +0300 Subject: [PATCH 46/55] create a doc with goals and restrictions --- odbc/proposal.md | 83 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/odbc/proposal.md b/odbc/proposal.md index b4a8117ed5c..8d98c165523 100644 --- a/odbc/proposal.md +++ b/odbc/proposal.md @@ -18,19 +18,19 @@ Each language has one binding, one pinned upstream revision, its upstream databa | Core | Ruby | [ruby-odbc](https://github.com/larskanis/ruby-odbc) | Upstream test scripts | | Core | Lua | [LuaSQL ODBC](https://github.com/lunarmodules/luasql) | Common LuaSQL and ODBC parameter tests | | Core | Perl | [DBD::ODBC](https://github.com/perl5-dbi/DBD-ODBC) | Upstream TAP tests | -| Core | R | [`odbc`](https://github.com/r-dbi/odbc) | `testthat` and DBItest | +| Core | R | [odbc](https://github.com/r-dbi/odbc) | `testthat` and DBItest | | Core | Julia | [ODBC.jl](https://github.com/JuliaDatabases/ODBC.jl) | ODBC.jl, DBInterface, and Tables tests | -| Core | Tcl | [`tdbc::odbc`](https://core.tcl-lang.org/tdbc) | ODBC backend `tcltest` suite | +| Core | Tcl | [tdbc::odbc](https://core.tcl-lang.org/tdbcodbc/timeline) | ODBC backend `tcltest` suite | | Expansion | Raku | [DBDish::ODBC](https://github.com/salortiz/DBDish-ODBC) | Upstream and DBIish tests | | Expansion | Crystal | [crystal-odbc](https://github.com/naqvis/crystal-odbc) | `crystal spec` | -| Expansion | Dart | [`dart_odbc`](https://pub.dev/packages/dart_odbc) | `dart test` | -| Expansion | D | [`odbc`](https://github.com/singingbush/odbc) | Upstream unit and integration tests | -| Expansion | OCaml | [`ocaml-odbc`](https://opam.ocaml.org/packages/odbc/) | Upstream tests | +| Expansion | Dart | [dart_odbc](https://pub.dev/packages/dart_odbc) | `dart test` | +| Expansion | D | [odbc](https://github.com/singingbush/odbc) | Upstream unit and integration tests | +| Expansion | OCaml | [ocaml-odbc](https://opam.ocaml.org/packages/odbc/) | Upstream tests | | Expansion | Common Lisp | [CLSQL ODBC](https://github.com/sharplispers/clsql) | ODBC ASDF tests | | Expansion | COBOL | [GixSQL ODBC](https://github.com/mridoni/gixsql) | ODBC regression tests | | Expansion | Pascal | [Free Pascal SQLDB ODBC](https://gitlab.com/freepascal.org/fpc/source/-/tree/main/packages/fcl-db) | SQLDB connector tests | | Expansion | Smalltalk | [Pharo-ODBC](https://github.com/pharo-rdbms/Pharo-ODBC) | SUnit tests | -| Expansion | Fortran | [`odbc.f`](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests | +| Expansion | Fortran | [odbc.f](https://davidpfister.github.io/odbc.f/) | Upstream fpm tests | ## Required driver behavior @@ -45,6 +45,22 @@ Each `SQLHDBC` owns: - its query session and active transaction; - its current catalog, credentials, and diagnostics. +Connection strings and DSNs must accept: + +- `Server` or `Endpoint`, `Database`, and `DSN`; +- `AuthMode=Anonymous`; +- `AuthMode=Token` with `Token`; +- `AuthMode=Static` with `User` and `Password`; +- `AuthMode=Metadata`, optionally with `MetadataHost` and `MetadataPort`; +- `AuthMode=ServiceAccount` with `ServiceAccountKeyFile`; +- `AuthMode=OAuth2` with `OAuth2KeyFile`; +- `AuthMode=Environment`; +- `IamEndpoint`, `RootCertificate`, `ClientCertificate`, and `ClientPrivateKey`. + +`UID`/`PWD`, `AccessToken`, `SaFile`, and `CaFile` are accepted aliases. The +authentication mode is inferred when exactly one credential type is present. +`SQLConnect` user and password arguments override DSN values. + The current implementation follows this model: `SQLAllocHandle(SQL_HANDLE_DBC)` creates a `TConnection`, and `TConnection::TYdbState` owns the SDK driver and clients. `SQLHENV` only tracks connection handles. Connections in the same environment therefore keep endpoint, database, session, transaction, and catalog state separate. `SQL_ATTR_CURRENT_CATALOG` uses a path below the connected database as `TablePathPrefix`. Setting it to another database path recreates only that connection's SDK state. @@ -88,12 +104,37 @@ The driver must support the operations used by the Core bindings: - independent statements and connections; - deterministic cleanup after errors. +### Row counts + +Data-modification statements must request Basic YDB query statistics. +`SQLRowCount` must sum updated and deleted rows from every query phase. +Parameter-array execution must sum the count of each executed parameter set. It +returns `-1` for other statements or when statistics do not contain a usable +count. + +### Debian package + +The driver is shipped as a separate `ydb-odbc` package with the same version as +the SDK release. It contains `libydb-odbc.so` in the multiarch library directory +and an unixODBC driver template. Package dependencies include `odbcinst` and the +shared-library dependencies derived from the built artifact. + +Installation registers the `YDB` driver with `odbcinst -i -d -f`. Upgrade +updates the registration without creating duplicate entries. Removal +unregisters only the entry owned by the package. The package does not install +`/etc/odbc.ini` or modify user DSNs. + +The package is built and published with the SDK release. A clean-container test +installs it, checks `odbcinst -q -d`, connects through `isql` and Qt QODBC, +tests an upgrade, removes the package, and verifies that unrelated drivers and +user DSNs remain unchanged. + ## Current limitations | Area | Limitation | |---|---| | SQL dialect | Statements are YQL. The driver rewrites ODBC escapes and `?` parameters; it is not a general ANSI SQL translator. | -| Authentication | Connection strings currently configure only endpoint, database, and DSN. User/password, token, service-account, metadata credentials, TLS certificates, and custom IAM settings are not wired into `TDriverConfig`. | +| Authentication | Connection strings currently configure only endpoint, database, and DSN. Authentication and TLS settings are not wired into `TDriverConfig`. | | Retry classification | The driver cannot infer whether arbitrary SQL is idempotent. Autocommit statement retries use `TRetryOperationSettings::Idempotent(false)`. This enables only retries safe for a non-idempotent operation and may return an error with an unknown execution outcome. See [YDB retry settings](https://ydb.tech/docs/en/recipes/ydb-sdk/retry) and [error handling](https://ydb.tech/docs/en/reference/ydb-sdk/error_handling). | | Explicit transactions | An ODBC transaction is not retried by the driver. Conflicts, node failures, maintenance, and network failures can abort it, including at commit. The application must open a new transaction and replay the entire unit of work in a retry loop. Retrying only the failed statement is incorrect ([query execution](https://ydb.tech/docs/en/concepts/query_execution/), [transactions](https://ydb.tech/docs/en/concepts/transactions)). | | Transaction isolation | Read-write connections support serializable and snapshot read-write modes. ODBC read-committed and read-uncommitted requests are rejected. | @@ -102,7 +143,7 @@ The driver must support the operations used by the Core bindings: | Cursor support | The current implementation is forward-only. `SQLFetchScroll` accepts only `SQL_FETCH_NEXT`; static scrolling and spill are planned. | | Result sets | Only the first result set is exposed. `SQLMoreResults` returns `SQL_NO_DATA`. | | Result buffering | Query execution uses the non-streaming SDK result and keeps it for cursor fetches. Large results can consume memory proportional to the result size. | -| Row counts | `SQLRowCount` returns `-1`; affected-row counts are not extracted from YDB query statistics. | +| Row counts | `SQLRowCount` currently returns `-1`; affected-row counts are not extracted from YDB query statistics. | | Prepare | `SQLPrepare` stores the query and counts client-side parameter markers. It does not create a persistent server-side prepared statement. | | Batches | Parameter arrays are accepted only for data-modification statements and execute sequentially. Earlier parameter sets may already be committed when a later set fails. | | Cancellation | Execution is synchronous. `SQLCancel` clears local cursor and parameter state but does not interrupt an in-flight SDK request. | @@ -135,23 +176,31 @@ Test output is converted to Allure without changing the upstream runner. Reports The complete matrix runs only: - after a merge into `odbc-driver-feature`; -- for a pushed tag. +- when a pull request has the special full-matrix tag. -It does not run for pull requests, direct non-merge pushes, schedules, or manual dispatches. +It does not run for ordinary pull requests, direct non-merge pushes, Git tags, schedules, or manual dispatches. -Each job starts a pinned YDB version, registers the driver in a job-local `odbcinst.ini`, verifies the upstream source, runs the upstream tests and example, and uploads native and Allure results. +Each job starts a pinned YDB version, installs the `ydb-odbc` package, creates a +job-local DSN, verifies the upstream source, runs the upstream tests and +example, and uploads native and Allure results. ## Delivery order -1. Add the registry, framework runner template, source verification, result conversion, and report aggregation. -2. Add static cursor emulation and cursor integration tests. -3. Add two-endpoint and two-database isolation tests. -4. Onboard Core bindings. -5. Onboard Expansion bindings. -6. Enable a zero-regression gate for stable bindings. +1. Stabilize the driver build and add the `ydb-odbc` package component. +2. Add clean install, upgrade, removal, unixODBC registration, `isql`, and Qt + QODBC package tests. +3. Complete connection settings, authentication, TLS, row counts, and + endpoint/database isolation. +4. Add static cursor emulation and cursor integration tests. +5. Add the registry, framework runner template, source verification, result + conversion, and report aggregation. +6. Onboard Core bindings and enable the full-matrix gate. +7. Onboard Expansion bindings after the initial release. ## Acceptance +- `ydb-odbc` passes clean install, upgrade, removal, registration, `isql`, and + Qt QODBC tests. - Every selected binding runs from a pinned, verified upstream source. - Binding implementation code is unchanged. - Every upstream database test is executed or reported as unsupported with its original test identifier. From 9fd35b0b20c7c6f548b41276420d828ebe0f2326 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Fri, 31 Jul 2026 14:45:28 +0300 Subject: [PATCH 47/55] fix qt odbc --- odbc/src/odbc_driver.cpp | 20 ++++++++++ odbc/src/utils/util.cpp | 61 +++++++++++++++++++++++++++++- odbc/src/utils/util.h | 4 +- odbc/tests/unit/conn_string_ut.cpp | 20 ++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 75f26c2bfc4..ec0564688dc 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 f3d06c81d51..63d1da10cc0 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 942b20293eb..9914ff9bac0 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 48bb5f7694d..4ed8cec3703 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"); +} From 4953ea598a9bc34ddba0fc0fb77052d65c75dca6 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Fri, 31 Jul 2026 14:48:38 +0300 Subject: [PATCH 48/55] add odbc deb package --- .github/workflows/release_publish.yaml | 2 +- .github/workflows/tests.yaml | 2 +- .github/workflows/warmup_cache.yaml | 2 +- README.md | 7 +- cmake/PackSDK.cmake | 15 ++ odbc/CMakeLists.txt | 58 ++++++-- odbc/README.md | 13 +- odbc/packaging/postinst.in | 10 ++ odbc/packaging/prerm.in | 12 ++ scripts/build_cpack_deb_packages.sh | 10 +- scripts/test_deb_packages.sh | 131 +++++++++++++++++- tests/deb_package/Dockerfile | 7 + tests/deb_package/odbc_qt_test/CMakeLists.txt | 10 ++ tests/deb_package/odbc_qt_test/main.cpp | 58 ++++++++ 14 files changed, 316 insertions(+), 21 deletions(-) create mode 100644 odbc/packaging/postinst.in create mode 100644 odbc/packaging/prerm.in create mode 100644 tests/deb_package/odbc_qt_test/CMakeLists.txt create mode 100644 tests/deb_package/odbc_qt_test/main.cpp diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml index 9b935a766c0..6e2c31686eb 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 54e79c108dd..15af71f068a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -228,7 +228,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 e5864270b4d..d0d31089b06 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 4f787127900..2f1479633d3 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 c204e7a49e3..c93a48c988a 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 16afdd5e708..c8c10f7d05e 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 13ad517fd6d..56219dbea39 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 00000000000..902817b6424 --- /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 00000000000..8e4fec76041 --- /dev/null +++ b/odbc/packaging/prerm.in @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +case "${1:-}" in + remove|upgrade|deconfigure) + if odbcinst -q -d -n YDB >/dev/null 2>&1; then + odbcinst -u -d -n YDB + fi + ;; +esac + +exit 0 diff --git a/scripts/build_cpack_deb_packages.sh b/scripts/build_cpack_deb_packages.sh index 91f0b8941e2..b5663cedb43 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 194c6090d8f..8d8c241c17f 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,101 @@ 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 + configure_components() { local name="$1" local components="${2:-}" diff --git a/tests/deb_package/Dockerfile b/tests/deb_package/Dockerfile index 89a02daea37..83fcb708f37 100644 --- a/tests/deb_package/Dockerfile +++ b/tests/deb_package/Dockerfile @@ -25,8 +25,15 @@ RUN apt-get update && apt-get install -y \ libre2-dev \ libc-ares-dev \ rapidjson-dev \ + unixodbc \ + odbcinst \ + qt6-base-dev \ + libqt6sql6-odbc \ && rm -rf /var/lib/apt/lists/* COPY test_project /test_project COPY component_test /component_test +COPY odbc_qt_test /odbc_qt_test +RUN cmake -S /odbc_qt_test -B /odbc_qt_test/build \ + && cmake --build /odbc_qt_test/build -j"$(nproc)" WORKDIR /test_project diff --git a/tests/deb_package/odbc_qt_test/CMakeLists.txt b/tests/deb_package/odbc_qt_test/CMakeLists.txt new file mode 100644 index 00000000000..097754e324d --- /dev/null +++ b/tests/deb_package/odbc_qt_test/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) +project(ydb_odbc_qt_test LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Sql) + +add_executable(ydb_odbc_qt_test main.cpp) +target_link_libraries(ydb_odbc_qt_test PRIVATE Qt6::Core Qt6::Sql) diff --git a/tests/deb_package/odbc_qt_test/main.cpp b/tests/deb_package/odbc_qt_test/main.cpp new file mode 100644 index 00000000000..10ca10c7aae --- /dev/null +++ b/tests/deb_package/odbc_qt_test/main.cpp @@ -0,0 +1,58 @@ +#include +#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; +} From 156617f22f989cff46a985a12be0977a73a78e9e Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Fri, 31 Jul 2026 16:23:52 +0300 Subject: [PATCH 49/55] run tests for odbc pull requests --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 15af71f068a..8dd30346773 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 From 01d0fb36d6d22bf45ad5da75a6920995df1d921c Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Fri, 31 Jul 2026 16:31:14 +0300 Subject: [PATCH 50/55] run only odbc tests for feature pull requests --- .github/workflows/tests.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8dd30346773..0a3ad051f18 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -55,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: | @@ -133,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)\.' @@ -145,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 }} From 9ccdd0ce96a780ad4d5f55bfe6d7d721304a93ee Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Mon, 3 Aug 2026 12:47:32 +0300 Subject: [PATCH 51/55] fix driver ownership --- odbc/packaging/prerm.in | 3 ++- scripts/test_deb_packages.sh | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/odbc/packaging/prerm.in b/odbc/packaging/prerm.in index 8e4fec76041..a91d015be1b 100644 --- a/odbc/packaging/prerm.in +++ b/odbc/packaging/prerm.in @@ -3,7 +3,8 @@ set -e case "${1:-}" in remove|upgrade|deconfigure) - if odbcinst -q -d -n YDB >/dev/null 2>&1; then + 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 ;; diff --git a/scripts/test_deb_packages.sh b/scripts/test_deb_packages.sh index 8d8c241c17f..2c1e4341949 100755 --- a/scripts/test_deb_packages.sh +++ b/scripts/test_deb_packages.sh @@ -159,6 +159,21 @@ 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 < Date: Tue, 4 Aug 2026 14:52:55 +0300 Subject: [PATCH 52/55] implemented connection string authentification --- .github/scripts/run_iam_integration_tests.sh | 11 +- .github/workflows/coverage.yml | 2 +- .github/workflows/tests.yaml | 11 +- odbc/CMakeLists.txt | 4 + odbc/README.md | 47 ++ odbc/odbc.ini | 2 +- odbc/src/connection.cpp | 89 ++-- odbc/src/connection.h | 18 +- odbc/src/connection_config.cpp | 431 +++++++++++++++++++ odbc/src/connection_config.h | 38 ++ odbc/src/statement.cpp | 93 +++- odbc/src/statement.h | 7 +- odbc/src/utils/util.cpp | 21 +- odbc/src/utils/util.h | 9 +- odbc/tests/CMakeLists.txt | 1 + odbc/tests/integration/CMakeLists.txt | 9 + odbc/tests/integration/authentication_it.cpp | 193 +++++++++ odbc/tests/integration/connection_api_it.cpp | 57 +++ odbc/tests/integration/statement_api_it.cpp | 104 ++++- odbc/tests/integration/test_utils.h | 5 +- 20 files changed, 1074 insertions(+), 78 deletions(-) create mode 100644 odbc/src/connection_config.cpp create mode 100644 odbc/src/connection_config.h create mode 100644 odbc/tests/integration/authentication_it.cpp diff --git a/.github/scripts/run_iam_integration_tests.sh b/.github/scripts/run_iam_integration_tests.sh index 43cf89f56c8..2c3a1e7220e 100755 --- a/.github/scripts/run_iam_integration_tests.sh +++ b/.github/scripts/run_iam_integration_tests.sh @@ -2,7 +2,7 @@ set -euo pipefail -IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' +IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' IAM_CONTAINER_NAME="${IAM_CONTAINER_NAME:-ydb-iam}" IAM_CTEST_JOBS="${IAM_CTEST_JOBS:-2}" IAM_READY_ATTEMPTS="${IAM_READY_ATTEMPTS:-60}" @@ -29,6 +29,13 @@ wait_for_iam_ydb() { return 1 } +provision_odbc_static_user() { + docker exec "${IAM_CONTAINER_NAME}" /ydb \ + --endpoint grpc://localhost:2136 \ + --database /local \ + sql -s "CREATE USER odbcauth PASSWORD '12345678'" +} + trap cleanup_iam EXIT cleanup_iam @@ -42,6 +49,8 @@ docker run -d --name "${IAM_CONTAINER_NAME}" --hostname localhost \ ghcr.io/ydb-platform/local-ydb:trunk wait_for_iam_ydb +provision_odbc_static_user YDB_ENDPOINT=localhost:2236 YDB_DATABASE=/local \ +YDB_ODBC_STATIC_USER=odbcauth YDB_ODBC_STATIC_PASSWORD=12345678 \ ctest -j"${IAM_CTEST_JOBS}" --test-dir build -R "${IAM_REGEX}" --output-on-failure diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c6d978e0df8..cd95e56c85d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -77,7 +77,7 @@ jobs: run: | set -euo pipefail - IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' + IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' FLAKY_REGEX='(ManyMessages|DiscoveryHang|DescribeHang)' EXCLUDE_REGEX="${IAM_REGEX}|${FLAKY_REGEX}" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0a3ad051f18..b20b4167524 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -142,7 +142,7 @@ jobs: if: github.event_name != 'pull_request' || github.base_ref == 'main' shell: bash run: | - IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' + IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' YDB_VERSION=${{ matrix.ydb-version }} ctest -j2 --preset integration \ -E "${IAM_REGEX}" --output-on-failure @@ -157,7 +157,14 @@ jobs: shell: bash run: | YDB_VERSION=${{ matrix.ydb-version }} \ - ctest --test-dir build/odbc/tests/integration -j2 --output-on-failure + ctest --test-dir build/odbc/tests/integration -j2 \ + -E '^OdbcAuthentication\.' --output-on-failure + + case '${{ matrix.ydb-version }}' in + 25.1|trunk) + ./.github/scripts/run_iam_integration_tests.sh + ;; + esac test-install: if: github.event_name != 'pull_request' || github.base_ref == 'main' diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index c8c10f7d05e..f5416b7b606 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(ydb-odbc SHARED src/utils/error_manager.cpp src/odbc_driver.cpp src/connection_attr.cpp + src/connection_config.cpp src/connection.cpp src/statement_attr.cpp src/statement.cpp @@ -34,6 +35,9 @@ target_link_libraries(ydb-odbc YDB-CPP-SDK::Table YDB-CPP-SDK::Scheme YDB-CPP-SDK::Driver + YDB-CPP-SDK::Credentials + YDB-CPP-SDK::Helpers + YDB-CPP-SDK::Iam ODBC::ODBC odbcinst ) diff --git a/odbc/README.md b/odbc/README.md index 56219dbea39..03d71210705 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -60,6 +60,48 @@ YDB=YDB ODBC Driver Driver=YDB Server=localhost:2136 Database=/local +AuthMode=Anonymous +``` + +`SQLDriverConnect` may also combine a DSN with explicit attributes. Values in +the connection string take precedence over values from the DSN. The user name +and password passed to `SQLConnect` take precedence over `User` and `Password` +in the DSN. + +### Connection attributes + +| Attribute | Meaning | +| --- | --- | +| `Endpoint` | YDB endpoint. `Server` is an alias. A `grpc://` prefix forces a plaintext connection; `grpcs://` enables TLS. | +| `Database` | YDB database path. | +| `DSN` | DSN section to load before applying the remaining connection-string attributes. | +| `AuthMode` | `Anonymous`, `Token`, `Static`, `Metadata`, `ServiceAccount`, `OAuth2`, or `Environment`. Values are case-insensitive. | +| `Token` | Access token for `Token` mode. `AccessToken` is an alias. | +| `User`, `Password` | Credentials for `Static` mode. `UID` and `PWD` are aliases. | +| `MetadataHost`, `MetadataPort` | Optional metadata service address for `Metadata` mode. | +| `ServiceAccountKeyFile` | Path to a service-account JSON key for `ServiceAccount` mode. `SaFile` is an alias. | +| `OAuth2KeyFile` | Path to an OAuth 2.0 token-exchange configuration file for `OAuth2` mode. | +| `IamEndpoint` | IAM gRPC endpoint for service-account authentication, or HTTP token endpoint override for OAuth 2.0 token exchange. | +| `RootCertificate` | Path to a PEM root-certificate file. `CaFile` is an alias. | +| `ClientCertificate`, `ClientPrivateKey` | Paths to the PEM client certificate and private key. They must be specified together. | + +If `AuthMode` is omitted, the driver infers it from exactly one credential +family (`Token`, static user/password, metadata settings, service-account key, +or OAuth 2.0 key). With no credential attributes it uses `Anonymous`. Conflicting +families and incomplete credentials are rejected with SQLSTATE `28000`. +`Environment` uses the SDK's standard `YDB_*_CREDENTIALS` variables. + +Certificate attributes contain file paths, not inline PEM. The driver reads the +files while establishing the ODBC connection. Supplying certificates enables +TLS; certificates cannot be combined with an explicitly plaintext `grpc://` +endpoint. + +Examples: + +```text +Driver=YDB;Endpoint=grpcs://ydb.example.net:2135;Database=/production;AuthMode=Token;Token=... +DSN=YDB;AuthMode=Static;UID=app;PWD=secret +Driver=YDB;Endpoint=localhost:2136;Database=/local;AuthMode=ServiceAccount;SaFile=/run/secrets/sa.json;IamEndpoint=grpc://localhost:4284 ``` ## Usage @@ -102,6 +144,11 @@ SQLCHAR connStr[] = "Driver=YDB;Endpoint=localhost:2136;Database=/local"; SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); ``` +For `INSERT`, `UPDATE`, `DELETE`, `UPSERT`, and `REPLACE`, `SQLRowCount` +returns the affected-row count reported by YDB query statistics. Counts from +executed parameter-array entries are summed; ignored entries are not counted. +For statements without an applicable count, it returns `-1`. + ## Parameters `?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` diff --git a/odbc/odbc.ini b/odbc/odbc.ini index a1ba3c951c3..f7334b046f3 100644 --- a/odbc/odbc.ini +++ b/odbc/odbc.ini @@ -6,4 +6,4 @@ Driver=YDB Description=YDB Database Connection Server=localhost:2136 Database=/local -AuthMode=none +AuthMode=Anonymous diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index fca372daaa1..9d14cb00796 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -1,11 +1,9 @@ #include "connection.h" #include "statement.h" -#include "utils/util.h" #include #include -#include #include #include #include @@ -13,8 +11,6 @@ #include #include -#include - namespace NYdb::NOdbc { TConnection::~TConnection() { @@ -27,53 +23,48 @@ void TConnection::DestroyYdbState() { Ydb_.reset(); } -SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { - const std::map params = ParseConnectionString(connectionString); - Endpoint_ = params.contains("Server") ? params.at("Server") : params.contains("Endpoint") ? params.at("Endpoint") : ""; - Database_ = params.contains("Database") ? params.at("Database") : ""; - DataSourceName_ = params.contains("DSN") ? params.at("DSN") : ""; - - if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in connection string"); +SQLRETURN TConnection::DriverConnect(std::string_view connectionString) { + TConnectionParameters explicitParameters = ParseAndNormalizeConnectionString(connectionString); + const auto dsnIt = explicitParameters.find("DSN"); + TConnectionParameters parameters; + if (dsnIt != explicitParameters.end() && !dsnIt->second.empty()) { + parameters = ReadDsnParameters(dsnIt->second); } - - TConnectionAttributes::NormalizeCatalogPath(Database_); - RecreateYdbClients(); - Attributes_.SetCurrentCatalog(Database_); + OverlayConnectionParameters(parameters, explicitParameters); + ApplyResolvedSettings(ResolveConnectionSettings(std::move(parameters))); return SQL_SUCCESS; } -SQLRETURN TConnection::Connect(const std::string& serverName, - const std::string& userName, - const std::string& auth) { - DataSourceName_ = serverName; - - char endpoint[256] = {0}; - char server[256] = {0}; - char database[256] = {0}; - - SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); - SQLGetPrivateProfileString(serverName.c_str(), "Server", "", server, sizeof(server), nullptr); - SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); - - Endpoint_ = endpoint[0] ? endpoint : server; - Database_ = database; - - if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in DSN"); +SQLRETURN TConnection::Connect(std::string_view serverName, + std::string_view userName, + std::string_view auth) { + TConnectionParameters parameters = ReadDsnParameters(serverName); + if (!userName.empty() || !auth.empty()) { + for (const std::string_view key : { + "Token", "MetadataHost", "MetadataPort", "ServiceAccountKeyFile", + "OAuth2KeyFile", "IamEndpoint"}) { + parameters.erase(std::string(key)); + } + parameters["AuthMode"] = "Static"; } - - TConnectionAttributes::NormalizeCatalogPath(Database_); - RecreateYdbClients(); - Attributes_.SetCurrentCatalog(Database_); + if (!userName.empty()) { + parameters["User"] = std::string(userName); + } + if (!auth.empty()) { + parameters["Password"] = std::string(auth); + } + ApplyResolvedSettings(ResolveConnectionSettings(std::move(parameters), std::string(serverName))); return SQL_SUCCESS; } SQLRETURN TConnection::Disconnect() { DestroyYdbState(); + DriverConfig_.reset(); DbmsVersionCache_.reset(); + Endpoint_.clear(); + Database_.clear(); DataSourceName_.clear(); return SQL_SUCCESS; } @@ -258,15 +249,31 @@ const std::string& TConnection::GetDbmsVersion() { } void TConnection::RecreateYdbClients() { + if (!DriverConfig_) { + throw TOdbcException("08003", 0, "Connection configuration is not available"); + } DestroyYdbState(); DbmsVersionCache_.reset(); - Ydb_.emplace(Endpoint_, Database_); + Ydb_.emplace(*DriverConfig_); +} + +void TConnection::ApplyResolvedSettings(TResolvedConnectionSettings&& settings) { + TConnectionAttributes::NormalizeCatalogPath(settings.Database); + settings.DriverConfig.SetDatabase(settings.Database); + + Endpoint_ = std::move(settings.Endpoint); + Database_ = std::move(settings.Database); + DataSourceName_ = std::move(settings.DataSourceName); + DriverConfig_.emplace(std::move(settings.DriverConfig)); + RecreateYdbClients(); + Attributes_.SetCurrentCatalog(Database_); } -void TConnection::RebindToDatabase(const std::string& newDatabase) { - std::string db = newDatabase; +void TConnection::RebindToDatabase(std::string_view newDatabase) { + std::string db(newDatabase); TConnectionAttributes::NormalizeCatalogPath(db); Database_ = std::move(db); + DriverConfig_->SetDatabase(Database_); Attributes_.SetCurrentCatalog(Database_); RecreateYdbClients(); } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index ae921ecd1af..74437b73eb5 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -2,6 +2,7 @@ #include "environment.h" #include "connection_attr.h" +#include "connection_config.h" #include "utils/error_manager.h" #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -32,8 +34,8 @@ class TConnection : public TErrorManager { NScheme::TSchemeClient SchemeClient; NTable::TTableClient TableClient; - TYdbState(const std::string& endpoint, const std::string& database) - : Driver(TDriverConfig().SetEndpoint(endpoint).SetDatabase(database)) + explicit TYdbState(const TDriverConfig& config) + : Driver(config) , QueryClient(Driver) , SchemeClient(Driver) , TableClient(Driver) @@ -45,6 +47,7 @@ class TConnection : public TErrorManager { }; std::optional Ydb_; + std::optional DriverConfig_; std::optional Tx_; std::optional QuerySession_; @@ -59,16 +62,17 @@ class TConnection : public TErrorManager { std::unordered_set Descriptors_; void DestroyYdbState(); + void ApplyResolvedSettings(TResolvedConnectionSettings&& settings); void RecreateYdbClients(); - void RebindToDatabase(const std::string& newDatabase); + void RebindToDatabase(std::string_view newDatabase); public: ~TConnection(); - SQLRETURN Connect(const std::string& serverName, - const std::string& userName, - const std::string& auth); + SQLRETURN Connect(std::string_view serverName, + std::string_view userName, + std::string_view auth); - SQLRETURN DriverConnect(const std::string& connectionString); + SQLRETURN DriverConnect(std::string_view connectionString); SQLRETURN Disconnect(); std::unique_ptr CreateStatement(); diff --git a/odbc/src/connection_config.cpp b/odbc/src/connection_config.cpp new file mode 100644 index 00000000000..a31d706bdd0 --- /dev/null +++ b/odbc/src/connection_config.cpp @@ -0,0 +1,431 @@ +#include "connection_config.h" + +#include "utils/error_manager.h" +#include "utils/util.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NYdb::NOdbc { + +namespace { + +std::string ToLower(std::string_view value) { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return result; +} + +std::optional CanonicalKey(std::string_view key) { + const std::string lower = ToLower(key); + if (lower == "driver") return "Driver"; + if (lower == "description") return "Description"; + if (lower == "dsn") return "DSN"; + if (lower == "server" || lower == "endpoint") return "Endpoint"; + if (lower == "database") return "Database"; + if (lower == "authmode") return "AuthMode"; + if (lower == "token" || lower == "accesstoken") return "Token"; + if (lower == "user" || lower == "uid") return "User"; + if (lower == "password" || lower == "pwd") return "Password"; + if (lower == "metadatahost") return "MetadataHost"; + if (lower == "metadataport") return "MetadataPort"; + if (lower == "serviceaccountkeyfile" || lower == "safile") return "ServiceAccountKeyFile"; + if (lower == "oauth2keyfile") return "OAuth2KeyFile"; + if (lower == "iamendpoint") return "IamEndpoint"; + if (lower == "rootcertificate" || lower == "cafile") return "RootCertificate"; + if (lower == "clientcertificate") return "ClientCertificate"; + if (lower == "clientprivatekey") return "ClientPrivateKey"; + return std::nullopt; +} + +[[noreturn]] void ThrowInvalidAttribute(std::string_view attribute, std::string_view detail) { + throw TOdbcException("01S00", 0, "Invalid connection string attribute " + + std::string(attribute) + ": " + std::string(detail)); +} + +bool Has(const TConnectionParameters& parameters, std::string_view key) { + return parameters.contains(std::string(key)); +} + +std::string_view Get(const TConnectionParameters& parameters, std::string_view key) { + const auto it = parameters.find(std::string(key)); + return it == parameters.end() ? std::string_view{} : std::string_view(it->second); +} + +std::string_view RequireNonEmpty( + const TConnectionParameters& parameters, + std::string_view key, + std::string_view authMode) +{ + const auto value = Get(parameters, key); + if (value.empty()) { + throw TOdbcException("28000", 0, std::string(authMode) + + " authentication requires " + std::string(key)); + } + return value; +} + +std::string ReadDsnValue(std::string_view dsn, std::string_view key) { + const std::string dsnName(dsn); + const std::string attribute(key); + std::vector buffer(256); + while (buffer.size() <= 1024 * 1024) { + const int length = SQLGetPrivateProfileString( + dsnName.c_str(), attribute.c_str(), "", buffer.data(), static_cast(buffer.size()), nullptr); + if (length < 0) { + return {}; + } + if (static_cast(length) + 1 < buffer.size()) { + return std::string(buffer.data(), static_cast(length)); + } + buffer.resize(buffer.size() * 2); + } + throw TOdbcException("08001", 0, "DSN attribute is too large: " + attribute); +} + +std::string ReadFile(std::string_view attribute, std::string_view path) { + const std::string pathString(path); + std::ifstream input(pathString, std::ios::binary); + if (!input) { + throw TOdbcException("08001", 0, "Unable to read " + std::string(attribute) + " file: " + pathString); + } + std::string content{ + std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + if (content.empty()) { + throw TOdbcException("08001", 0, std::string(attribute) + " file is empty: " + pathString); + } + return content; +} + +struct TEndpointSettings { + std::string Endpoint; + bool Secure = false; + bool ExplicitlyInsecure = false; +}; + +TEndpointSettings ParseYdbEndpoint(std::string_view value) { + constexpr std::string_view grpc = "grpc://"; + constexpr std::string_view grpcs = "grpcs://"; + if (value.starts_with(grpc)) { + return {std::string(value.substr(grpc.size())), false, true}; + } + if (value.starts_with(grpcs)) { + return {std::string(value.substr(grpcs.size())), true, false}; + } + if (value.find("://") != std::string::npos) { + ThrowInvalidAttribute("Endpoint", "only grpc:// and grpcs:// protocols are supported"); + } + return {std::string(value), false, false}; +} + +void ApplyIamEndpoint(TIamJwtFilename& params, std::string_view value) { + if (value.empty()) { + return; + } + constexpr std::string_view grpc = "grpc://"; + constexpr std::string_view grpcs = "grpcs://"; + if (value.starts_with(grpc)) { + params.Endpoint = std::string(value.substr(grpc.size())); + params.EnableSsl = false; + } else if (value.starts_with(grpcs)) { + params.Endpoint = std::string(value.substr(grpcs.size())); + params.EnableSsl = true; + } else if (value.find("://") != std::string::npos) { + ThrowInvalidAttribute("IamEndpoint", "service-account IAM supports grpc:// and grpcs://"); + } else { + params.Endpoint = std::string(value); + params.EnableSsl = true; + } +} + +uint32_t ParseMetadataPort(std::string_view value) { + if (value.empty()) { + ThrowInvalidAttribute("MetadataPort", "value is empty"); + } + uint32_t port = 0; + const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), port); + if (error != std::errc() || end != value.data() + value.size() || port == 0 || port > 65535) { + ThrowInvalidAttribute("MetadataPort", "expected an integer from 1 to 65535"); + } + return port; +} + +EAuthenticationMode ParseAuthMode(std::string_view value) { + const std::string mode = ToLower(value); + if (mode == "anonymous") return EAuthenticationMode::Anonymous; + if (mode == "token") return EAuthenticationMode::Token; + if (mode == "static") return EAuthenticationMode::Static; + if (mode == "metadata") return EAuthenticationMode::Metadata; + if (mode == "serviceaccount") return EAuthenticationMode::ServiceAccount; + if (mode == "oauth2") return EAuthenticationMode::OAuth2; + if (mode == "environment") return EAuthenticationMode::Environment; + throw TOdbcException("28000", 0, "Unknown authentication mode: " + std::string(value)); +} + +EAuthenticationMode ResolveAuthMode(const TConnectionParameters& parameters) { + const bool token = Has(parameters, "Token"); + const bool staticCredentials = Has(parameters, "User") || Has(parameters, "Password"); + const bool metadata = Has(parameters, "MetadataHost") || Has(parameters, "MetadataPort"); + const bool serviceAccount = Has(parameters, "ServiceAccountKeyFile"); + const bool oauth2 = Has(parameters, "OAuth2KeyFile"); + const size_t familyCount = static_cast(token) + static_cast(staticCredentials) + + static_cast(metadata) + static_cast(serviceAccount) + static_cast(oauth2); + + EAuthenticationMode mode; + if (Has(parameters, "AuthMode")) { + mode = ParseAuthMode(Get(parameters, "AuthMode")); + } else if (familyCount == 0) { + if (Has(parameters, "IamEndpoint")) { + throw TOdbcException("28000", 0, "IamEndpoint requires ServiceAccount or OAuth2 authentication"); + } + mode = EAuthenticationMode::Anonymous; + } else if (familyCount > 1) { + throw TOdbcException("28000", 0, "Authentication mode is ambiguous"); + } else if (token) { + mode = EAuthenticationMode::Token; + } else if (staticCredentials) { + mode = EAuthenticationMode::Static; + } else if (metadata) { + mode = EAuthenticationMode::Metadata; + } else if (serviceAccount) { + mode = EAuthenticationMode::ServiceAccount; + } else { + mode = EAuthenticationMode::OAuth2; + } + + const bool modeMatchesFamily = + (mode == EAuthenticationMode::Token && token && familyCount == 1) || + (mode == EAuthenticationMode::Static && staticCredentials && familyCount == 1) || + (mode == EAuthenticationMode::Metadata && (!familyCount || (metadata && familyCount == 1))) || + (mode == EAuthenticationMode::ServiceAccount && serviceAccount && familyCount == 1) || + (mode == EAuthenticationMode::OAuth2 && oauth2 && familyCount == 1) || + ((mode == EAuthenticationMode::Anonymous || mode == EAuthenticationMode::Environment) && familyCount == 0); + if (!modeMatchesFamily) { + throw TOdbcException("28000", 0, "Credential attributes conflict with the selected authentication mode"); + } + if (Has(parameters, "IamEndpoint") && mode != EAuthenticationMode::ServiceAccount && + mode != EAuthenticationMode::OAuth2) { + throw TOdbcException("28000", 0, "IamEndpoint is valid only for ServiceAccount or OAuth2 authentication"); + } + return mode; +} + +} // namespace + +TConnectionParameters ParseAndNormalizeConnectionString(std::string_view connectionString) { + TConnectionParameters parameters; + for (const auto& [key, value] : ParseConnectionStringEntries(connectionString)) { + const auto canonical = CanonicalKey(key); + if (!canonical) { + ThrowInvalidAttribute(key, "unknown attribute"); + } + parameters[*canonical] = value; + } + return parameters; +} + +TConnectionParameters ReadDsnParameters(std::string_view dsn) { + TConnectionParameters parameters; + // Aliases are read first so the canonical spelling wins inside a DSN. + static constexpr std::array keys = { + "Server", "UID", "PWD", "AccessToken", "SaFile", "CaFile", + "Driver", "Description", "Endpoint", "Database", "AuthMode", "Token", + "User", "Password", "MetadataHost", "MetadataPort", "ServiceAccountKeyFile", + "OAuth2KeyFile", "IamEndpoint", "RootCertificate", "ClientCertificate", + "ClientPrivateKey", "DSN"}; + for (const char* key : keys) { + std::string value = ReadDsnValue(dsn, key); + if (!value.empty()) { + parameters[*CanonicalKey(key)] = std::move(value); + } + } + return parameters; +} + +void OverlayConnectionParameters(TConnectionParameters& destination, const TConnectionParameters& source) { + std::optional selectedMode; + if (Has(source, "AuthMode")) { + selectedMode = ParseAuthMode(Get(source, "AuthMode")); + } else { + const bool token = Has(source, "Token"); + const bool staticCredentials = Has(source, "User") || Has(source, "Password"); + const bool metadata = Has(source, "MetadataHost") || Has(source, "MetadataPort"); + const bool serviceAccount = Has(source, "ServiceAccountKeyFile"); + const bool oauth2 = Has(source, "OAuth2KeyFile"); + const size_t familyCount = static_cast(token) + static_cast(staticCredentials) + + static_cast(metadata) + static_cast(serviceAccount) + static_cast(oauth2); + if (familyCount == 1) { + selectedMode = token ? EAuthenticationMode::Token + : staticCredentials ? EAuthenticationMode::Static + : metadata ? EAuthenticationMode::Metadata + : serviceAccount ? EAuthenticationMode::ServiceAccount + : EAuthenticationMode::OAuth2; + destination.erase("AuthMode"); + } + } + + if (selectedMode) { + const auto belongsToSelectedMode = [selectedMode](std::string_view key) { + switch (*selectedMode) { + case EAuthenticationMode::Token: + return key == "Token"; + case EAuthenticationMode::Static: + return key == "User" || key == "Password"; + case EAuthenticationMode::Metadata: + return key == "MetadataHost" || key == "MetadataPort"; + case EAuthenticationMode::ServiceAccount: + return key == "ServiceAccountKeyFile" || key == "IamEndpoint"; + case EAuthenticationMode::OAuth2: + return key == "OAuth2KeyFile" || key == "IamEndpoint"; + case EAuthenticationMode::Anonymous: + case EAuthenticationMode::Environment: + return false; + } + return false; + }; + for (const std::string_view key : { + "Token", "User", "Password", "MetadataHost", "MetadataPort", + "ServiceAccountKeyFile", "OAuth2KeyFile", "IamEndpoint"}) { + if (!belongsToSelectedMode(key)) { + destination.erase(std::string(key)); + } + } + } + + for (const auto& [key, value] : source) { + destination[key] = value; + } +} + +TResolvedConnectionSettings ResolveConnectionSettings( + TConnectionParameters parameters, + std::string dataSourceName) +{ + const std::string endpointValue(Get(parameters, "Endpoint")); + const std::string database(Get(parameters, "Database")); + if (endpointValue.empty() || database.empty()) { + throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database"); + } + + const TEndpointSettings endpoint = ParseYdbEndpoint(endpointValue); + const bool hasRoot = Has(parameters, "RootCertificate"); + const bool hasClientCert = Has(parameters, "ClientCertificate"); + const bool hasClientKey = Has(parameters, "ClientPrivateKey"); + if (hasClientCert != hasClientKey) { + throw TOdbcException("08001", 0, + "ClientCertificate and ClientPrivateKey must be specified together"); + } + const bool hasTlsFiles = hasRoot || hasClientCert; + if (endpoint.ExplicitlyInsecure && hasTlsFiles) { + ThrowInvalidAttribute("Endpoint", "grpc:// cannot be combined with TLS certificate attributes"); + } + + const EAuthenticationMode authMode = ResolveAuthMode(parameters); + TDriverConfig driverConfig = authMode == EAuthenticationMode::Environment + ? CreateFromEnvironment() + : TDriverConfig(); + driverConfig.SetEndpoint(endpoint.Endpoint).SetDatabase(database); + + switch (authMode) { + case EAuthenticationMode::Anonymous: + driverConfig.SetCredentialsProviderFactory(CreateInsecureCredentialsProviderFactory()); + break; + case EAuthenticationMode::Token: + driverConfig.SetCredentialsProviderFactory(CreateOAuthCredentialsProviderFactory( + std::string(RequireNonEmpty(parameters, "Token", "Token")))); + break; + case EAuthenticationMode::Static: + driverConfig.SetCredentialsProviderFactory(CreateLoginCredentialsProviderFactory({ + .User = std::string(RequireNonEmpty(parameters, "User", "Static")), + .Password = std::string(RequireNonEmpty(parameters, "Password", "Static")), + })); + break; + case EAuthenticationMode::Metadata: { + TIamHost params; + if (Has(parameters, "MetadataHost")) { + params.Host = std::string(RequireNonEmpty(parameters, "MetadataHost", "Metadata")); + } + if (Has(parameters, "MetadataPort")) { + params.Port = ParseMetadataPort(Get(parameters, "MetadataPort")); + } + driverConfig.SetCredentialsProviderFactory(CreateIamCredentialsProviderFactory(params)); + break; + } + case EAuthenticationMode::ServiceAccount: { + TIamJwtFilename params; + params.JwtFilename = std::string(RequireNonEmpty(parameters, "ServiceAccountKeyFile", "ServiceAccount")); + ApplyIamEndpoint(params, Get(parameters, "IamEndpoint")); + try { + driverConfig.SetCredentialsProviderFactory(CreateIamJwtFileCredentialsProviderFactory(params)); + } catch (const std::exception& ex) { + throw TOdbcException("08001", 0, + "Unable to load ServiceAccountKeyFile " + params.JwtFilename + ": " + ex.what()); + } + break; + } + case EAuthenticationMode::OAuth2: { + const std::string path(RequireNonEmpty(parameters, "OAuth2KeyFile", "OAuth2")); + try { + driverConfig.SetCredentialsProviderFactory( + CreateOauth2TokenExchangeFileCredentialsProviderFactory( + path, std::string(Get(parameters, "IamEndpoint")))); + } catch (const std::exception& ex) { + throw TOdbcException("08001", 0, + "Unable to load OAuth2KeyFile " + path + ": " + ex.what()); + } + break; + } + case EAuthenticationMode::Environment: + break; + } + + const bool secure = endpoint.Secure || hasTlsFiles; + std::string rootPem; + std::string clientCertPem; + std::string clientKeyPem; + if (hasRoot) { + rootPem = ReadFile("RootCertificate", Get(parameters, "RootCertificate")); + } + if (hasClientCert) { + clientCertPem = ReadFile("ClientCertificate", Get(parameters, "ClientCertificate")); + clientKeyPem = ReadFile("ClientPrivateKey", Get(parameters, "ClientPrivateKey")); + } + if (secure) { + driverConfig.UseSecureConnection(rootPem); + } + if (hasClientCert) { + driverConfig.UseClientCertificate(clientCertPem, clientKeyPem); + } + + if (dataSourceName.empty()) { + dataSourceName = std::string(Get(parameters, "DSN")); + } + return { + .Endpoint = endpoint.Endpoint, + .Database = database, + .DataSourceName = std::move(dataSourceName), + .DriverConfig = std::move(driverConfig), + }; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/connection_config.h b/odbc/src/connection_config.h new file mode 100644 index 00000000000..4192c78a8cf --- /dev/null +++ b/odbc/src/connection_config.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include +#include +#include + +namespace NYdb::NOdbc { + +enum class EAuthenticationMode { + Anonymous, + Token, + Static, + Metadata, + ServiceAccount, + OAuth2, + Environment, +}; + +using TConnectionParameters = std::map; + +struct TResolvedConnectionSettings { + std::string Endpoint; + std::string Database; + std::string DataSourceName; + TDriverConfig DriverConfig; +}; + +TConnectionParameters ParseAndNormalizeConnectionString(std::string_view connectionString); +TConnectionParameters ReadDsnParameters(std::string_view dsn); +void OverlayConnectionParameters(TConnectionParameters& destination, const TConnectionParameters& source); + +TResolvedConnectionSettings ResolveConnectionSettings( + TConnectionParameters parameters, + std::string dataSourceName = {}); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index e56425524ee..b396c35114c 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace NYdb::NOdbc { @@ -70,7 +71,7 @@ namespace { } bool StartsWithStatement( - const std::string& queryText, + std::string_view queryText, std::initializer_list keywords) { size_t i = 0; while (i < queryText.size()) { @@ -97,13 +98,43 @@ namespace { const size_t remaining = queryText.size() - i; for (const std::string_view keyword : keywords) { if (StartsWithPrefix( - queryText.c_str() + i, remaining, keyword.data(), keyword.size())) { + queryText.data() + i, remaining, keyword.data(), keyword.size())) { return true; } } return false; } + std::optional ExtractAffectedRows(const NQuery::TExecuteQueryResult& result) { + const auto& stats = result.GetStats(); + if (!stats) { + return std::nullopt; + } + + const uint64_t maxSqlLen = static_cast(std::numeric_limits::max()); + uint64_t affectedRows = 0; + bool hasTableAccess = false; + for (const auto& phase : stats->GetQueryPhases()) { + for (const auto& table : phase.GetTableAccess()) { + hasTableAccess = true; + const uint64_t updates = table.GetUpdates().GetRows(); + const uint64_t deletes = table.GetDeletes().GetRows(); + if (updates > maxSqlLen - affectedRows) { + return std::nullopt; + } + affectedRows += updates; + if (deletes > maxSqlLen - affectedRows) { + return std::nullopt; + } + affectedRows += deletes; + } + } + if (!hasTableAccess) { + return std::nullopt; + } + return static_cast(affectedRows); + } + } TStatement::TStatement(TConnection* conn) @@ -135,6 +166,7 @@ void TStatement::DetachDescriptor(TDescriptor* desc) { SQLRETURN TStatement::Prepare(const std::string& statementText) { RowsFetched_ = 0; + RowCount_ = -1; SetCursor(nullptr); PreparedQuery_ = statementText; IsPrepared_ = true; @@ -173,6 +205,11 @@ SQLRETURN TStatement::Execute() { } SQLRETURN TStatement::ExecuteInternal() { + const bool collectAffectedRows = StartsWithStatement( + PreparedQuery_, {"INSERT", "UPDATE", "DELETE", "UPSERT", "REPLACE"}); + RowCount_ = collectAffectedRows ? 0 : -1; + bool hasSuccessfulParamSet = false; + bool rowCountUsable = collectAffectedRows; const SQLULEN paramsetSize = ParamCount_ > 0 ? CurrentAppParamDesc_->GetArraySize() : 1; SQLUSMALLINT* const operations = CurrentAppParamDesc_->GetArrayStatusPtr(); SQLUSMALLINT* const statuses = ImpParamDesc_.GetArrayStatusPtr(); @@ -201,7 +238,16 @@ SQLRETURN TStatement::ExecuteInternal() { } return AddError("HY024", 0, "Invalid parameter operation value"); } - const SQLRETURN rc = ExecuteParamSet(paramSet); + std::optional affectedRows; + SQLRETURN rc; + try { + rc = ExecuteParamSet(paramSet, collectAffectedRows, affectedRows); + } catch (...) { + if (collectAffectedRows && !hasSuccessfulParamSet) { + RowCount_ = -1; + } + throw; + } if (statuses) { statuses[paramSet] = rc == SQL_SUCCESS_WITH_INFO ? SQL_PARAM_SUCCESS_WITH_INFO @@ -211,8 +257,20 @@ SQLRETURN TStatement::ExecuteInternal() { *processed = paramSet + 1; } if (rc == SQL_ERROR) { + if (collectAffectedRows && !hasSuccessfulParamSet) { + RowCount_ = -1; + } return SQL_ERROR; } + hasSuccessfulParamSet = true; + if (collectAffectedRows && rowCountUsable) { + if (!affectedRows || *affectedRows > std::numeric_limits::max() - RowCount_) { + RowCount_ = -1; + rowCountUsable = false; + } else { + RowCount_ += *affectedRows; + } + } if (rc == SQL_SUCCESS_WITH_INFO) { result = SQL_SUCCESS_WITH_INFO; } @@ -220,7 +278,11 @@ SQLRETURN TStatement::ExecuteInternal() { return result; } -SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { +SQLRETURN TStatement::ExecuteParamSet( + SQLULEN paramSet, + bool collectAffectedRows, + std::optional& affectedRows) +{ RowsFetched_ = 0; SetCursor(nullptr); auto client = Conn_->GetClient(); @@ -239,11 +301,14 @@ SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { const NYdb::NRetry::TRetryOperationSettings retrySettings = MakeAutocommitRetrySettings(); const NYdb::TStatus execStatus = client->RetryQuerySync( - [this, ¶ms](NQuery::TSession session) -> NYdb::TStatus { - NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); + [this, ¶ms, collectAffectedRows, &affectedRows](NQuery::TSession session) -> NYdb::TStatus { + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params, collectAffectedRows); if (!result.IsSuccess()) { return StatusFrom(result); } + if (collectAffectedRows) { + affectedRows = ExtractAffectedRows(result); + } SetCursor(CreateExecCursor(result)); return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }, @@ -252,11 +317,13 @@ SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { NStatusHelpers::ThrowOnError(execStatus); } else { NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); - NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params, collectAffectedRows); NStatusHelpers::ThrowOnError(result); + if (collectAffectedRows) { + affectedRows = ExtractAffectedRows(result); + } SetCursor(CreateExecCursor(result)); } - RowCount_ = -1; InAtExec_ = false; NeedDataParam_ = 0; NeedDataTokenDelivered_ = false; @@ -290,7 +357,11 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() return settings; } -NQuery::TExecuteQueryResult TStatement::ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params) { +NQuery::TExecuteQueryResult TStatement::ExecuteQuery( + NQuery::TSession& session, + const NYdb::TParams& params, + bool collectAffectedRows) +{ const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ : RewriteOdbcEscapes(PreparedQuery_); @@ -303,6 +374,9 @@ NQuery::TExecuteQueryResult TStatement::ExecuteQuery(NQuery::TSession& session, rewritten.Sql, {"CREATE", "DROP", "ALTER", "GRANT", "REVOKE"}); const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; + if (collectAffectedRows) { + execSettings.StatsMode(NQuery::EStatsMode::Basic); + } const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); @@ -625,6 +699,7 @@ SQLRETURN TStatement::NumParams(SQLSMALLINT* paramCount) { void TStatement::ResetForMetadata() { ClearErrors(); RowsFetched_ = 0; + RowCount_ = -1; SetCursor(nullptr); } diff --git a/odbc/src/statement.h b/odbc/src/statement.h index c67848d4be4..022d8b8eceb 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -115,7 +116,8 @@ class TStatement : public TErrorManager { std::vector GetDataOffsets_; SQLRETURN BuildParams(NYdb::TParams& out, SQLULEN paramSet); - SQLRETURN ExecuteParamSet(SQLULEN paramSet); + SQLRETURN ExecuteParamSet(SQLULEN paramSet, bool collectAffectedRows, + std::optional& affectedRows); void FillBoundColumns(); std::vector GetBoundParams(SQLULEN paramSet) const; void SetCursor(std::unique_ptr cursor); @@ -125,7 +127,8 @@ class TStatement : public TErrorManager { SQLUSMALLINT FindNextNeedDataParam() const; std::string GetTraversalRoot(const std::string& pattern) const; - NQuery::TExecuteQueryResult ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params); + NQuery::TExecuteQueryResult ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params, + bool collectAffectedRows); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); std::vector GetPatternEntries(const std::string& pattern); diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp index 63d1da10cc0..ba587c6edf1 100644 --- a/odbc/src/utils/util.cpp +++ b/odbc/src/utils/util.cpp @@ -100,15 +100,15 @@ bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t pre return true; } -std::map ParseConnectionString(const std::string& connectionString) { - std::map params; +TConnectionStringEntries ParseConnectionStringEntries(std::string_view connectionString) { + TConnectionStringEntries entries; size_t pos = 0; while (pos < connectionString.size()) { const size_t eq = connectionString.find('=', pos); if (eq == std::string::npos) { break; } - std::string key = connectionString.substr(pos, eq - pos); + std::string key(connectionString.substr(pos, eq - pos)); TrimInPlace(key); if (key.empty()) { break; @@ -140,7 +140,8 @@ std::map ParseConnectionString(const std::string& conn valueEnd = connectionString.size(); pos = connectionString.size(); } - params[key] = connectionString.substr(valueStart, valueEnd - valueStart); + entries.emplace_back( + std::move(key), std::string(connectionString.substr(valueStart, valueEnd - valueStart))); continue; } @@ -151,9 +152,17 @@ std::map ParseConnectionString(const std::string& conn } else { pos = connectionString.size(); } - std::string val = connectionString.substr(valueStart, valueEnd - valueStart); + std::string val(connectionString.substr(valueStart, valueEnd - valueStart)); TrimInPlace(val); - params[key] = val; + entries.emplace_back(std::move(key), std::move(val)); + } + return entries; +} + +std::map ParseConnectionString(std::string_view connectionString) { + std::map params; + for (auto&& [key, value] : ParseConnectionStringEntries(connectionString)) { + params[std::move(key)] = std::move(value); } return params; } diff --git a/odbc/src/utils/util.h b/odbc/src/utils/util.h index 9914ff9bac0..adb5d5d4906 100644 --- a/odbc/src/utils/util.h +++ b/odbc/src/utils/util.h @@ -7,6 +7,9 @@ #include #include +#include +#include +#include namespace NYdb::NOdbc { @@ -16,6 +19,10 @@ std::string GetString(SQLWCHAR* str, SQLINTEGER length); bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen); -std::map ParseConnectionString(const std::string& connectionString); +using TConnectionStringEntries = std::vector>; + +TConnectionStringEntries ParseConnectionStringEntries(std::string_view connectionString); + +std::map ParseConnectionString(std::string_view connectionString); } // namespace NYdb::NOdbc diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 8abcd081832..916fa4a0b8d 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -15,6 +15,7 @@ Driver=YDB Description=YDB Database Connection Server=${YDB_ODBC_DSN_SERVER} Database=${YDB_ODBC_DSN_DATABASE} +AuthMode=Anonymous ") add_subdirectory(integration) diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 1b116da8bd0..a2419c0e459 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -13,6 +13,15 @@ add_odbc_test(NAME odbc-connection_api_it connection_api_it.cpp ) +add_odbc_test(NAME odbc-authentication_it + SOURCES + authentication_it.cpp + LINK_LIBRARIES + tests-iam-mocks + client-oauth2-ut-helpers + cpp-testing-unittest +) + add_odbc_test(NAME odbc-statement_api_it SOURCES statement_api_it.cpp diff --git a/odbc/tests/integration/authentication_it.cpp b/odbc/tests/integration/authentication_it.cpp new file mode 100644 index 00000000000..d1450265e9e --- /dev/null +++ b/odbc/tests/integration/authentication_it.cpp @@ -0,0 +1,193 @@ +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using namespace NYdb::NTest; + +namespace { + +constexpr std::string_view RootToken = "root@builtin"; + +class TScopedEnvironmentVariable { +public: + TScopedEnvironmentVariable(std::string_view name, std::string_view value) + : Name_(name) + { + if (const char* oldValue = std::getenv(Name_.c_str())) { + OldValue_ = oldValue; + } + setenv(Name_.c_str(), std::string(value).c_str(), 1); + } + + ~TScopedEnvironmentVariable() { + if (OldValue_) { + setenv(Name_.c_str(), OldValue_->c_str(), 1); + } else { + unsetenv(Name_.c_str()); + } + } + +private: + std::string Name_; + std::optional OldValue_; +}; + +class OdbcAuthentication : public ::testing::Test { +protected: + void SetUp() override { + const char* endpoint = std::getenv("YDB_ENDPOINT"); + const char* database = std::getenv("YDB_DATABASE"); + if (!endpoint || !database) { + GTEST_SKIP() << "Authentication integration tests require the IAM-enabled YDB fixture"; + } + Endpoint_ = endpoint; + Database_ = database; + AllocEnv(&Env_); + } + + void TearDown() override { + Disconnect(); + if (Env_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, Env_); + } + } + + void Connect(std::string_view authenticationAttributes) { + Disconnect(); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, Env_, &Dbc_), SQL_SUCCESS); + std::string connectionString = "Driver=" ODBC_DRIVER_PATH ";Endpoint=" + Endpoint_ + + ";Database=" + Database_ + ";" + std::string(authenticationAttributes); + const SQLRETURN rc = SQLDriverConnect( + Dbc_, nullptr, reinterpret_cast(connectionString.data()), SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + CHECK_ODBC_OK(rc, Dbc_, SQL_HANDLE_DBC); + } + + void Disconnect() { + if (Dbc_ != SQL_NULL_HDBC) { + SQLDisconnect(Dbc_); + SQLFreeHandle(SQL_HANDLE_DBC, Dbc_); + Dbc_ = SQL_NULL_HDBC; + } + } + + void Execute(std::string_view query) { + SQLHSTMT statement = SQL_NULL_HSTMT; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &statement), SQL_SUCCESS); + std::string queryString(query); + const SQLRETURN rc = SQLExecDirect( + statement, reinterpret_cast(queryString.data()), SQL_NTS); + CHECK_ODBC_OK(rc, statement, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, statement); + } + + void SelectOne() { + Execute("SELECT 1"); + } + + SQLHENV Env_ = SQL_NULL_HENV; + SQLHDBC Dbc_ = SQL_NULL_HDBC; + std::string Endpoint_; + std::string Database_; +}; + +} // namespace + +TEST_F(OdbcAuthentication, TokenAndAccessTokenAlias) { + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Token;Token=root@builtin;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + ASSERT_NO_FATAL_FAILURE(Connect("AccessToken=root@builtin;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} + +TEST_F(OdbcAuthentication, Anonymous) { + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Anonymous;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} + +TEST_F(OdbcAuthentication, StaticUserAndPasswordAliases) { + const char* user = std::getenv("YDB_ODBC_STATIC_USER"); + const char* password = std::getenv("YDB_ODBC_STATIC_PASSWORD"); + if (!user || !password) { + GTEST_SKIP() << "Static authentication requires credentials provisioned by the IAM fixture"; + } + + ASSERT_NO_FATAL_FAILURE(Connect( + "AuthMode=Static;UID=" + std::string(user) + ";PWD=" + std::string(password) + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} + +TEST_F(OdbcAuthentication, MetadataService) { + TMetadataServer server; + server.SetResponse(HTTP_OK, MakeTokenResponse(std::string(RootToken), 3600)); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Metadata;MetadataHost=127.0.0.1;MetadataPort=" + + std::to_string(server.Port) + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + EXPECT_GE(server.GetRequestCount(), 1); + AssertMetadataRequestShape(server); +} + +TEST_F(OdbcAuthentication, ServiceAccountFileAndAlias) { + TIamTokenServiceStub stub; + stub.SetResponseToken(std::string(RootToken)); + TIamGrpcServer server(&stub); + ASSERT_TRUE(server.Start()); + + TTempDir tempDirectory; + const TString keyPath = tempDirectory.Path() / "service-account.json"; + TFileOutput(keyPath).Write(MakeJwtKeyFileContent()); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=ServiceAccount;SaFile=" + std::string(keyPath) + + ";IamEndpoint=grpc://" + server.Endpoint() + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + EXPECT_GE(stub.GetRequestCount(), 1); + ASSERT_TRUE(stub.HasLastRequest()); + AssertIamJwt(stub.GetLastRequest().jwt()); +} + +TEST_F(OdbcAuthentication, OAuth2TokenExchangeFile) { + TTestTokenExchangeServer server; + server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); + server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.ExpectedInputParams.emplace("subject_token", "odbc-subject-token"); + server.Check.ExpectedInputParams.emplace("subject_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.Response = + R"({"access_token":"root@builtin","token_type":"bearer","expires_in":3600})"; + + TTempDir tempDirectory; + const TString configPath = tempDirectory.Path() / "oauth2.json"; + TFileOutput(configPath).Write( + R"({"subject-credentials":{"type":"Fixed","token":"odbc-subject-token","token-type":"urn:ietf:params:oauth:token-type:access_token"}})"); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=OAuth2;OAuth2KeyFile=" + std::string(configPath) + + ";IamEndpoint=" + server.GetEndpoint() + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + server.CheckExpectations(); +} + +TEST_F(OdbcAuthentication, EnvironmentAccessToken) { + TScopedEnvironmentVariable serviceAccount("YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS", ""); + TScopedEnvironmentVariable anonymous("YDB_ANONYMOUS_CREDENTIALS", "0"); + TScopedEnvironmentVariable metadata("YDB_METADATA_CREDENTIALS", "0"); + TScopedEnvironmentVariable oauth2("YDB_OAUTH2_KEY_FILE", ""); + TScopedEnvironmentVariable token("YDB_ACCESS_TOKEN_CREDENTIALS", RootToken); + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Environment;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} diff --git a/odbc/tests/integration/connection_api_it.cpp b/odbc/tests/integration/connection_api_it.cpp index aff067a9e20..a8911bef802 100644 --- a/odbc/tests/integration/connection_api_it.cpp +++ b/odbc/tests/integration/connection_api_it.cpp @@ -81,6 +81,63 @@ TEST(ConnectionApi, SQLDriverConnectInvalidConnString) { SQLFreeHandle(SQL_HANDLE_ENV, env); } +TEST(ConnectionApi, SQLDriverConnectValidatesAuthenticationSettings) { + SQLHENV env; + AllocEnv(&env); + + const struct { + const char* ConnectionString; + const char* SqlState; + } cases[] = { + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=None;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;Token=a;UID=b;PWD=c;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Static;UID=b;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Metadata;MetadataPort=70000;", "01S00"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=ServiceAccount;SaFile=/missing/sa.json;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=OAuth2;OAuth2KeyFile=/missing/oauth2.json;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;ClientCertificate=client.pem;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=grpc://localhost:2136;Database=/local;CaFile=ca.pem;", "01S00"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;RootCertificate=/missing/ca.pem;", "08001"}, + }; + + for (const auto& testCase : cases) { + SQLHDBC dbc; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + const SQLRETURN rc = SQLDriverConnect( + dbc, nullptr, reinterpret_cast(const_cast(testCase.ConnectionString)), SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + ASSERT_EQ(rc, SQL_ERROR) << testCase.ConnectionString; + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), testCase.SqlState)) + << testCase.ConnectionString << ": " << GetOdbcError(dbc, SQL_HANDLE_DBC); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectSupportsAliasesAndDsnOverlay) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLCHAR connectionString[] = + "DSN=YDB;Endpoint=grpc://127.0.0.1:2136;AuthMode=Token;AccessToken=ignored-by-anonymous-server;"; + CHECK_ODBC_OK(SQLDriverConnect( + dbc, nullptr, connectionString, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT), + dbc, SQL_HANDLE_DBC); + + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + TEST(ConnectionApi, SQLConnectMissingDSN) { SQLHENV env; SQLHDBC dbc; diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index efa1bde2ed0..41abbca4383 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -380,14 +380,108 @@ TEST(StatementApi, RowCount) { AllocEnvAndConnect(&env, &dbc); ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - CHECK_ODBC_OK(SQLExecDirect(stmt, - (SQLCHAR*)"SELECT * FROM AS_TABLE(ListMap(ListFromRange(1, 4), ($x) -> (AsStruct($x AS v))))", + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS row_count_test", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE row_count_test (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), stmt, SQL_HANDLE_STMT); - - SQLLEN rowCount; + + SQLLEN rowCount = -2; CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); EXPECT_EQ(rowCount, -1); - + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPSERT INTO row_count_test (id, value) VALUES (1, 10), (2, 20), (3, 30)", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 3); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPDATE row_count_test SET value = value + 1 WHERE id <= 2", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 2); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"DELETE FROM row_count_test WHERE id = 3", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPDATE row_count_test SET value = 0 WHERE id = 100", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 0); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT * FROM row_count_test", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, -1); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE row_count_test", SQL_NTS); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, RowCountAggregatesParameterArrays) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS row_count_param_test", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE row_count_param_test (id Int32, value Int32, PRIMARY KEY (id))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLPrepare(stmt, + (SQLCHAR*)"UPSERT INTO row_count_param_test (id, value) VALUES (?, ?)", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER ids[] = {1, 2, 3}; + SQLINTEGER values[] = {10, 20, 30}; + SQLLEN idLengths[] = {0, 0, 0}; + SQLLEN valueLengths[] = {0, 0, 0}; + SQLUSMALLINT operations[] = {SQL_PARAM_PROCEED, SQL_PARAM_IGNORE, SQL_PARAM_PROCEED}; + SQLUSMALLINT statuses[] = {SQL_PARAM_UNUSED, SQL_PARAM_UNUSED, SQL_PARAM_UNUSED}; + SQLULEN processed = 0; + + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAMSET_SIZE, + reinterpret_cast(3), 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_OPERATION_PTR, + operations, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_STATUS_PTR, + statuses, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, + &processed, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, ids, 0, idLengths), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, values, 0, valueLengths), stmt, SQL_HANDLE_STMT); + + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + SQLLEN rowCount = -1; + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 2); + EXPECT_EQ(processed, 3); + EXPECT_EQ(statuses[0], SQL_PARAM_SUCCESS); + EXPECT_EQ(statuses[1], SQL_PARAM_UNUSED); + EXPECT_EQ(statuses[2], SQL_PARAM_SUCCESS); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE row_count_param_test", SQL_NTS); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h index a14cbac5f3a..b0eae273349 100644 --- a/odbc/tests/integration/test_utils.h +++ b/odbc/tests/integration/test_utils.h @@ -8,6 +8,7 @@ #include #include #include +#include inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { SQLCHAR sqlState[6] = {0}; @@ -26,8 +27,8 @@ inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Server=localhost:2136;Database=/local;"; -inline bool SqlStatePrefix(const std::string& diag, const char* state5) { - return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; +inline bool SqlStatePrefix(std::string_view diag, std::string_view state) { + return diag.starts_with(state); } inline void AllocEnv(SQLHENV* env) { From 879ba805ff875cff7f4a5e41376aba3d8f816525 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Tue, 4 Aug 2026 15:53:09 +0300 Subject: [PATCH 53/55] remove useless stats conditional --- odbc/README.md | 4 +++ odbc/src/connection.cpp | 17 ++++++++- odbc/src/connection_config.cpp | 8 +++-- odbc/src/connection_config.h | 5 ++- odbc/src/statement.cpp | 38 ++++++++------------ odbc/src/statement.h | 6 ++-- odbc/tests/integration/connection_api_it.cpp | 26 ++++++++++++++ odbc/tests/integration/statement_api_it.cpp | 22 ++++++++++++ 8 files changed, 94 insertions(+), 32 deletions(-) diff --git a/odbc/README.md b/odbc/README.md index 03d71210705..b4a9b634837 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -91,6 +91,10 @@ or OAuth 2.0 key). With no credential attributes it uses `Anonymous`. Conflictin families and incomplete credentials are rejected with SQLSTATE `28000`. `Environment` uses the SDK's standard `YDB_*_CREDENTIALS` variables. +Unrecognized connection-string attributes are ignored after reporting SQLSTATE +`01S00`; `SQLDriverConnect` completes with `SQL_SUCCESS_WITH_INFO`. This allows +ODBC applications to supply tool-specific attributes such as `APP` or `WSID`. + Certificate attributes contain file paths, not inline PEM. The driver reads the files while establishing the ODBC connection. Supplying certificates enables TLS; certificates cannot be combined with an explicitly plaintext `grpc://` diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 9d14cb00796..2a5c85645a2 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -24,7 +24,9 @@ void TConnection::DestroyYdbState() { } SQLRETURN TConnection::DriverConnect(std::string_view connectionString) { - TConnectionParameters explicitParameters = ParseAndNormalizeConnectionString(connectionString); + std::vector ignoredAttributes; + TConnectionParameters explicitParameters = + ParseAndNormalizeConnectionString(connectionString, ignoredAttributes); const auto dsnIt = explicitParameters.find("DSN"); TConnectionParameters parameters; if (dsnIt != explicitParameters.end() && !dsnIt->second.empty()) { @@ -33,6 +35,19 @@ SQLRETURN TConnection::DriverConnect(std::string_view connectionString) { OverlayConnectionParameters(parameters, explicitParameters); ApplyResolvedSettings(ResolveConnectionSettings(std::move(parameters))); + if (!ignoredAttributes.empty()) { + std::string message = ignoredAttributes.size() == 1 + ? "Invalid connection string attribute ignored: " + : "Invalid connection string attributes ignored: "; + for (size_t i = 0; i < ignoredAttributes.size(); ++i) { + if (i != 0) { + message += ", "; + } + message += ignoredAttributes[i]; + } + return AddError("01S00", 0, message, SQL_SUCCESS_WITH_INFO); + } + return SQL_SUCCESS; } diff --git a/odbc/src/connection_config.cpp b/odbc/src/connection_config.cpp index a31d706bdd0..88bee2a5249 100644 --- a/odbc/src/connection_config.cpp +++ b/odbc/src/connection_config.cpp @@ -232,12 +232,16 @@ EAuthenticationMode ResolveAuthMode(const TConnectionParameters& parameters) { } // namespace -TConnectionParameters ParseAndNormalizeConnectionString(std::string_view connectionString) { +TConnectionParameters ParseAndNormalizeConnectionString( + std::string_view connectionString, + std::vector& ignoredAttributes) +{ TConnectionParameters parameters; for (const auto& [key, value] : ParseConnectionStringEntries(connectionString)) { const auto canonical = CanonicalKey(key); if (!canonical) { - ThrowInvalidAttribute(key, "unknown attribute"); + ignoredAttributes.push_back(key); + continue; } parameters[*canonical] = value; } diff --git a/odbc/src/connection_config.h b/odbc/src/connection_config.h index 4192c78a8cf..2caa640e504 100644 --- a/odbc/src/connection_config.h +++ b/odbc/src/connection_config.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace NYdb::NOdbc { @@ -27,7 +28,9 @@ struct TResolvedConnectionSettings { TDriverConfig DriverConfig; }; -TConnectionParameters ParseAndNormalizeConnectionString(std::string_view connectionString); +TConnectionParameters ParseAndNormalizeConnectionString( + std::string_view connectionString, + std::vector& ignoredAttributes); TConnectionParameters ReadDsnParameters(std::string_view dsn); void OverlayConnectionParameters(TConnectionParameters& destination, const TConnectionParameters& source); diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index b396c35114c..43fd8627b5f 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -129,7 +129,7 @@ namespace { affectedRows += deletes; } } - if (!hasTableAccess) { + if (affectedRows == 0 && (!hasTableAccess || !result.GetResultSets().empty())) { return std::nullopt; } return static_cast(affectedRows); @@ -205,11 +205,9 @@ SQLRETURN TStatement::Execute() { } SQLRETURN TStatement::ExecuteInternal() { - const bool collectAffectedRows = StartsWithStatement( - PreparedQuery_, {"INSERT", "UPDATE", "DELETE", "UPSERT", "REPLACE"}); - RowCount_ = collectAffectedRows ? 0 : -1; + RowCount_ = 0; bool hasSuccessfulParamSet = false; - bool rowCountUsable = collectAffectedRows; + bool rowCountUsable = true; const SQLULEN paramsetSize = ParamCount_ > 0 ? CurrentAppParamDesc_->GetArraySize() : 1; SQLUSMALLINT* const operations = CurrentAppParamDesc_->GetArrayStatusPtr(); SQLUSMALLINT* const statuses = ImpParamDesc_.GetArrayStatusPtr(); @@ -241,9 +239,9 @@ SQLRETURN TStatement::ExecuteInternal() { std::optional affectedRows; SQLRETURN rc; try { - rc = ExecuteParamSet(paramSet, collectAffectedRows, affectedRows); + rc = ExecuteParamSet(paramSet, affectedRows); } catch (...) { - if (collectAffectedRows && !hasSuccessfulParamSet) { + if (!hasSuccessfulParamSet) { RowCount_ = -1; } throw; @@ -257,13 +255,13 @@ SQLRETURN TStatement::ExecuteInternal() { *processed = paramSet + 1; } if (rc == SQL_ERROR) { - if (collectAffectedRows && !hasSuccessfulParamSet) { + if (!hasSuccessfulParamSet) { RowCount_ = -1; } return SQL_ERROR; } hasSuccessfulParamSet = true; - if (collectAffectedRows && rowCountUsable) { + if (rowCountUsable) { if (!affectedRows || *affectedRows > std::numeric_limits::max() - RowCount_) { RowCount_ = -1; rowCountUsable = false; @@ -280,7 +278,6 @@ SQLRETURN TStatement::ExecuteInternal() { SQLRETURN TStatement::ExecuteParamSet( SQLULEN paramSet, - bool collectAffectedRows, std::optional& affectedRows) { RowsFetched_ = 0; @@ -301,14 +298,12 @@ SQLRETURN TStatement::ExecuteParamSet( const NYdb::NRetry::TRetryOperationSettings retrySettings = MakeAutocommitRetrySettings(); const NYdb::TStatus execStatus = client->RetryQuerySync( - [this, ¶ms, collectAffectedRows, &affectedRows](NQuery::TSession session) -> NYdb::TStatus { - NQuery::TExecuteQueryResult result = ExecuteQuery(session, params, collectAffectedRows); + [this, ¶ms, &affectedRows](NQuery::TSession session) -> NYdb::TStatus { + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); if (!result.IsSuccess()) { return StatusFrom(result); } - if (collectAffectedRows) { - affectedRows = ExtractAffectedRows(result); - } + affectedRows = ExtractAffectedRows(result); SetCursor(CreateExecCursor(result)); return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }, @@ -317,11 +312,9 @@ SQLRETURN TStatement::ExecuteParamSet( NStatusHelpers::ThrowOnError(execStatus); } else { NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); - NQuery::TExecuteQueryResult result = ExecuteQuery(session, params, collectAffectedRows); + NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); NStatusHelpers::ThrowOnError(result); - if (collectAffectedRows) { - affectedRows = ExtractAffectedRows(result); - } + affectedRows = ExtractAffectedRows(result); SetCursor(CreateExecCursor(result)); } InAtExec_ = false; @@ -359,8 +352,7 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() NQuery::TExecuteQueryResult TStatement::ExecuteQuery( NQuery::TSession& session, - const NYdb::TParams& params, - bool collectAffectedRows) + const NYdb::TParams& params) { const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ @@ -374,9 +366,7 @@ NQuery::TExecuteQueryResult TStatement::ExecuteQuery( rewritten.Sql, {"CREATE", "DROP", "ALTER", "GRANT", "REVOKE"}); const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; - if (collectAffectedRows) { - execSettings.StatsMode(NQuery::EStatsMode::Basic); - } + execSettings.StatsMode(NQuery::EStatsMode::Basic); const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 022d8b8eceb..643ca0bd0d8 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -116,8 +116,7 @@ class TStatement : public TErrorManager { std::vector GetDataOffsets_; SQLRETURN BuildParams(NYdb::TParams& out, SQLULEN paramSet); - SQLRETURN ExecuteParamSet(SQLULEN paramSet, bool collectAffectedRows, - std::optional& affectedRows); + SQLRETURN ExecuteParamSet(SQLULEN paramSet, std::optional& affectedRows); void FillBoundColumns(); std::vector GetBoundParams(SQLULEN paramSet) const; void SetCursor(std::unique_ptr cursor); @@ -127,8 +126,7 @@ class TStatement : public TErrorManager { SQLUSMALLINT FindNextNeedDataParam() const; std::string GetTraversalRoot(const std::string& pattern) const; - NQuery::TExecuteQueryResult ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params, - bool collectAffectedRows); + NQuery::TExecuteQueryResult ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); std::vector GetPatternEntries(const std::string& pattern); diff --git a/odbc/tests/integration/connection_api_it.cpp b/odbc/tests/integration/connection_api_it.cpp index a8911bef802..d1a48f31c1a 100644 --- a/odbc/tests/integration/connection_api_it.cpp +++ b/odbc/tests/integration/connection_api_it.cpp @@ -81,6 +81,32 @@ TEST(ConnectionApi, SQLDriverConnectInvalidConnString) { SQLFreeHandle(SQL_HANDLE_ENV, env); } +TEST(ConnectionApi, SQLDriverConnectIgnoresUnrecognizedAttributes) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLCHAR connectionString[] = + "Driver=" ODBC_DRIVER_PATH + ";Endpoint=localhost:2136;Database=/local;APP=PowerBI;WSID=desktop;Timeout=30;"; + const SQLRETURN rc = SQLDriverConnect( + dbc, nullptr, connectionString, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + ASSERT_EQ(rc, SQL_SUCCESS_WITH_INFO) << GetOdbcError(dbc, SQL_HANDLE_DBC); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "01S00")); + + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, reinterpret_cast(const_cast("SELECT 1")), SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + TEST(ConnectionApi, SQLDriverConnectValidatesAuthenticationSettings) { SQLHENV env; AllocEnv(&env); diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index 41abbca4383..5cc1dbc8457 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -419,6 +419,28 @@ TEST(StatementApi, RowCount) { EXPECT_EQ(rowCount, 0); SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"PRAGMA TablePathPrefix = \"/local\";\n" + "UPDATE row_count_test SET value = value + 1 WHERE id = 1", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLPrepare(stmt, + (SQLCHAR*)"DECLARE $p1 AS Int32?;\n" + "UPDATE row_count_test SET value = value + 1 WHERE id = $p1", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER nativeId = 2; + SQLLEN nativeIdLength = 0; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &nativeId, 0, &nativeIdLength), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_RESET_PARAMS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM row_count_test", SQL_NTS), stmt, SQL_HANDLE_STMT); From bf606c3498c8b22b9cb46d49423d8c7f6c6de49e Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Tue, 4 Aug 2026 16:10:59 +0300 Subject: [PATCH 54/55] fix SQL_DIAG_ROW_COUNT --- odbc/src/statement.cpp | 8 ++------ odbc/tests/integration/statement_api_it.cpp | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index 43fd8627b5f..419d8f53783 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -982,12 +982,8 @@ SQLRETURN TStatement::GetDiagField( SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { - if (recNumber == 0 && diagIdentifier == SQL_DIAG_ROW_COUNT) { - if (!diagInfoPtr) { - return SQL_ERROR; - } - *reinterpret_cast(diagInfoPtr) = -1; - return SQL_SUCCESS; + if (diagIdentifier == SQL_DIAG_ROW_COUNT) { + return RowCount(static_cast(diagInfoPtr)); } return TErrorManager::GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); } diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index 5cc1dbc8457..9dbb1b188b1 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -394,8 +394,12 @@ TEST(StatementApi, RowCount) { CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"UPSERT INTO row_count_test (id, value) VALUES (1, 10), (2, 20), (3, 30)", SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLLEN diagRowCount = -2; + CHECK_ODBC_OK(SQLGetDiagField(SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_ROW_COUNT, + &diagRowCount, 0, nullptr), stmt, SQL_HANDLE_STMT); CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); EXPECT_EQ(rowCount, 3); + EXPECT_EQ(diagRowCount, rowCount); SQLFreeStmt(stmt, SQL_CLOSE); CHECK_ODBC_OK(SQLExecDirect(stmt, From 062fb784eddac1191b26a2f94a38c535355ca281 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Tue, 4 Aug 2026 18:14:06 +0300 Subject: [PATCH 55/55] fix issues and ci --- .github/scripts/run_iam_integration_tests.sh | 11 +++++++---- odbc/src/connection.cpp | 3 +++ odbc/src/connection_config.cpp | 2 +- odbc/tests/integration/authentication_it.cpp | 18 ++++++++++++++++-- odbc/tests/integration/connection_api_it.cpp | 4 ++-- scripts/googleapis_deb/CMakeLists.txt | 2 ++ 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/scripts/run_iam_integration_tests.sh b/.github/scripts/run_iam_integration_tests.sh index 2c3a1e7220e..516ee6700dd 100755 --- a/.github/scripts/run_iam_integration_tests.sh +++ b/.github/scripts/run_iam_integration_tests.sh @@ -16,7 +16,7 @@ cleanup_iam() { wait_for_iam_ydb() { for _ in $(seq 1 "${IAM_READY_ATTEMPTS}"); do if docker exec -e "YDB_TOKEN=${IAM_TOKEN}" "${IAM_CONTAINER_NAME}" /ydb \ - --endpoint grpc://localhost:2136 \ + --endpoint grpc://localhost:2236 \ --database /local \ sql -s 'select 1' >/dev/null 2>&1; then return 0 @@ -30,8 +30,8 @@ wait_for_iam_ydb() { } provision_odbc_static_user() { - docker exec "${IAM_CONTAINER_NAME}" /ydb \ - --endpoint grpc://localhost:2136 \ + docker exec -e "YDB_TOKEN=${IAM_TOKEN}" "${IAM_CONTAINER_NAME}" /ydb \ + --endpoint grpc://localhost:2236 \ --database /local \ sql -s "CREATE USER odbcauth PASSWORD '12345678'" } @@ -40,8 +40,11 @@ trap cleanup_iam EXIT cleanup_iam docker run -d --name "${IAM_CONTAINER_NAME}" --hostname localhost \ - -p 2235:2135 -p 2236:2136 -p 28765:8765 \ + -p 2235:2235 -p 2236:2236 -p 28765:28765 \ -v /tmp/ydb_iam_certs:/ydb_certs \ + -e GRPC_TLS_PORT=2235 \ + -e GRPC_PORT=2236 \ + -e MON_PORT=28765 \ -e YDB_USE_IN_MEMORY_PDISKS=true \ -e YDB_TABLE_ENABLE_PREPARED_DDL=true \ -e YDB_ENFORCE_USER_TOKEN_REQUIREMENT=true \ diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index 2a5c85645a2..b055b965d2d 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -285,6 +285,9 @@ void TConnection::ApplyResolvedSettings(TResolvedConnectionSettings&& settings) } void TConnection::RebindToDatabase(std::string_view newDatabase) { + if (!DriverConfig_) { + throw TOdbcException("08003", 0, "Connection configuration is not available"); + } std::string db(newDatabase); TConnectionAttributes::NormalizeCatalogPath(db); Database_ = std::move(db); diff --git a/odbc/src/connection_config.cpp b/odbc/src/connection_config.cpp index 88bee2a5249..db1735def5d 100644 --- a/odbc/src/connection_config.cpp +++ b/odbc/src/connection_config.cpp @@ -58,7 +58,7 @@ std::optional CanonicalKey(std::string_view key) { } [[noreturn]] void ThrowInvalidAttribute(std::string_view attribute, std::string_view detail) { - throw TOdbcException("01S00", 0, "Invalid connection string attribute " + + throw TOdbcException("HY024", 0, "Invalid connection string attribute " + std::string(attribute) + ": " + std::string(detail)); } diff --git a/odbc/tests/integration/authentication_it.cpp b/odbc/tests/integration/authentication_it.cpp index d1450265e9e..4fc9eae283a 100644 --- a/odbc/tests/integration/authentication_it.cpp +++ b/odbc/tests/integration/authentication_it.cpp @@ -98,6 +98,19 @@ class OdbcAuthentication : public ::testing::Test { Execute("SELECT 1"); } + void ExpectSelectOneAuthFailure() { + SQLHSTMT statement = SQL_NULL_HSTMT; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &statement), SQL_SUCCESS); + SQLCHAR query[] = "SELECT 1"; + const SQLRETURN rc = SQLExecDirect(statement, query, SQL_NTS); + EXPECT_EQ(rc, SQL_ERROR); + if (rc == SQL_ERROR) { + const std::string error = GetOdbcError(statement, SQL_HANDLE_STMT); + EXPECT_TRUE(SqlStatePrefix(error, "28000")) << error; + } + EXPECT_EQ(SQLFreeHandle(SQL_HANDLE_STMT, statement), SQL_SUCCESS); + } + SQLHENV Env_ = SQL_NULL_HENV; SQLHDBC Dbc_ = SQL_NULL_HDBC; std::string Endpoint_; @@ -116,7 +129,7 @@ TEST_F(OdbcAuthentication, TokenAndAccessTokenAlias) { TEST_F(OdbcAuthentication, Anonymous) { ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Anonymous;")); - ASSERT_NO_FATAL_FAILURE(SelectOne()); + ASSERT_NO_FATAL_FAILURE(ExpectSelectOneAuthFailure()); } TEST_F(OdbcAuthentication, StaticUserAndPasswordAliases) { @@ -178,7 +191,8 @@ TEST_F(OdbcAuthentication, OAuth2TokenExchangeFile) { ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=OAuth2;OAuth2KeyFile=" + std::string(configPath) + ";IamEndpoint=" + server.GetEndpoint() + ";")); - ASSERT_NO_FATAL_FAILURE(SelectOne()); + // The local IAM fixture accepts builtin tokens, not OAuth "Bearer" credentials. + ASSERT_NO_FATAL_FAILURE(ExpectSelectOneAuthFailure()); server.CheckExpectations(); } diff --git a/odbc/tests/integration/connection_api_it.cpp b/odbc/tests/integration/connection_api_it.cpp index d1a48f31c1a..4d029aeaa39 100644 --- a/odbc/tests/integration/connection_api_it.cpp +++ b/odbc/tests/integration/connection_api_it.cpp @@ -118,11 +118,11 @@ TEST(ConnectionApi, SQLDriverConnectValidatesAuthenticationSettings) { {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=None;", "28000"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;Token=a;UID=b;PWD=c;", "28000"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Static;UID=b;", "28000"}, - {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Metadata;MetadataPort=70000;", "01S00"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Metadata;MetadataPort=70000;", "HY024"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=ServiceAccount;SaFile=/missing/sa.json;", "08001"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=OAuth2;OAuth2KeyFile=/missing/oauth2.json;", "08001"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;ClientCertificate=client.pem;", "08001"}, - {"Driver=" ODBC_DRIVER_PATH ";Endpoint=grpc://localhost:2136;Database=/local;CaFile=ca.pem;", "01S00"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=grpc://localhost:2136;Database=/local;CaFile=ca.pem;", "HY024"}, {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;RootCertificate=/missing/ca.pem;", "08001"}, }; diff --git a/scripts/googleapis_deb/CMakeLists.txt b/scripts/googleapis_deb/CMakeLists.txt index 0c96c2358d4..b0cf2700666 100644 --- a/scripts/googleapis_deb/CMakeLists.txt +++ b/scripts/googleapis_deb/CMakeLists.txt @@ -48,6 +48,8 @@ endforeach() add_library(api-common-protos STATIC ${PROTO_SRCS} ${PROTO_HDRS}) add_library(yandex-googleapis-api-common-protos::api-common-protos ALIAS api-common-protos) +set_target_properties(api-common-protos PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(api-common-protos PUBLIC $ $