Skip to content

Added device flow, native error handling to ODBC, Powershell installer#26

Closed
dprophet wants to merge 1 commit intotrinodb:mainfrom
bloomberg:main
Closed

Added device flow, native error handling to ODBC, Powershell installer#26
dprophet wants to merge 1 commit intotrinodb:mainfrom
bloomberg:main

Conversation

@dprophet
Copy link
Contributor

@dprophet dprophet commented Oct 13, 2025

This checkin includes the following

  • Initial checkin of device flow (aka device grant)
  • Better error reporting back to ODBC interface
  • Powershell installer for development ODBC driver.

Summary by Sourcery

Add device flow authentication, implement native ODBC error mapping and reporting, and provide a PowerShell installer for the development ODBC driver.

New Features:

  • Support OAuth2 device flow authentication in the ODBC driver.
  • Add PowerShell script to install and register the development ODBC driver on Windows.

Enhancements:

  • Introduce TrinoOdbcErrorHandler to map Trino JSON errors to ODBC diagnostics and integrate it into TrinoQuery and SQLGetDiagRec.
  • Add input validation and enhanced CURL error logging in connection and query handling.

Build:

  • Update CMakeLists.txt to include device flow provider and error handler source files.

@cla-bot cla-bot bot added the cla-signed label Oct 13, 2025
@sourcery-ai
Copy link

sourcery-ai bot commented Oct 13, 2025

Reviewer's Guide

This PR adds full support for OAuth2 device flow authentication, implements a native ODBC error‐mapping layer with chunked diagnostics, and ships a PowerShell script to install the development ODBC driver.

Class diagram for new and updated authentication providers (Device Flow)

classDiagram
    class AuthConfig {
    }
    class TokenCacheAuthProviderBase {
    }
    class DeviceCredAuthConfig {
        +oidcDiscoveryUrl: string
        +clientId: string
        +clientSecret: string
        +scope: string
        +grantType: string
        +tokenEndpoint: string
        +obtainAccessToken(curl, responseData, responseHeaderData): string
    }
    AuthConfig <|-- TokenCacheAuthProviderBase
    TokenCacheAuthProviderBase <|-- DeviceCredAuthConfig
    class "getDeviceFlowAuthProvider()" {
    }
    "getDeviceFlowAuthProvider()" ..> DeviceCredAuthConfig
    class ConnectionConfig {
        +tokenEndpoint: string
        +grantType: string
        +authMethod: ApiAuthMethod
        +authConfigPtr: unique_ptr<AuthConfig>
    }
    ConnectionConfig --> AuthConfig
    class DriverConfig {
        +tokenEndpoint: string
        +grantType: string
        +getTokenEndpoint(): string
        +setTokenEndpoint(tokenEndpoint: string)
        +getGrantType(): string
        +setGrantType(grantType: string)
    }
    class ApiAuthMethod {
        AM_NO_AUTH
        AM_EXTERNAL_AUTH
        AM_CLIENT_CRED_AUTH
        AM_DEVICE_FLOW
    }
    ConnectionConfig --> ApiAuthMethod
Loading

Class diagram for TrinoOdbcErrorHandler and error mapping

classDiagram
    class TrinoOdbcErrorHandler {
        +FromTrinoJson(err: json, queryId: string): OdbcError
        +LookupEntryByName(errorName: string): Entry*
        +ReloadMappingFromJson(path: string, error_out: string*): bool
        +SetConfigDirectory(dir: string)
        +GetEffectiveConfigPath(): string
        +OdbcErrorToString(err: OdbcError, include_stack: bool): string
    }
    class TrinoOdbcErrorHandler_OdbcError {
        +ret: SQLRETURN
        +sqlstate: string
        +native: SQLINTEGER
        +message: string
        +description: string
        +stack: vector<string>
        +lineNumber: optional<int>
        +columnNumber: optional<int>
        +queryId: string
    }
    class TrinoOdbcErrorHandler_Entry {
        +trino_code: int
        +trino_name: string
        +sqlstate: string
        +description: string
    }
    TrinoOdbcErrorHandler ..> TrinoOdbcErrorHandler_OdbcError
    TrinoOdbcErrorHandler ..> TrinoOdbcErrorHandler_Entry
Loading

File-Level Changes

Change Details Files
Add OAuth2 device flow authentication provider
  • Introduce AM_DEVICE_FLOW enum and map to name
  • Extend DriverConfig and ConnectionConfig with grantType and tokenEndpoint fields
  • Modify Connection::configure and ConnectionConfig constructor to accept device flow params
  • Add getDeviceFlowAuthProvider and implement deviceFlowAuthProvider (discovery, polling, browser launch)
  • Update CMakeLists.txt to include new deviceFlowAuthProvider sources
