Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions .github/scripts/run_iam_integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand All @@ -29,19 +29,31 @@ wait_for_iam_ydb() {
return 1
}

provision_odbc_static_user() {
docker exec -e "YDB_TOKEN=${IAM_TOKEN}" "${IAM_CONTAINER_NAME}" /ydb \
--endpoint grpc://localhost:2236 \
--database /local \
sql -s "CREATE USER odbcauth PASSWORD '12345678'"
}

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 \
-e YDB_DEFAULT_CLUSTERADMIN=root@builtin \
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
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
4 changes: 4 additions & 0 deletions odbc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
Expand Down
51 changes: 51 additions & 0 deletions odbc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,52 @@ 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.

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://`
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
Expand Down Expand Up @@ -102,6 +148,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 <type>?;`
Expand Down
2 changes: 1 addition & 1 deletion odbc/odbc.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ Driver=YDB
Description=YDB Database Connection
Server=localhost:2136
Database=/local
AuthMode=none
AuthMode=Anonymous
107 changes: 66 additions & 41 deletions odbc/src/connection.cpp
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
#include "connection.h"
#include "statement.h"
#include "utils/util.h"

#include <ydb-cpp-sdk/client/result/result.h>
#include <ydb-cpp-sdk/client/types/status/status.h>

#include <map>
#include <string>
#include <algorithm>
#include <cstring>

#include <sql.h>
#include <sqlext.h>

#include <odbcinst.h>

namespace NYdb::NOdbc {

TConnection::~TConnection() {
Expand All @@ -27,53 +23,63 @@ void TConnection::DestroyYdbState() {
Ydb_.reset();
}

SQLRETURN TConnection::DriverConnect(const std::string& connectionString) {
const std::map<std::string, std::string> 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) {
std::vector<std::string> ignoredAttributes;
TConnectionParameters explicitParameters =
ParseAndNormalizeConnectionString(connectionString, ignoredAttributes);
const auto dsnIt = explicitParameters.find("DSN");
TConnectionParameters parameters;
if (dsnIt != explicitParameters.end() && !dsnIt->second.empty()) {
parameters = ReadDsnParameters(dsnIt->second);
}
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);
}

TConnectionAttributes::NormalizeCatalogPath(Database_);
RecreateYdbClients();
Attributes_.SetCurrentCatalog(Database_);

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;
}
Expand Down Expand Up @@ -258,15 +264,34 @@ 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::RebindToDatabase(const std::string& newDatabase) {
std::string db = newDatabase;
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(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);
DriverConfig_->SetDatabase(Database_);
Comment thread
Shfdis marked this conversation as resolved.
Attributes_.SetCurrentCatalog(Database_);
RecreateYdbClients();
}
Expand Down
18 changes: 11 additions & 7 deletions odbc/src/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "environment.h"
#include "connection_attr.h"
#include "connection_config.h"
#include "utils/error_manager.h"

#include <ydb-cpp-sdk/client/driver/driver.h>
Expand All @@ -15,6 +16,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <unordered_set>

Expand All @@ -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)
Expand All @@ -45,6 +47,7 @@ class TConnection : public TErrorManager {
};

std::optional<TYdbState> Ydb_;
std::optional<TDriverConfig> DriverConfig_;
std::optional<NQuery::TTransaction> Tx_;
std::optional<NQuery::TSession> QuerySession_;

Expand All @@ -59,16 +62,17 @@ class TConnection : public TErrorManager {
std::unordered_set<TDescriptor*> 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<TStatement> CreateStatement();
Expand Down
Loading
Loading