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
244 changes: 151 additions & 93 deletions csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs

Large diffs are not rendered by default.

164 changes: 116 additions & 48 deletions csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace Apache.Arrow.Adbc.DriverManager
{
Expand All @@ -33,12 +35,20 @@ namespace Apache.Arrow.Adbc.DriverManager
/// </para>
/// <para>
/// Options come in three typed flavors: string, 64-bit integer, and double.
/// String option values of the form <c>env_var(ENV_VAR_NAME)</c> are expanded
/// from the named environment variable by <see cref="ResolveEnvVars"/>.
/// String option values may contain <c>{{ env_var(NAME) }}</c> placeholders that
/// <see cref="ResolveEnvVars"/> expands using process environment variables.
/// </para>
/// </remarks>
public sealed class ConnectionProfile
{
// Per docs/source/format/connection_profiles.rst, dynamic substitutions
// are written as `{{ <function-call> }}` and may appear anywhere inside
// a string value. The character set inside the placeholder excludes
// braces so adjacent placeholders don't accidentally merge.
private static readonly Regex PlaceholderRegex = new Regex(
@"\{\{\s*([^{}]*?)\s*\}\}",
RegexOptions.Compiled | RegexOptions.CultureInvariant);

private const string EnvVarPrefix = "env_var(";

private readonly Dictionary<string, string> _stringOptions;
Expand All @@ -49,25 +59,24 @@ public sealed class ConnectionProfile
/// Initializes a new <see cref="ConnectionProfile"/>.
/// </summary>
/// <param name="driverName">
/// The driver name. For native drivers this is the path to a shared library or
/// a bare driver name; for managed drivers this is the path to the .NET assembly.
/// </param>
/// <param name="driverTypeName">
/// The fully-qualified .NET type name of the <see cref="AdbcDriver"/> subclass
/// to instantiate for managed (pure .NET) drivers, or <c>null</c> for native drivers.
/// The driver reference: a bare driver name (resolved against the manifest
/// search path), an absolute or relative path to a shared library, or an
/// absolute or relative path to a driver manifest <c>.toml</c> file. For
/// managed (.NET) drivers, the manifest at this location selects the
/// runtime via <c>[Driver].entrypoint</c>; alternatively, a profile that
/// points directly at a managed assembly can supply the type name through
/// an <c>entrypoint</c> option.
/// </param>
/// <param name="stringOptions">String options, or <c>null</c> for none.</param>
/// <param name="intOptions">Integer options, or <c>null</c> for none.</param>
/// <param name="doubleOptions">Double options, or <c>null</c> for none.</param>
public ConnectionProfile(
string? driverName = null,
string? driverTypeName = null,
IReadOnlyDictionary<string, string>? stringOptions = null,
IReadOnlyDictionary<string, long>? intOptions = null,
IReadOnlyDictionary<string, double>? doubleOptions = null)
{
DriverName = driverName;
DriverTypeName = driverTypeName;
_stringOptions = new Dictionary<string, string>(StringComparer.Ordinal);
if (stringOptions != null)
{
Expand All @@ -86,25 +95,16 @@ public ConnectionProfile(
}

/// <summary>
/// Gets the name of the driver specified by this profile, or <c>null</c> if
/// the profile does not specify a driver.
/// Gets the driver reference specified by this profile, or <c>null</c> if
/// the profile does not specify one. May be a bare driver name, a shared
/// library path, or a driver manifest path.
/// </summary>
public string? DriverName { get; }

/// <summary>
/// Gets the fully-qualified .NET type name of the <see cref="AdbcDriver"/>
/// subclass to instantiate for managed (pure .NET) drivers, or <c>null</c>
/// for native drivers.
/// </summary>
/// <example>
/// <c>Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver</c>
/// </example>
public string? DriverTypeName { get; }

/// <summary>
/// Gets the string options specified by this profile. Values of the form
/// <c>env_var(ENV_VAR_NAME)</c> will be expanded from the named environment
/// variable when <see cref="ResolveEnvVars"/> is called.
/// Gets the string options specified by this profile. Values may contain
/// <c>{{ env_var(NAME) }}</c> placeholders that <see cref="ResolveEnvVars"/>
/// expands using process environment variables.
/// </summary>
public IReadOnlyDictionary<string, string> StringOptions => _stringOptions;

Expand All @@ -119,38 +119,106 @@ public ConnectionProfile(
public IReadOnlyDictionary<string, double> DoubleOptions => _doubleOptions;

/// <summary>
/// Returns a new profile with any <c>env_var(NAME)</c> values in
/// <see cref="StringOptions"/> replaced by the value of the corresponding
/// environment variable.
/// Returns a new profile with any <c>{{ env_var(NAME) }}</c> placeholders
/// in <see cref="StringOptions"/> expanded using process environment
/// variables.
/// </summary>
/// <remarks>
/// <para>
/// Placeholder syntax matches the ADBC spec (see
/// <c>docs/source/format/connection_profiles.rst</c>):
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// Placeholders use <c>{{ }}</c> as the escape delimiters and may
/// appear anywhere inside a value. Whitespace inside the braces is
/// optional. Multiple placeholders may appear in one value
/// (e.g. <c>"jdbc://{{ env_var(HOST) }}:{{ env_var(PORT) }}/db"</c>).
/// </description>
/// </item>
/// <item>
/// <description>
/// A missing environment variable expands to an empty string and
/// processing continues; this matches the C/C++ driver manager.
/// </description>
/// </item>
/// <item>
/// <description>
/// The only supported function inside a placeholder is
/// <c>env_var(NAME)</c>. Any other content -- including a literal
/// <c>{{</c> in a value -- is rejected with
/// <see cref="AdbcStatusCode.InvalidArgument"/>.
/// </description>
/// </item>
/// </list>
/// </remarks>
/// <exception cref="AdbcException">
/// Thrown when a referenced environment variable is not set.
/// Thrown when a placeholder uses an unrecognized function or is malformed
/// (e.g. missing the closing parenthesis or environment variable name).
/// </exception>
public ConnectionProfile ResolveEnvVars()
{
Dictionary<string, string> resolved = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (KeyValuePair<string, string> kv in _stringOptions)
{
string value = kv.Value;
if (value.StartsWith(EnvVarPrefix, StringComparison.Ordinal) &&
value.EndsWith(")", StringComparison.Ordinal))
{
string varName = value.Substring(EnvVarPrefix.Length, value.Length - EnvVarPrefix.Length - 1);
string? envValue = Environment.GetEnvironmentVariable(varName);
if (envValue == null)
{
throw new AdbcException(
$"Environment variable '{varName}' required by profile option '{kv.Key}' is not set.",
AdbcStatusCode.InvalidState);
}
resolved[kv.Key] = envValue;
}
else
{
resolved[kv.Key] = value;
}
resolved[kv.Key] = ExpandPlaceholders(kv.Key, kv.Value);
}
return new ConnectionProfile(DriverName, resolved, _intOptions, _doubleOptions);
}

/// <summary>
/// Substitutes every <c>{{ ... }}</c> placeholder in <paramref name="value"/>
/// with its expansion. The only recognized function is <c>env_var(NAME)</c>;
/// anything else is an error.
/// </summary>
private static string ExpandPlaceholders(string key, string value)
{
if (string.IsNullOrEmpty(value) || value.IndexOf("{{", StringComparison.Ordinal) < 0)
{
return value;
}

StringBuilder sb = new StringBuilder(value.Length);
int lastIndex = 0;
foreach (Match match in PlaceholderRegex.Matches(value))
{
sb.Append(value, lastIndex, match.Index - lastIndex);
sb.Append(ExpandFunction(key, match.Groups[1].Value));
lastIndex = match.Index + match.Length;
}
sb.Append(value, lastIndex, value.Length - lastIndex);
return sb.ToString();
}

private static string ExpandFunction(string key, string content)
{
if (!content.StartsWith(EnvVarPrefix, StringComparison.Ordinal))
{
throw new AdbcException(
$"Profile option '{key}' uses an unsupported substitution '{content}'. " +
"Only env_var(NAME) is recognized.",
AdbcStatusCode.InvalidArgument);
}
return new ConnectionProfile(DriverName, DriverTypeName, resolved, _intOptions, _doubleOptions);
if (content.Length == 0 || content[content.Length - 1] != ')')
{
throw new AdbcException(
$"Profile option '{key}' has a malformed env_var() placeholder: missing closing parenthesis.",
AdbcStatusCode.InvalidArgument);
}

string varName = content.Substring(EnvVarPrefix.Length, content.Length - EnvVarPrefix.Length - 1);
if (varName.Length == 0)
{
throw new AdbcException(
$"Profile option '{key}' has a malformed env_var() placeholder: missing environment variable name.",
AdbcStatusCode.InvalidArgument);
}

// Missing environment variables expand to empty per the spec, matching
// the C/C++ driver manager. Callers that want to require an env var
// should validate after ResolveEnvVars returns.
return Environment.GetEnvironmentVariable(varName) ?? string.Empty;
}
}
}
Loading
Loading