src/driver/config/driverConfig.hpp
src/driver/config/driverConfig.cpp
src/trinoAPIWrapper/apiAuthMethod.hpp
src/trinoAPIWrapper/connectionConfig.hpp
src/trinoAPIWrapper/connectionConfig.cpp
src/driver/handles/connHandle.hpp
src/driver/handles/connHandle.cpp
src/CMakeLists.txt
src/trinoAPIWrapper/authProvider/deviceFlowAuthProvider.hpp
src/trinoAPIWrapper/authProvider/deviceFlowAuthProvider.cpp
Implement TrinoOdbcErrorHandler and integrate into TrinoQuery
  • Create TrinoOdbcErrorHandler module with JSON‐based error catalog and mapping to SQLSTATE/native codes
  • Add optional field, parseTrinoError stub, and setQueryId/getQueryId methods in TrinoQuery
  • Integrate FromTrinoJson in updateSelfFromResponse and log via OdbcErrorToString
  • Reset odbcError in TrinoQuery::reset
src/trinoAPIWrapper/TrinoOdbcErrorHandler.hpp
src/trinoAPIWrapper/TrinoOdbcErrorHandler.cpp
src/trinoAPIWrapper/trinoQuery.hpp
src/trinoAPIWrapper/trinoQuery.cpp
Enhance ODBC diagnostics functions to consume mapped errors
  • Update SQLGetDiagRec to emit SQLSTATE, native error, and chunk text lines from OdbcError
  • Log HandleType in diagnostics entry
  • Return SQL_ERROR in SQLNumResultCols when TrinoQuery::hasError()
src/driver/getDiagRec.cpp
src/driver/numResultCols.cpp
Propagate queryId through statements and diagnostics
  • Add setQueryId/getQueryId and hasError/getError to TrinoQuery and Statement
  • Extract queryId from both "queryId" and legacy "id" JSON fields
  • Store queryId in Statement and pass it to OdbcError creation
src/trinoAPIWrapper/trinoQuery.hpp
src/trinoAPIWrapper/trinoQuery.cpp
src/driver/handles/statementHandle.hpp
src/driver/handles/statementHandle.cpp
Add PowerShell installer for development ODBC driver
  • New install_development_drivers.ps1 with admin check, DLL resolution, registry key updates
  • Generate accompanying .reg file for manual import
  • Support 32/64-bit registry entries and build variants
install_development_drivers.ps1

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/trinoAPIWrapper/trinoQuery.cpp:197-198` </location>
<code_context>

   CURLcode res = curl_easy_perform(curl);

+  if (res != CURLE_OK) {
+    WriteLog(LL_ERROR, std::string("CURL error: ") + curl_easy_strerror(res));
+  }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider propagating CURL errors to the caller, not just logging.

If the caller needs to handle network errors, return an error code or set an error state instead of only logging.

Suggested implementation:

```cpp
  CURLcode res = curl_easy_perform(curl);

  if (res != CURLE_OK) {
    WriteLog(LL_ERROR, std::string("CURL error: ") + curl_easy_strerror(res));
    return res; // Propagate error to caller
  }

```

You will need to ensure that the containing function's return type is `CURLcode` (or some error-propagating type), and that callers handle the returned error code appropriately. If the function currently returns `void`, change its signature to `CURLcode`. If you prefer to use exceptions, you could throw a custom exception instead of returning.
</issue_to_address>

### Comment 2
<location> `src/trinoAPIWrapper/trinoQuery.cpp:486-487` </location>
<code_context>
+  }
+}
+
+const TrinoOdbcErrorHandler::OdbcError& TrinoQuery::getError() const {
+  return odbcError.value();
+};
</code_context>

<issue_to_address>
**issue:** getError() will throw if odbcError is not set.

Document or enforce that getError() should only be called when hasError() returns true to prevent exceptions.
</issue_to_address>

### Comment 3
<location> `src/trinoAPIWrapper/connectionConfig.cpp:66-68` </location>
<code_context>
+
   this->curl           = nullptr;

+  if (hostname.empty()) {
+    throw std::invalid_argument("hostname");
+  }
+
</code_context>

<issue_to_address>
**suggestion:** Throwing std::invalid_argument for empty hostname is appropriate, but consider including more context in the error message.

Adding the invalid hostname value to the exception message will make debugging easier.

```suggestion
  if (hostname.empty()) {
    throw std::invalid_argument("Invalid hostname: value is empty");
  }
