Skip to content
Open
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
58 changes: 48 additions & 10 deletions csharp/src/PropertyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using AdbcDrivers.HiveServer2.Spark;
using Apache.Arrow.Adbc;

Expand Down Expand Up @@ -220,10 +221,29 @@ public static long GetPositiveLongPropertyWithValidation(IReadOnlyDictionary<str
return null;
}

// Matches the workspace ID inside an all-purpose-compute HiveServer2 path of the form
// [/]sql/protocolv1/o/<workspace-id>/<cluster-id>[/...]. Workspace IDs are decimal
// integers; the regex rejects anything else so a stray segment can't be shipped as an org ID.
private static readonly Regex s_clusterPathOrgIdPattern = new(
@"(?:^|/)sql/protocolv1/o/(\d+)/[^/?]+",
RegexOptions.Compiled | RegexOptions.CultureInvariant);

/// <summary>
/// Extracts the org ID from connection properties by inspecting the http path and URI query strings.
/// Checks <see cref="SparkParameters.Path"/> first, then falls back to <see cref="AdbcOptions.Uri"/>.
/// Extracts the org ID from connection properties by inspecting the http path and URI.
/// </summary>
/// <remarks>
/// Two sources are checked, in priority order, for both
/// <see cref="SparkParameters.Path"/> and <see cref="AdbcOptions.Uri"/>:
/// <list type="number">
/// <item><c>?o=&lt;workspace-id&gt;</c> query parameter (warehouse paths on SPOG
/// typically encode the workspace this way).</item>
/// <item><c>/sql/protocolv1/o/&lt;workspace-id&gt;/&lt;cluster-id&gt;</c> path segment
/// (all-purpose-compute paths embed the workspace in the path itself).</item>
/// </list>
/// Without the path-segment fallback, non-Thrift requests (telemetry, feature flags)
/// on SPOG (custom-URL) hosts lack workspace context and PoPP redirects them to
/// <c>/login</c>, silently dropping telemetry.
/// </remarks>
/// <param name="properties">Connection properties.</param>
/// <returns>The org ID value, or null if not present.</returns>
public static string? ParseOrgIdFromProperties(IReadOnlyDictionary<string, string>? properties)
Expand All @@ -232,22 +252,40 @@ public static long GetPositiveLongPropertyWithValidation(IReadOnlyDictionary<str

if (properties.TryGetValue(SparkParameters.Path, out string? path) && !string.IsNullOrEmpty(path))
{
int q = path.IndexOf('?');
if (q >= 0)
string? orgId = ParseOrgIdFromPathOrUriQuery(path);
if (orgId != null) return orgId;
}

if (properties.TryGetValue(AdbcOptions.Uri, out string? uri) && !string.IsNullOrEmpty(uri)
&& Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsedUri))
{
if (!string.IsNullOrEmpty(parsedUri.Query))
{
string? orgId = ParseOrgIdFromQueryString(path.Substring(q + 1));
string? orgId = ParseOrgIdFromQueryString(parsedUri.Query.TrimStart('?'));
if (orgId != null) return orgId;
}

// Fall back to /sql/protocolv1/o/<wsid>/<cluster> in the URI path.
Match uriPathMatch = s_clusterPathOrgIdPattern.Match(parsedUri.AbsolutePath);
if (uriPathMatch.Success) return uriPathMatch.Groups[1].Value;
}

if (properties.TryGetValue(AdbcOptions.Uri, out string? uri) && !string.IsNullOrEmpty(uri)
&& Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsedUri)
&& !string.IsNullOrEmpty(parsedUri.Query))
return null;
}

// ?o= in the query string wins; otherwise look for the cluster path segment.
private static string? ParseOrgIdFromPathOrUriQuery(string path)
{
int q = path.IndexOf('?');
if (q >= 0)
{
return ParseOrgIdFromQueryString(parsedUri.Query.TrimStart('?'));
string? orgId = ParseOrgIdFromQueryString(path.Substring(q + 1));
if (orgId != null) return orgId;
}

return null;
string pathOnly = q >= 0 ? path.Substring(0, q) : path;
Match match = s_clusterPathOrgIdPattern.Match(pathOnly);
return match.Success ? match.Groups[1].Value : null;
}
}
}
94 changes: 94 additions & 0 deletions csharp/test/Unit/PropertyHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.Collections.Generic;
using AdbcDrivers.HiveServer2.Spark;
using Apache.Arrow.Adbc;
using Xunit;

namespace AdbcDrivers.Databricks.Tests.Unit
{
/// <summary>
/// Tests for the SPOG cluster-path fallback in <see cref="PropertyHelper.ParseOrgIdFromProperties"/>.
/// Existing <c>?o=</c> extraction is covered indirectly through
/// <see cref="StatementExecution.StatementExecutionConnectionOrgIdTests"/>.
/// </summary>
public class PropertyHelperTests
{
[Fact]
public void ParseOrgIdFromProperties_ClusterPathWithoutQueryParam_ExtractsOrgIdFromPathSegment()
{
var props = new Dictionary<string, string>
{
{ SparkParameters.Path, "sql/protocolv1/o/6051921418418893/0528-220959-uzmcn1qt" },
};

Assert.Equal("6051921418418893", PropertyHelper.ParseOrgIdFromProperties(props));
}

[Fact]
public void ParseOrgIdFromProperties_ClusterPathWithLeadingSlash_ExtractsOrgIdFromPathSegment()
{
var props = new Dictionary<string, string>
{
{ SparkParameters.Path, "/sql/protocolv1/o/6051921418418893/0528-220959-uzmcn1qt" },
};

Assert.Equal("6051921418418893", PropertyHelper.ParseOrgIdFromProperties(props));
}

[Fact]
public void ParseOrgIdFromProperties_ClusterPathWithQueryParam_QueryParamWins()
{
// ?o= takes precedence over the path segment when both are present.
var props = new Dictionary<string, string>
{
{ SparkParameters.Path, "sql/protocolv1/o/111/0528-220959-uzmcn1qt?o=222" },
};

Assert.Equal("222", PropertyHelper.ParseOrgIdFromProperties(props));
}

[Fact]
public void ParseOrgIdFromProperties_WarehousePathWithoutQueryParam_ReturnsNull()
{
// Regression guard: the cluster-path fallback must not match warehouse paths
// (they never embed the workspace ID).
var props = new Dictionary<string, string>
{
{ SparkParameters.Path, "/sql/1.0/warehouses/abc123" },
};

Assert.Null(PropertyHelper.ParseOrgIdFromProperties(props));
}

[Fact]
public void ParseOrgIdFromProperties_UriWithClusterPath_ExtractsOrgIdFromPathSegment()
{
// When the path is supplied via AdbcOptions.Uri instead of SparkParameters.Path,
// the cluster-segment fallback still applies.
var props = new Dictionary<string, string>
{
{
AdbcOptions.Uri,
"https://host.databricks.com/sql/protocolv1/o/6051921418418893/0528-220959-uzmcn1qt"
},
};

Assert.Equal("6051921418418893", PropertyHelper.ParseOrgIdFromProperties(props));
}
}
}
Loading