diff --git a/csharp/src/Hive2/HiveServer2Connection.cs b/csharp/src/Hive2/HiveServer2Connection.cs index e2f503c..1fc3d28 100644 --- a/csharp/src/Hive2/HiveServer2Connection.cs +++ b/csharp/src/Hive2/HiveServer2Connection.cs @@ -841,7 +841,7 @@ protected internal string GetProductVersion() return fileVersionInfo.ProductVersion ?? GetProductVersionDefault(); } - protected static Uri GetBaseAddress(string? uri, string? hostName, string? path, string? port, string hostOptionName, bool isTlsEnabled) + protected internal static Uri GetBaseAddress(string? uri, string? hostName, string? path, string? port, string hostOptionName, bool isTlsEnabled) { // Uri property takes precedent. if (!string.IsNullOrWhiteSpace(uri)) @@ -874,7 +874,16 @@ protected static Uri GetBaseAddress(string? uri, string? hostName, string? path, else throw new ArgumentOutOfRangeException(nameof(port), portNumber, $"Port number is not in a valid range."); - Uri baseAddress = new UriBuilder(uriScheme, hostName, uriPort, path).Uri; + // UriBuilder percent-encodes '?' as '%3F' when a query string is embedded + // in the path argument (e.g. /sql/1.0/warehouses/?o=). + // Use the overload that takes extraValue so the query is passed correctly. + int queryIndex = path?.IndexOf('?') ?? -1; + Uri baseAddress = new UriBuilder( + uriScheme, + hostName, + uriPort, + queryIndex >= 0 ? path!.Substring(0, queryIndex) : path, + queryIndex >= 0 ? path!.Substring(queryIndex) : string.Empty).Uri; return baseAddress; } diff --git a/csharp/test/Hive2/HiveServer2ConnectionGetBaseAddressTest.cs b/csharp/test/Hive2/HiveServer2ConnectionGetBaseAddressTest.cs new file mode 100644 index 0000000..29eb8c2 --- /dev/null +++ b/csharp/test/Hive2/HiveServer2ConnectionGetBaseAddressTest.cs @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 ADBC Drivers Contributors + * + * Licensed 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 AdbcDrivers.HiveServer2.Hive2; +using AdbcDrivers.HiveServer2.Spark; +using Xunit; + +namespace AdbcDrivers.Tests.HiveServer2.Hive2 +{ + /// + /// Unit tests for HiveServer2Connection.GetBaseAddress, focusing on correct + /// handling of HTTP paths that contain query parameters (e.g. ?o=orgId). + /// + public class HiveServer2ConnectionGetBaseAddressTest + { + /// + /// Verifies that a path containing a query parameter (e.g. Databricks warehouse + /// paths with ?o=orgId) is not percent-encoded. The '?' must remain a literal + /// query separator and not become %3F in the resulting URI. + /// + [Fact] + public void GetBaseAddress_PathWithQueryParam_DoesNotPercentEncodeQuestionMark() + { + Uri result = InvokeGetBaseAddress( + uri: null, + hostName: "test.server.com", + path: "/sql/1.0/warehouses/abc123?o=999888777", + port: "443", + isTlsEnabled: true); + + Assert.DoesNotContain("%3F", result.ToString()); + Assert.Equal("/sql/1.0/warehouses/abc123", result.AbsolutePath); + Assert.Equal("o=999888777", result.Query.TrimStart('?')); + Assert.Equal("https://test.server.com/sql/1.0/warehouses/abc123?o=999888777", result.ToString()); + } + + /// + /// Verifies that a plain path without a query parameter is unchanged. + /// + [Fact] + public void GetBaseAddress_PathWithoutQueryParam_ReturnsExpectedUri() + { + Uri result = InvokeGetBaseAddress( + uri: null, + hostName: "test.server.com", + path: "/sql/1.0/warehouses/abc123", + port: "443", + isTlsEnabled: true); + + Assert.Equal("/sql/1.0/warehouses/abc123", result.AbsolutePath); + Assert.Equal(string.Empty, result.Query); + Assert.Equal("https://test.server.com/sql/1.0/warehouses/abc123", result.ToString()); + } + + /// + /// Verifies that when a full URI is provided it is returned as-is, bypassing + /// the UriBuilder path entirely. + /// + [Fact] + public void GetBaseAddress_FullUri_ReturnsUriDirectly() + { + string fullUri = "https://test.server.com/sql/1.0/warehouses/abc123"; + + Uri result = InvokeGetBaseAddress( + uri: fullUri, + hostName: null, + path: null, + port: null, + isTlsEnabled: true); + + Assert.Equal(new Uri(fullUri), result); + } + + private static Uri InvokeGetBaseAddress(string? uri, string? hostName, string? path, string? port, bool isTlsEnabled) + { + return HiveServer2Connection.GetBaseAddress(uri, hostName, path, port, SparkParameters.HostName, isTlsEnabled); + } + } +}