blob: d6480d214f42b73bfcada2ed439eea65a1447756 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Apache.Arrow.Adbc.Drivers.Apache;
using Apache.Arrow.Adbc.Drivers.Apache.Hive2;
using Apache.Arrow.Adbc.Drivers.Apache.Hive2.Client;
using Apache.Arrow.Adbc.Drivers.Apache.Spark;
using Apache.Arrow.Adbc.Drivers.Databricks.Auth;
using Apache.Arrow.Adbc.Drivers.Databricks.Reader;
using Apache.Arrow.Ipc;
using Apache.Hive.Service.Rpc.Thrift;
using Thrift.Protocol;
namespace Apache.Arrow.Adbc.Drivers.Databricks
{
internal class DatabricksConnection : SparkHttpConnection
{
internal static new readonly string s_assemblyName = ApacheUtility.GetAssemblyName(typeof(DatabricksConnection));
internal static new readonly string s_assemblyVersion = ApacheUtility.GetAssemblyVersion(typeof(DatabricksConnection));
/// <summary>
/// The environment variable name that contains the path to the default Databricks configuration file.
/// </summary>
public const string DefaultConfigEnvironmentVariable = "DATABRICKS_CONFIG_FILE";
public const string DefaultInitialSchema = "default";
internal static readonly Dictionary<string, string> timestampConfig = new Dictionary<string, string>
{
{ "spark.thriftserver.arrowBasedRowSet.timestampAsString", "false" },
};
private bool _applySSPWithQueries = false;
private bool _enableDirectResults = true;
private bool _enableMultipleCatalogSupport = true;
private bool _enablePKFK = true;
private bool _runAsyncInThrift = true;
// DirectQuery configuration
private const long DefaultDirectResultMaxBytes = 10 * 1024 * 1024; // 10MB for direct query results size limit
private const long DefaultDirectResultMaxRows = 500 * 1000; // upper limit for 10MB result assume smallest 20 Byte column
private long _directResultMaxBytes = DefaultDirectResultMaxBytes;
private long _directResultMaxRows = DefaultDirectResultMaxRows;
// CloudFetch configuration
private const long DefaultMaxBytesPerFile = 20 * 1024 * 1024; // 20MB
private const int DefaultQueryTimeSeconds = 3 * 60 * 60; // 3 hours
private bool _useCloudFetch = true;
private bool _canDecompressLz4 = true;
private long _maxBytesPerFile = DefaultMaxBytesPerFile;
private const long DefaultMaxBytesPerFetchRequest = 400 * 1024 * 1024; // 400MB
private long _maxBytesPerFetchRequest = DefaultMaxBytesPerFetchRequest;
private const bool DefaultRetryOnUnavailable = true;
private const int DefaultTemporarilyUnavailableRetryTimeout = 900;
private bool _useDescTableExtended = false;
// Trace propagation configuration
private bool _tracePropagationEnabled = true;
private string _traceParentHeaderName = "traceparent";
private bool _traceStateEnabled = false;
// Identity federation client ID for token exchange
private string? _identityFederationClientId;
// Heartbeat interval configuration
private int _fetchHeartbeatIntervalSeconds = DatabricksConstants.DefaultOperationStatusPollingIntervalSeconds;
// Request timeout configuration
private int _operationStatusRequestTimeoutSeconds = DatabricksConstants.DefaultOperationStatusRequestTimeoutSeconds;
// Default namespace
private TNamespace? _defaultNamespace;
private HttpClient? _authHttpClient;
public DatabricksConnection(IReadOnlyDictionary<string, string> properties) : base(MergeWithDefaultEnvironmentConfig(properties))
{
ValidateProperties();
}
public override IEnumerable<KeyValuePair<string, object?>>? GetActivitySourceTags(IReadOnlyDictionary<string, string> properties)
{
IEnumerable<KeyValuePair<string, object?>>? tags = base.GetActivitySourceTags(properties);
// TODO: Add any additional tags specific to Databricks connection
//tags ??= [];
//tags.Concat([new("key", "value")]);
return tags;
}
protected override TCLIService.IAsync CreateTCLIServiceClient(TProtocol protocol)
{
return new ThreadSafeClient(new TCLIService.Client(protocol));
}
/// <summary>
/// Automatically merges properties from the default DATABRICKS_CONFIG_FILE environment variable with passed-in properties.
/// The merge priority is controlled by the "adbc.databricks.driver_config_take_precedence" property.
/// If DATABRICKS_CONFIG_FILE is not set or invalid, only passed-in properties are used.
/// </summary>
/// <param name="properties">Properties passed to constructor.</param>
/// <returns>Merged properties dictionary.</returns>
private static IReadOnlyDictionary<string, string> MergeWithDefaultEnvironmentConfig(IReadOnlyDictionary<string, string> properties)
{
// Try to load configuration from the default environment variable
var environmentConfig = DatabricksConfiguration.TryFromEnvironmentVariable(DefaultConfigEnvironmentVariable);
if (environmentConfig != null)
{
// Determine precedence setting - check passed-in properties first, then environment config
bool driverConfigTakesPrecedence = DetermineDriverConfigPrecedence(properties, environmentConfig.Properties);
if (driverConfigTakesPrecedence)
{
// Environment config properties override passed-in properties
return MergeProperties(properties, environmentConfig.Properties);
}
else
{
// Passed-in properties override environment config properties (default behavior)
return MergeProperties(environmentConfig.Properties, properties);
}
}
// No environment config available, use only passed-in properties
return properties;
}
/// <summary>
/// Determines whether driver configuration should take precedence based on the precedence property.
/// Checks passed-in properties first, then environment properties, defaulting to false.
/// </summary>
/// <param name="passedInProperties">Properties passed to constructor.</param>
/// <param name="environmentProperties">Properties loaded from environment configuration.</param>
/// <returns>True if driver config should take precedence, false otherwise.</returns>
private static bool DetermineDriverConfigPrecedence(IReadOnlyDictionary<string, string> passedInProperties, IReadOnlyDictionary<string, string> environmentProperties)
{
// Priority 1: Check passed-in properties for precedence setting
if (passedInProperties.TryGetValue(DatabricksParameters.DriverConfigTakePrecedence, out string? passedInValue))
{
if (bool.TryParse(passedInValue, out bool passedInPrecedence))
{
return passedInPrecedence;
}
}
// Priority 2: Check environment config for precedence setting
if (environmentProperties.TryGetValue(DatabricksParameters.DriverConfigTakePrecedence, out string? environmentValue))
{
if (bool.TryParse(environmentValue, out bool environmentPrecedence))
{
return environmentPrecedence;
}
}
// Default: Passed-in properties override environment config (current behavior)
return false;
}
/// <summary>
/// Merges two property dictionaries, with additional properties taking precedence.
/// </summary>
/// <param name="baseProperties">Base properties dictionary.</param>
/// <param name="additionalProperties">Additional properties to merge. These take precedence over base properties.</param>
/// <returns>Merged properties dictionary.</returns>
private static IReadOnlyDictionary<string, string> MergeProperties(IReadOnlyDictionary<string, string> baseProperties, IReadOnlyDictionary<string, string>? additionalProperties)
{
if (additionalProperties == null || additionalProperties.Count == 0)
{
return baseProperties;
}
var merged = new Dictionary<string, string>();
// Add base properties first
foreach (var kvp in baseProperties)
{
merged[kvp.Key] = kvp.Value;
}
// Additional properties override base properties
foreach (var kvp in additionalProperties)
{
merged[kvp.Key] = kvp.Value;
}
return merged;
}
private void ValidateProperties()
{
if (Properties.TryGetValue(DatabricksParameters.EnablePKFK, out string? enablePKFKStr))
{
if (bool.TryParse(enablePKFKStr, out bool enablePKFKValue))
{
_enablePKFK = enablePKFKValue;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.EnablePKFK}' value '{enablePKFKStr}' could not be parsed. Valid values are 'true', 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.EnableMultipleCatalogSupport, out string? enableMultipleCatalogSupportStr))
{
if (bool.TryParse(enableMultipleCatalogSupportStr, out bool enableMultipleCatalogSupportValue))
{
_enableMultipleCatalogSupport = enableMultipleCatalogSupportValue;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.EnableMultipleCatalogSupport}' value '{enableMultipleCatalogSupportStr}' could not be parsed. Valid values are 'true', 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.ApplySSPWithQueries, out string? applySSPWithQueriesStr))
{
if (bool.TryParse(applySSPWithQueriesStr, out bool applySSPWithQueriesValue))
{
_applySSPWithQueries = applySSPWithQueriesValue;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.ApplySSPWithQueries}' value '{applySSPWithQueriesStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.EnableDirectResults, out string? enableDirectResultsStr))
{
if (bool.TryParse(enableDirectResultsStr, out bool enableDirectResultsValue))
{
_enableDirectResults = enableDirectResultsValue;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.EnableDirectResults}' value '{enableDirectResultsStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
// Parse CloudFetch options from connection properties
if (Properties.TryGetValue(DatabricksParameters.UseCloudFetch, out string? useCloudFetchStr))
{
if (bool.TryParse(useCloudFetchStr, out bool useCloudFetchValue))
{
_useCloudFetch = useCloudFetchValue;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.UseCloudFetch}' value '{useCloudFetchStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.CanDecompressLz4, out string? canDecompressLz4Str))
{
if (bool.TryParse(canDecompressLz4Str, out bool canDecompressLz4Value))
{
_canDecompressLz4 = canDecompressLz4Value;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.CanDecompressLz4}' value '{canDecompressLz4Str}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.UseDescTableExtended, out string? useDescTableExtendedStr))
{
if (bool.TryParse(useDescTableExtendedStr, out bool useDescTableExtended))
{
_useDescTableExtended = useDescTableExtended;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.UseDescTableExtended}' value '{useDescTableExtendedStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.EnableRunAsyncInThriftOp, out string? enableRunAsyncInThriftStr))
{
if (bool.TryParse(enableRunAsyncInThriftStr, out bool enableRunAsyncInThrift))
{
_runAsyncInThrift = enableRunAsyncInThrift;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.EnableRunAsyncInThriftOp}' value '{enableRunAsyncInThriftStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.MaxBytesPerFile, out string? maxBytesPerFileStr))
{
if (!long.TryParse(maxBytesPerFileStr, out long maxBytesPerFileValue))
{
throw new ArgumentException($"Parameter '{DatabricksParameters.MaxBytesPerFile}' value '{maxBytesPerFileStr}' could not be parsed. Valid values are positive integers.");
}
if (maxBytesPerFileValue <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(Properties),
maxBytesPerFileValue,
$"Parameter '{DatabricksParameters.MaxBytesPerFile}' value must be a positive integer.");
}
_maxBytesPerFile = maxBytesPerFileValue;
}
if (Properties.TryGetValue(DatabricksParameters.MaxBytesPerFetchRequest, out string? maxBytesPerFetchRequestStr))
{
try
{
long maxBytesPerFetchRequestValue = ParseBytesWithUnits(maxBytesPerFetchRequestStr);
if (maxBytesPerFetchRequestValue < 0)
{
throw new ArgumentOutOfRangeException(
nameof(Properties),
maxBytesPerFetchRequestValue,
$"Parameter '{DatabricksParameters.MaxBytesPerFetchRequest}' value must be a non-negative integer. Use 0 for no limit.");
}
_maxBytesPerFetchRequest = maxBytesPerFetchRequestValue;
}
catch (FormatException)
{
throw new ArgumentException($"Parameter '{DatabricksParameters.MaxBytesPerFetchRequest}' value '{maxBytesPerFetchRequestStr}' could not be parsed. Valid formats: number with optional unit suffix (B, KB, MB, GB). Examples: '400MB', '1024KB', '1073741824'.");
}
}
// Parse default namespace
string? defaultCatalog = null;
string? defaultSchema = null;
// only if enableMultipleCatalogSupport is true, do we supply catalog from connection properties
if (_enableMultipleCatalogSupport)
{
Properties.TryGetValue(AdbcOptions.Connection.CurrentCatalog, out defaultCatalog);
}
Properties.TryGetValue(AdbcOptions.Connection.CurrentDbSchema, out defaultSchema);
// This maintains backward compatibility with older workspaces, where the Hive metastore was accessed via the spark catalog name.
// In newer DBR versions with Unity Catalog, the default catalog is typically hive_metastore.
// Passing null here allows the runtime to fall back to the workspace-defined default catalog for the session.
defaultCatalog = HandleSparkCatalog(defaultCatalog);
var ns = new TNamespace();
ns.SchemaName = string.IsNullOrWhiteSpace(defaultSchema) ? DefaultInitialSchema : defaultSchema;
if (!string.IsNullOrWhiteSpace(defaultCatalog))
ns.CatalogName = defaultCatalog!;
_defaultNamespace = ns;
// Parse trace propagation options
if (Properties.TryGetValue(DatabricksParameters.TracePropagationEnabled, out string? tracePropagationEnabledStr))
{
if (bool.TryParse(tracePropagationEnabledStr, out bool tracePropagationEnabled))
{
_tracePropagationEnabled = tracePropagationEnabled;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.TracePropagationEnabled}' value '{tracePropagationEnabledStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (Properties.TryGetValue(DatabricksParameters.TraceParentHeaderName, out string? traceParentHeaderName))
{
if (!string.IsNullOrWhiteSpace(traceParentHeaderName))
{
_traceParentHeaderName = traceParentHeaderName;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.TraceParentHeaderName}' cannot be empty.");
}
}
if (Properties.TryGetValue(DatabricksParameters.TraceStateEnabled, out string? traceStateEnabledStr))
{
if (bool.TryParse(traceStateEnabledStr, out bool traceStateEnabled))
{
_traceStateEnabled = traceStateEnabled;
}
else
{
throw new ArgumentException($"Parameter '{DatabricksParameters.TraceStateEnabled}' value '{traceStateEnabledStr}' could not be parsed. Valid values are 'true' and 'false'.");
}
}
if (!Properties.ContainsKey(ApacheParameters.QueryTimeoutSeconds))
{
// Default QueryTimeSeconds in Hive2Connection is only 60s, which is too small for lots of long running query
QueryTimeoutSeconds = DefaultQueryTimeSeconds;
}
if (Properties.TryGetValue(DatabricksParameters.IdentityFederationClientId, out string? identityFederationClientId))
{
_identityFederationClientId = identityFederationClientId;
}
if (Properties.TryGetValue(DatabricksParameters.FetchHeartbeatInterval, out string? fetchHeartbeatIntervalStr))
{
if (!int.TryParse(fetchHeartbeatIntervalStr, out int fetchHeartbeatIntervalValue))
{
throw new ArgumentException($"Parameter '{DatabricksParameters.FetchHeartbeatInterval}' value '{fetchHeartbeatIntervalStr}' could not be parsed. Valid values are positive integers.");
}
if (fetchHeartbeatIntervalValue <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(Properties),
fetchHeartbeatIntervalValue,
$"Parameter '{DatabricksParameters.FetchHeartbeatInterval}' value must be a positive integer.");
}
_fetchHeartbeatIntervalSeconds = fetchHeartbeatIntervalValue;
}
if (Properties.TryGetValue(DatabricksParameters.OperationStatusRequestTimeout, out string? operationStatusRequestTimeoutStr))
{
if (!int.TryParse(operationStatusRequestTimeoutStr, out int operationStatusRequestTimeoutValue))
{
throw new ArgumentException($"Parameter '{DatabricksParameters.OperationStatusRequestTimeout}' value '{operationStatusRequestTimeoutStr}' could not be parsed. Valid values are positive integers.");
}
if (operationStatusRequestTimeoutValue <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(Properties),
operationStatusRequestTimeoutValue,
$"Parameter '{DatabricksParameters.OperationStatusRequestTimeout}' value must be a positive integer.");
}
_operationStatusRequestTimeoutSeconds = operationStatusRequestTimeoutValue;
}
}
/// <summary>
/// Gets whether server side properties should be applied using queries.
/// </summary>
internal bool ApplySSPWithQueries => _applySSPWithQueries;
/// <summary>
/// Gets whether direct results are enabled.
/// </summary>
internal bool EnableDirectResults => _enableDirectResults;
/// <inheritdoc/>
protected internal override bool TrySetGetDirectResults(IRequest request)
{
if (EnableDirectResults)
{
request.GetDirectResults = new()
{
MaxRows = _directResultMaxRows,
MaxBytes = _directResultMaxBytes
};
return true;
}
return false;
}
/// <summary>
/// Gets the maximum bytes per fetch block for directResult
/// </summary>
internal long DirectResultMaxBytes => _directResultMaxBytes;
/// <summary>
/// Gets the maximum rows per fetch block for directResult
/// </summary>
internal long DirectResultMaxRows => _directResultMaxRows;
/// <summary>
/// Gets whether CloudFetch is enabled.
/// </summary>
internal bool UseCloudFetch => _useCloudFetch;
/// <summary>
/// Gets whether LZ4 decompression is enabled.
/// </summary>
internal bool CanDecompressLz4 => _canDecompressLz4;
/// <summary>
/// Gets the maximum bytes per file for CloudFetch.
/// </summary>
internal long MaxBytesPerFile => _maxBytesPerFile;
/// <summary>
/// Gets the maximum bytes per fetch request.
/// </summary>
internal long MaxBytesPerFetchRequest => _maxBytesPerFetchRequest;
/// <summary>
/// Gets the default namespace to use for SQL queries.
/// </summary>
internal TNamespace? DefaultNamespace => _defaultNamespace;
/// <summary>
/// Gets the heartbeat interval in seconds for long-running operations.
/// </summary>
internal int FetchHeartbeatIntervalSeconds => _fetchHeartbeatIntervalSeconds;
/// <summary>
/// Gets the request timeout in seconds for operation status polling requests.
/// </summary>
internal int OperationStatusRequestTimeoutSeconds => _operationStatusRequestTimeoutSeconds;
/// <summary>
/// Gets whether multiple catalog is supported
/// </summary>
internal bool EnableMultipleCatalogSupport => _enableMultipleCatalogSupport;
/// <summary>
/// Check if current connection can use `DESC TABLE EXTENDED` query
/// </summary>
internal bool CanUseDescTableExtended => _useDescTableExtended && ServerProtocolVersion != null && FeatureVersionNegotiator.SupportsDESCTableExtended(ServerProtocolVersion.Value);
/// <summary>
/// Gets whether PK/FK metadata call is enabled
/// </summary>
public bool EnablePKFK => _enablePKFK;
/// <summary>
/// Enable RunAsync flag in Thrift Operation
/// </summary>
public bool RunAsyncInThrift => _runAsyncInThrift;
/// <summary>
/// Gets a value indicating whether to retry requests that receive a 503 response with a Retry-After header.
/// </summary>
protected bool TemporarilyUnavailableRetry { get; private set; } = DefaultRetryOnUnavailable;
/// <summary>
/// Gets the maximum total time in seconds to retry 503 responses before failing.
/// </summary>
protected int TemporarilyUnavailableRetryTimeout { get; private set; } = DefaultTemporarilyUnavailableRetryTimeout;
protected override HttpMessageHandler CreateHttpHandler()
{
HttpMessageHandler baseHandler = base.CreateHttpHandler();
HttpMessageHandler baseAuthHandler = HiveServer2TlsImpl.NewHttpClientHandler(TlsOptions, _proxyConfigurator);
// IMPORTANT: Handler Order Matters!
//
// HTTP delegating handlers form a chain where execution flows from outermost to innermost
// on the request, and then innermost to outermost on the response.
//
// Request flow (outer → inner): Handler1 → Handler2 → Handler3 → Network
// Response flow (inner → outer): Network → Handler3 → Handler2 → Handler1
//
// Current chain order (outermost to innermost):
// 1. OAuth handlers (OAuthDelegatingHandler, etc.) - only on baseHandler for API requests
// 2. ThriftErrorMessageHandler - extracts x-thriftserver-error-message and throws descriptive exceptions
// 3. RetryHttpHandler - retries 408, 502, 503, 504 with Retry-After support
// 4. TracingDelegatingHandler - propagates W3C trace context
// 5. Base HTTP handler - actual network communication
//
// Why this order:
// - TracingDelegatingHandler must be innermost (closest to network) to capture full request timing
// - RetryHttpHandler must be INSIDE ThriftErrorMessageHandler so it can retry 503 responses
// (e.g., during cluster auto-start) before ThriftErrorMessageHandler throws an exception
// - ThriftErrorMessageHandler must be OUTSIDE RetryHttpHandler so it only processes final
// error responses after all retry attempts are exhausted
// - OAuth handlers are outermost since they modify request headers and don't need retry logic
//
// DO NOT change this order without understanding the implications!
// Add tracing handler to propagate W3C trace context if enabled (INNERMOST - closest to network)
if (_tracePropagationEnabled)
{
baseHandler = new TracingDelegatingHandler(baseHandler, this, _traceParentHeaderName, _traceStateEnabled);
baseAuthHandler = new TracingDelegatingHandler(baseAuthHandler, this, _traceParentHeaderName, _traceStateEnabled);
}
if (TemporarilyUnavailableRetry)
{
// Add retry handler for 408, 502, 503, 504 responses with Retry-After support
// This must be INSIDE ThriftErrorMessageHandler so retries happen before exceptions are thrown
baseHandler = new RetryHttpHandler(baseHandler, TemporarilyUnavailableRetryTimeout);
baseAuthHandler = new RetryHttpHandler(baseAuthHandler, TemporarilyUnavailableRetryTimeout);
}
// Add Thrift error message handler AFTER retry handler (OUTSIDE in the chain)
// This ensures retryable status codes (408, 502, 503, 504) are retried by RetryHttpHandler
// before ThriftErrorMessageHandler throws exceptions with Thrift error messages
baseHandler = new ThriftErrorMessageHandler(baseHandler);
baseAuthHandler = new ThriftErrorMessageHandler(baseAuthHandler);
if (Properties.TryGetValue(SparkParameters.AuthType, out string? authType) &&
SparkAuthTypeParser.TryParse(authType, out SparkAuthType authTypeValue) &&
authTypeValue == SparkAuthType.OAuth)
{
Debug.Assert(_authHttpClient == null, "Auth HttpClient should not be initialized yet.");
_authHttpClient = new HttpClient(baseAuthHandler);
string host = GetHost();
ITokenExchangeClient tokenExchangeClient = new TokenExchangeClient(_authHttpClient, host);
// Mandatory token exchange should be the inner handler so that it happens
// AFTER the OAuth handlers (e.g. after M2M sets the access token)
baseHandler = new MandatoryTokenExchangeDelegatingHandler(
baseHandler,
tokenExchangeClient,
_identityFederationClientId);
// Add OAuth client credentials handler if OAuth M2M authentication is being used
if (Properties.TryGetValue(DatabricksParameters.OAuthGrantType, out string? grantTypeStr) &&
DatabricksOAuthGrantTypeParser.TryParse(grantTypeStr, out DatabricksOAuthGrantType grantType) &&
grantType == DatabricksOAuthGrantType.ClientCredentials)
{
Properties.TryGetValue(DatabricksParameters.OAuthClientId, out string? clientId);
Properties.TryGetValue(DatabricksParameters.OAuthClientSecret, out string? clientSecret);
Properties.TryGetValue(DatabricksParameters.OAuthScope, out string? scope);
var tokenProvider = new OAuthClientCredentialsProvider(
_authHttpClient,
clientId!,
clientSecret!,
host!,
scope: scope ?? "sql",
timeoutMinutes: 1
);
baseHandler = new OAuthDelegatingHandler(baseHandler, tokenProvider);
}
// Add token renewal handler for OAuth access token
else if (Properties.TryGetValue(DatabricksParameters.TokenRenewLimit, out string? tokenRenewLimitStr) &&
int.TryParse(tokenRenewLimitStr, out int tokenRenewLimit) &&
tokenRenewLimit > 0 &&
Properties.TryGetValue(SparkParameters.AccessToken, out string? accessToken))
{
if (string.IsNullOrEmpty(accessToken))
{
throw new ArgumentException("Access token is required for OAuth authentication with token renewal.");
}
// Check if token is a JWT token by trying to decode it
if (JwtTokenDecoder.TryGetExpirationTime(accessToken, out DateTime expiryTime))
{
baseHandler = new TokenRefreshDelegatingHandler(
baseHandler,
tokenExchangeClient,
accessToken,
expiryTime,
tokenRenewLimit);
}
}
}
return baseHandler;
}
protected override bool GetObjectsPatternsRequireLowerCase => true;
internal override IArrowArrayStream NewReader<T>(T statement, Schema schema, IResponse response, TGetResultSetMetadataResp? metadataResp = null)
{
bool isLz4Compressed = false;
DatabricksStatement? databricksStatement = statement as DatabricksStatement;
if (databricksStatement == null)
{
throw new InvalidOperationException("Cannot obtain a reader for Databricks");
}
if (metadataResp != null && metadataResp.__isset.lz4Compressed)
{
isLz4Compressed = metadataResp.Lz4Compressed;
}
HttpClient httpClient = new HttpClient(HiveServer2TlsImpl.NewHttpClientHandler(TlsOptions, _proxyConfigurator));
return new DatabricksCompositeReader(databricksStatement, schema, response, isLz4Compressed, httpClient);
}
internal override SchemaParser SchemaParser => new DatabricksSchemaParser();
public override AdbcStatement CreateStatement()
{
DatabricksStatement statement = new DatabricksStatement(this);
return statement;
}
protected override TOpenSessionReq CreateSessionRequest()
{
var req = new TOpenSessionReq
{
Client_protocol = TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V7,
Client_protocol_i64 = (long)TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V7,
CanUseMultipleCatalogs = _enableMultipleCatalogSupport,
};
// Set default namespace if available
if (_defaultNamespace != null)
{
req.InitialNamespace = _defaultNamespace;
}
req.Configuration = new Dictionary<string, string>();
// merge timestampConfig with serverSideProperties
foreach (var kvp in timestampConfig)
{
req.Configuration[kvp.Key] = kvp.Value;
}
// If not using queries to set server-side properties, include them in Configuration
if (!_applySSPWithQueries)
{
var serverSideProperties = GetServerSideProperties();
foreach (var property in serverSideProperties)
{
req.Configuration[property.Key] = property.Value;
}
}
return req;
}
protected override async Task HandleOpenSessionResponse(TOpenSessionResp? session, Activity? activity = default)
{
await base.HandleOpenSessionResponse(session, activity);
if (session != null)
{
var version = session.ServerProtocolVersion;
if (!FeatureVersionNegotiator.IsDatabricksProtocolVersion(version))
{
throw new DatabricksException("Attempted to use databricks driver with a non-databricks server");
}
_enablePKFK = _enablePKFK && FeatureVersionNegotiator.SupportsPKFK(version);
_enableMultipleCatalogSupport = session.__isset.canUseMultipleCatalogs ? session.CanUseMultipleCatalogs : false;
if (session.__isset.initialNamespace)
{
_defaultNamespace = session.InitialNamespace;
}
else if (_defaultNamespace != null && !string.IsNullOrEmpty(_defaultNamespace.SchemaName))
{
// catalog in namespace is introduced when SET CATALOG is introduced, so we don't need to fallback
// server version is too old. Explicitly set the schema using queries
await SetSchema(_defaultNamespace.SchemaName);
}
}
}
// Since Databricks Namespace was introduced in newer versions, we fallback to USE SCHEMA to set default schema
// in case the server version is too low.
private async Task SetSchema(string schemaName)
{
using var statement = new DatabricksStatement(this);
statement.SqlQuery = $"USE {schemaName}";
await statement.ExecuteUpdateAsync();
}
/// <summary>
/// Gets a dictionary of server-side properties extracted from connection properties.
/// </summary>
/// <returns>Dictionary of server-side properties with prefix removed from keys.</returns>
private Dictionary<string, string> GetServerSideProperties()
{
return Properties
.Where(p => p.Key.ToLowerInvariant().StartsWith(DatabricksParameters.ServerSidePropertyPrefix))
.ToDictionary(
p => p.Key.Substring(DatabricksParameters.ServerSidePropertyPrefix.Length),
p => p.Value
);
}
/// <summary>
/// Applies server-side properties by executing "set key=value" queries.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task ApplyServerSidePropertiesAsync()
{
if (!_applySSPWithQueries)
{
return;
}
var serverSideProperties = GetServerSideProperties();
if (serverSideProperties.Count == 0)
{
return;
}
using var statement = new DatabricksStatement(this);
foreach (var property in serverSideProperties)
{
if (!IsValidPropertyName(property.Key))
{
Debug.WriteLine($"Skipping invalid property name: {property.Key}");
continue;
}
string escapedValue = EscapeSqlString(property.Value);
string query = $"SET {property.Key}={escapedValue}";
statement.SqlQuery = query;
try
{
await statement.ExecuteUpdateAsync();
}
catch (Exception ex)
{
Debug.WriteLine($"Error setting server-side property '{property.Key}': {ex.Message}");
}
}
}
private bool IsValidPropertyName(string propertyName)
{
// Allow only letters and underscores in property names
return System.Text.RegularExpressions.Regex.IsMatch(
propertyName,
@"^[a-zA-Z_]+$");
}
private string EscapeSqlString(string value)
{
return "`" + value.Replace("`", "``") + "`";
}
/// <summary>
/// Parses a byte value that may include unit suffixes (B, KB, MB, GB).
/// </summary>
/// <param name="value">The value to parse, e.g., "400MB", "1024KB", "1073741824"</param>
/// <returns>The value in bytes</returns>
/// <exception cref="FormatException">Thrown when the value cannot be parsed</exception>
internal static long ParseBytesWithUnits(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new FormatException("Value cannot be null or empty");
}
value = value.Trim().ToUpperInvariant();
// Check for unit suffixes
long multiplier = 1;
string numberPart = value;
if (value.EndsWith("GB"))
{
multiplier = 1024L * 1024L * 1024L;
numberPart = value.Substring(0, value.Length - 2);
}
else if (value.EndsWith("MB"))
{
multiplier = 1024L * 1024L;
numberPart = value.Substring(0, value.Length - 2);
}
else if (value.EndsWith("KB"))
{
multiplier = 1024L;
numberPart = value.Substring(0, value.Length - 2);
}
else if (value.EndsWith("B"))
{
multiplier = 1L;
numberPart = value.Substring(0, value.Length - 1);
}
if (!long.TryParse(numberPart.Trim(), out long number))
{
throw new FormatException($"Invalid number format: {numberPart}");
}
try
{
return checked(number * multiplier);
}
catch (OverflowException)
{
throw new FormatException($"Value {value} results in overflow when converted to bytes");
}
}
protected override void ValidateOptions()
{
base.ValidateOptions();
if (Properties.TryGetValue(DatabricksParameters.TemporarilyUnavailableRetry, out string? tempUnavailableRetryStr))
{
if (!bool.TryParse(tempUnavailableRetryStr, out bool tempUnavailableRetryValue))
{
throw new ArgumentOutOfRangeException(DatabricksParameters.TemporarilyUnavailableRetry, tempUnavailableRetryStr,
$"must be a value of false (disabled) or true (enabled). Default is true.");
}
TemporarilyUnavailableRetry = tempUnavailableRetryValue;
}
if (Properties.TryGetValue(DatabricksParameters.TemporarilyUnavailableRetryTimeout, out string? tempUnavailableRetryTimeoutStr))
{
if (!int.TryParse(tempUnavailableRetryTimeoutStr, out int tempUnavailableRetryTimeoutValue) ||
tempUnavailableRetryTimeoutValue < 0)
{
throw new ArgumentOutOfRangeException(DatabricksParameters.TemporarilyUnavailableRetryTimeout, tempUnavailableRetryTimeoutStr,
$"must be a value of 0 (retry indefinitely) or a positive integer representing seconds. Default is 900 seconds (15 minutes).");
}
TemporarilyUnavailableRetryTimeout = tempUnavailableRetryTimeoutValue;
}
// When TemporarilyUnavailableRetry is enabled, we need to make sure connection timeout (which is used to cancel the HttpConnection) is equal
// or greater than TemporarilyUnavailableRetryTimeout so that it won't timeout before server startup timeout (TemporarilyUnavailableRetryTimeout)
if (TemporarilyUnavailableRetry && TemporarilyUnavailableRetryTimeout * 1000 > ConnectTimeoutMilliseconds)
{
ConnectTimeoutMilliseconds = TemporarilyUnavailableRetryTimeout * 1000;
}
}
protected override Task<TGetResultSetMetadataResp> GetResultSetMetadataAsync(IResponse response, CancellationToken cancellationToken = default) =>
Task.FromResult(response.DirectResults!.ResultSetMetadata);
protected override Task<TRowSet> GetRowSetAsync(IResponse response, CancellationToken cancellationToken = default) =>
Task.FromResult(response.DirectResults!.ResultSet.Results);
protected override AuthenticationHeaderValue? GetAuthenticationHeaderValue(SparkAuthType authType)
{
if (authType == SparkAuthType.OAuth)
{
Properties.TryGetValue(DatabricksParameters.OAuthGrantType, out string? grantTypeStr);
if (DatabricksOAuthGrantTypeParser.TryParse(grantTypeStr, out DatabricksOAuthGrantType grantType) &&
grantType == DatabricksOAuthGrantType.ClientCredentials)
{
// Return null for client credentials flow since OAuth handler will handle authentication
return null;
}
}
return base.GetAuthenticationHeaderValue(authType);
}
protected override void ValidateOAuthParameters()
{
Properties.TryGetValue(DatabricksParameters.OAuthGrantType, out string? grantTypeStr);
DatabricksOAuthGrantType grantType;
if (!DatabricksOAuthGrantTypeParser.TryParse(grantTypeStr, out grantType))
{
throw new ArgumentOutOfRangeException(
DatabricksParameters.OAuthGrantType,
grantTypeStr,
$"Unsupported {DatabricksParameters.OAuthGrantType} value. Refer to the Databricks documentation for valid values."
);
}
// If we have a valid grant type, validate the required parameters
if (grantType == DatabricksOAuthGrantType.ClientCredentials)
{
Properties.TryGetValue(DatabricksParameters.OAuthClientId, out string? clientId);
Properties.TryGetValue(DatabricksParameters.OAuthClientSecret, out string? clientSecret);
if (string.IsNullOrEmpty(clientId))
{
throw new ArgumentException(
$"Parameter '{DatabricksParameters.OAuthGrantType}' is set to '{DatabricksConstants.OAuthGrantTypes.ClientCredentials}' but parameter '{DatabricksParameters.OAuthClientId}' is not set. Please provide a value for '{DatabricksParameters.OAuthClientId}'.",
nameof(Properties));
}
if (string.IsNullOrEmpty(clientSecret))
{
throw new ArgumentException(
$"Parameter '{DatabricksParameters.OAuthGrantType}' is set to '{DatabricksConstants.OAuthGrantTypes.ClientCredentials}' but parameter '{DatabricksParameters.OAuthClientSecret}' is not set. Please provide a value for '{DatabricksParameters.OAuthClientSecret}'.",
nameof(Properties));
}
}
else
{
// For other auth flows, use default OAuth validation
base.ValidateOAuthParameters();
}
}
/// <summary>
/// Gets the host from the connection properties.
/// </summary>
/// <returns>The host, or empty string if not found.</returns>
private string GetHost()
{
if (Properties.TryGetValue(SparkParameters.HostName, out string? host) && !string.IsNullOrEmpty(host))
{
return host;
}
if (Properties.TryGetValue(AdbcOptions.Uri, out string? uri) && !string.IsNullOrEmpty(uri))
{
// Parse the URI to extract the host
if (Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsedUri))
{
return parsedUri.Host;
}
}
throw new ArgumentException("Host not found in connection properties. Please provide a valid host using either 'HostName' or 'Uri' property.");
}
public override string AssemblyName => s_assemblyName;
public override string AssemblyVersion => s_assemblyVersion;
internal static string? HandleSparkCatalog(string? CatalogName)
{
if (CatalogName != null && CatalogName.Equals("SPARK", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return CatalogName;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_authHttpClient?.Dispose();
}
base.Dispose(disposing);
}
}
}