```
</issue_to_address>

### Comment 4
<location> `src/trinoAPIWrapper/authProvider/deviceFlowAuthProvider.cpp:104-105` </location>
<code_context>
+
+    std::string command = "start " + verificationUriComplete;
+
+    int result = std::system(command.c_str());
+    if (result != 0) {
+      WriteLog(LL_ERROR, "  Failed to open the browser. Command: " + command);
+    }
</code_context>

<issue_to_address>
**issue:** Using std::system to launch a browser is platform-specific and may fail silently.

Please use a cross-platform approach for opening URLs, or clearly document the Windows-only support. Also, review the security risks of using std::system.
</issue_to_address>

### Comment 5
<location> `src/trinoAPIWrapper/authProvider/deviceFlowAuthProvider.cpp:157-160` </location>
<code_context>
+        } else if (error == "slow_down") {
+          WriteLog(LL_DEBUG,
+                   "  Server requested slower polling, increasing interval...");
+          interval += 5; // Adjust interval as per server's suggestion
+        } else {
+          WriteLog(LL_ERROR, "  Polling failed with error: " + error);
</code_context>

<issue_to_address>
**suggestion:** Increasing polling interval by a fixed amount may not match server expectations.

Check if the response includes an 'interval' value and use it when available.

```suggestion
        } else if (error == "slow_down") {
          WriteLog(LL_DEBUG,
                   "  Server requested slower polling.");
          if (pollingResponse.contains("interval")) {
            int newInterval = pollingResponse["interval"];
            WriteLog(LL_DEBUG, "  Server suggested interval: " + std::to_string(newInterval));
            interval = newInterval;
          } else {
            WriteLog(LL_DEBUG, "  No interval provided by server, increasing by 5 seconds.");
            interval += 5; // Fallback if server does not provide interval
          }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +197 to +198
if (res != CURLE_OK) {
WriteLog(LL_ERROR, std::string("CURL error: ") + curl_easy_strerror(res));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider propagating CURL errors to the caller, not just logging.

If the caller needs to handle network errors, return an error code or set an error state instead of only logging.

Suggested implementation:

  CURLcode res = curl_easy_perform(curl);

  if (res != CURLE_OK) {
    WriteLog(LL_ERROR, std::string("CURL error: ") + curl_easy_strerror(res));
    return res; // Propagate error to caller
  }

You will need to ensure that the containing function's return type is CURLcode (or some error-propagating type), and that callers handle the returned error code appropriately. If the function currently returns void, change its signature to CURLcode. If you prefer to use exceptions, you could throw a custom exception instead of returning.

Comment on lines +66 to +68
if (hostname.empty()) {
throw std::invalid_argument("hostname");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Throwing std::invalid_argument for empty hostname is appropriate, but consider including more context in the error message.

Adding the invalid hostname value to the exception message will make debugging easier.

Suggested change
if (hostname.empty()) {
throw std::invalid_argument("hostname");
}
if (hostname.empty()) {
throw std::invalid_argument("Invalid hostname: value is empty");
}

Comment on lines +104 to +105
int result = std::system(command.c_str());
if (result != 0) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Using std::system to launch a browser is platform-specific and may fail silently.

Please use a cross-platform approach for opening URLs, or clearly document the Windows-only support. Also, review the security risks of using std::system.

Comment on lines +157 to +160
} else if (error == "slow_down") {
WriteLog(LL_DEBUG,
" Server requested slower polling, increasing interval...");
interval += 5; // Adjust interval as per server's suggestion
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Increasing polling interval by a fixed amount may not match server expectations.

Check if the response includes an 'interval' value and use it when available.

Suggested change
} else if (error == "slow_down") {
WriteLog(LL_DEBUG,
" Server requested slower polling, increasing interval...");
interval += 5; // Adjust interval as per server's suggestion
} else if (error == "slow_down") {
WriteLog(LL_DEBUG,
" Server requested slower polling.");
if (pollingResponse.contains("interval")) {
int newInterval = pollingResponse["interval"];
WriteLog(LL_DEBUG, " Server suggested interval: " + std::to_string(newInterval));
interval = newInterval;
} else {
WriteLog(LL_DEBUG, " No interval provided by server, increasing by 5 seconds.");
interval += 5; // Fallback if server does not provide interval
}

  * Initial checkin of device flow (aka device grant)
  * Better error reporting back to ODBC interface
  * Powershell installer for development ODBC driver.
@dprophet
Copy link
Contributor Author

I am closing this PR as I want to do it from a feature branch under our org, not from the main branch

@dprophet dprophet closed this Oct 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant