This is an automated email from the ASF dual-hosted git repository.
CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new e6818b37a fix(csharp): support spec-correct driver manifests (#4341)
e6818b37a is described below
commit e6818b37a0361db7ba87b1fd9dc4f5f282393745
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Thu May 21 05:10:34 2026 -0700
fix(csharp): support spec-correct driver manifests (#4341)
Closes #4329
The driver manager was parsing every TOML file as a connection profile,
so real driver manifests (with `manifest_version = 1` and a string
`version`) were rejected with "The 'profile_version' field has an
invalid value '1.5.2'. It must be an integer." Add a proper
DriverManifest parser per docs/source/format/driver_manifests.rst, with
[Driver.shared] as either a single string or a platform-tuple table.
Managed (.NET) driver selection moves from the C#-specific `driver_type`
field on connection profiles to a scheme-prefixed entrypoint on the
manifest: `dotnet:Type` for modern .NET, `netfx:Type` for .NET
Framework. The host rejects a manifest whose scheme doesn't match its
runtime, so mismatches fail with a clear error instead of an
assembly-loader mystery. Profile-driven managed loading uses the same
scheme via an `entrypoint` option in `[Options]`, which the driver
manager consumes and does not forward to the driver.
Also aligns env_var placeholder support with the spec syntax `{{
env_var(NAME) }}` per docs/source/format/connection_profiles.rst:
placeholders may be embedded anywhere in a value, repeated, missing vars
expand to "" (matching the C/C++ driver manager), and unknown functions
are rejected.
End-to-end coverage against DuckDB lives in
DriverManifestTests.FindLoadDriver_WithRealDriverManifest_LoadsDuckDbDriver.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../DriverManager/AdbcDriverManager.cs | 244 +++++++++------
.../DriverManager/ConnectionProfile.cs | 164 +++++++---
.../DriverManager/DriverManifest.cs | 331 +++++++++++++++++++++
.../DriverManager/FilesystemProfileProvider.cs | 8 +-
csharp/src/Apache.Arrow.Adbc/readme.md | 71 +++--
.../DriverManager/ColocatedManifestTests.cs | 90 ++++--
.../DriverManager/DriverManifestTests.cs | 320 ++++++++++++++++++++
.../DriverManager/EntrypointSchemeTests.cs | 123 ++++++++
.../DriverManager/TomlConnectionProfileTests.cs | 263 +++++++++++-----
9 files changed, 1357 insertions(+), 257 deletions(-)
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
index 88adeddfe..3badc3e45 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -64,10 +64,11 @@ namespace Apache.Arrow.Adbc.DriverManager
/// <para>
/// <b>Managed (.NET) drivers always require a manifest.</b> This
method loads the
/// driver as a native shared library unless a co-located TOML
manifest is present
- /// that specifies a managed <c>driver_type</c>. To load a managed
.NET driver,
- /// either point <paramref name="driverPath"/> at a directory
containing a
- /// co-located <c>.toml</c> manifest, call <see
cref="LoadFromManifest"/> directly,
- /// or use <see cref="LoadManagedDriver"/> to load a .NET assembly
without a manifest.
+ /// whose <c>[Driver].entrypoint</c> begins with a managed-runtime
scheme prefix
+ /// (e.g. <c>dotnet:My.Driver.Type</c>). To load a managed .NET
driver, either
+ /// point <paramref name="driverPath"/> at a directory containing such
a co-located
+ /// manifest or use <see cref="LoadManagedDriver"/> to load a .NET
assembly
+ /// without a manifest.
/// </para>
/// <para>
/// <b>Security:</b> <paramref name="driverPath"/> must be an
absolute, fully
@@ -112,12 +113,19 @@ namespace Apache.Arrow.Adbc.DriverManager
}
/// <summary>
- /// Loads a native driver assembly from a verified absolute path, with
no
- /// further manifest probing. All terminal native-load call sites
funnel
- /// through this helper so security policy is applied uniformly.
+ /// Loads a driver assembly from a verified absolute path, with no
further
+ /// manifest probing. If <paramref name="entrypoint"/> is
scheme-prefixed
+ /// (<c>dotnet:</c>, <c>netfx:</c>) the managed loader is used;
otherwise
+ /// the path is loaded as a native shared library. All terminal load
call
+ /// sites funnel through this helper so security policy and audit
logging
+ /// are applied uniformly.
/// </summary>
private static AdbcDriver LoadNativeDriver(string driverPath, string?
entrypoint, string loadMethod)
{
+ if (entrypoint != null && HasManagedEntrypointScheme(entrypoint))
+ {
+ return LoadByEntrypointScheme(driverPath, entrypoint,
manifestPath: null, loadMethod);
+ }
string resolvedEntrypoint = entrypoint ??
DeriveEntrypoint(driverPath);
return LoadWithSecurity(
driverPath,
@@ -424,25 +432,30 @@ namespace Apache.Arrow.Adbc.DriverManager
// OpenDatabaseFromProfile – load driver + open database in one step
//
-----------------------------------------------------------------------
+ /// <summary>The option key the connection profile uses to override
the driver entrypoint.</summary>
+ internal const string EntrypointOptionKey = "entrypoint";
+
/// <summary>
/// Loads the driver specified by <paramref name="profile"/> and opens
a database,
/// applying all options from the profile as connection parameters.
/// </summary>
/// <remarks>
/// <para>
- /// If the profile has a non-null <see
cref="ConnectionProfile.DriverTypeName"/>, the
- /// driver is loaded as a managed .NET assembly via <see
cref="LoadManagedDriver"/> and
- /// <see cref="ConnectionProfile.DriverName"/> is used as the assembly
path.
- /// </para>
- /// <para>
- /// Otherwise the driver is loaded as a native shared library via
- /// <see cref="FindLoadDriver"/>.
+ /// The driver is located by name via <see cref="FindLoadDriver"/>. If
a driver
+ /// manifest is found, its <c>[Driver].entrypoint</c> determines
whether the
+ /// driver loads natively or via the managed (.NET) host; a
scheme-prefixed
+ /// entrypoint such as <c>dotnet:My.Driver.Type</c> selects the
managed loader.
+ /// The profile's <c>[Options]</c> table may carry an
<c>entrypoint</c> value
+ /// that overrides anything in the manifest -- useful when
<c>driver</c> is a
+ /// bare shared-library path with no companion manifest.
/// </para>
/// <para>
/// All options (string, integer, and double) are merged into a single
/// <c>string → string</c> dictionary. Integer and double values are
formatted
/// using <see cref="CultureInfo.InvariantCulture"/>. The merged
dictionary is
/// passed to <see
cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
+ /// The <c>entrypoint</c> option (if any) is consumed by the driver
manager
+ /// and is not forwarded to the driver.
/// </para>
/// <para>
/// Call <see cref="ConnectionProfile.ResolveEnvVars"/> on the profile
before
@@ -475,24 +488,24 @@ namespace Apache.Arrow.Adbc.DriverManager
/// options, the explicit value takes precedence.
/// </para>
/// <para>
- /// If the profile has a non-null <see
cref="ConnectionProfile.DriverTypeName"/>, the
- /// driver is loaded as a managed .NET assembly via <see
cref="LoadManagedDriver"/> and
- /// <see cref="ConnectionProfile.DriverName"/> is used as the assembly
path.
+ /// The driver is located by name via <see cref="FindLoadDriver"/>; the
+ /// <c>entrypoint</c> option (if present in either the profile's
<c>[Options]</c>
+ /// or <paramref name="explicitOptions"/>) is consumed by the driver
manager
+ /// and overrides any entrypoint specified by a discovered driver
manifest.
+ /// Scheme-prefixed values such as <c>dotnet:My.Driver.Type</c> route
the load
+ /// through the managed (.NET) host.
/// </para>
/// <para>
- /// Otherwise the driver is loaded as a native shared library via
- /// <see cref="FindLoadDriver"/>.
- /// </para>
- /// <para>
- /// All options are merged into a single
- /// following order (later values override earlier ones for the same
key):
+ /// All options are merged into a single dictionary in the following
order (later
+ /// values override earlier ones for the same key):
/// <list type="number">
/// <item>Profile integer options (formatted as strings)</item>
/// <item>Profile double options (formatted as strings)</item>
/// <item>Profile string options</item>
/// <item>Explicit options from <paramref
name="explicitOptions"/></item>
/// </list>
- /// The merged dictionary is passed to <see
cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
+ /// The merged dictionary, minus any <c>entrypoint</c> entry, is
passed to
+ /// <see cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
/// </para>
/// </remarks>
/// <param name="profile">The connection profile specifying the driver
and options.</param>
@@ -513,25 +526,24 @@ namespace Apache.Arrow.Adbc.DriverManager
{
if (profile == null) throw new
ArgumentNullException(nameof(profile));
- AdbcDriver driver;
-
- if (!string.IsNullOrEmpty(profile.DriverTypeName))
+ Dictionary<string, string> mergedOptions = new Dictionary<string,
string>(StringComparer.Ordinal);
+ foreach (KeyValuePair<string, string> kv in
BuildStringOptions(profile, explicitOptions))
{
- // Managed .NET driver path
- if (string.IsNullOrEmpty(profile.DriverName))
- throw new AdbcException(
- "The connection profile specifies a driver_type but no
driver assembly path (driver field).",
- AdbcStatusCode.InvalidArgument);
-
- driver = LoadManagedDriver(profile.DriverName!,
profile.DriverTypeName!);
+ mergedOptions[kv.Key] = kv.Value;
}
- else
+
+ // entrypoint is a driver-manager option, not a driver option:
pull it out
+ // of the bag before opening the database. When set, it overrides
any
+ // entrypoint declared by a driver manifest found via
FindLoadDriver.
+ string? entrypoint = null;
+ if (mergedOptions.TryGetValue(EntrypointOptionKey, out string?
entrypointValue))
{
- // Native shared-library path
- driver = LoadDriverFromProfile(profile, null, loadOptions,
additionalSearchPathList);
+ entrypoint = entrypointValue;
+ mergedOptions.Remove(EntrypointOptionKey);
}
- return driver.Open(BuildStringOptions(profile, explicitOptions));
+ AdbcDriver driver = LoadDriverFromProfile(profile, entrypoint,
loadOptions, additionalSearchPathList);
+ return driver.Open(mergedOptions);
}
/// <summary>
@@ -658,15 +670,9 @@ namespace Apache.Arrow.Adbc.DriverManager
/// </para>
/// <para>
/// Co-located manifests allow drivers to ship with metadata about how
they should
- /// be loaded (e.g., specifying they're managed .NET drivers via
<c>driver_type</c>,
- /// or redirecting to the actual driver location via the <c>driver</c>
field).
- /// </para>
- /// <para>
- /// <b>Important:</b> Options specified in co-located manifests are
NOT automatically
- /// applied to database connections. The manifest is used solely for
driver loading.
- /// To use manifest options, explicitly load the profile with
- /// <see cref="TomlConnectionProfile.FromFile"/> and use
- /// <see cref="OpenDatabaseFromProfile(ConnectionProfile,
IReadOnlyDictionary{string, string}?, AdbcLoadFlags, string?)"/>.
+ /// be loaded -- the symbol name to invoke (or, for managed .NET
drivers, the type
+ /// to instantiate via a <c>dotnet:</c> / <c>netfx:</c> scheme on
<c>entrypoint</c>),
+ /// and platform-specific shared library paths under
<c>[Driver.shared]</c>.
/// </para>
/// </remarks>
/// <param name="driverPath">The path to the driver file.</param>
@@ -694,6 +700,12 @@ namespace Apache.Arrow.Adbc.DriverManager
}
}
+ /// <summary>The entrypoint scheme prefix for managed .NET (Core / 5+)
drivers.</summary>
+ internal const string DotnetEntrypointScheme = "dotnet:";
+
+ /// <summary>The entrypoint scheme prefix for managed .NET Framework
4.x drivers.</summary>
+ internal const string NetFxEntrypointScheme = "netfx:";
+
private static AdbcDriver LoadFromManifest(string manifestPath,
string? entrypoint)
{
if (!File.Exists(manifestPath))
@@ -703,68 +715,114 @@ namespace Apache.Arrow.Adbc.DriverManager
AdbcStatusCode.NotFound);
}
- ConnectionProfile manifest =
FilesystemProfileProvider.LoadFromFile(manifestPath);
+ DriverManifest manifest =
DriverManifest.LoadFromFile(manifestPath);
- if (string.IsNullOrEmpty(manifest.DriverName))
- {
- throw new AdbcException(
- $"Driver manifest does not specify a 'driver' field.",
- AdbcStatusCode.InvalidArgument);
- }
+ // Caller-supplied entrypoint wins over the manifest's. Falls back
to
+ // a derived native symbol name only when neither is provided.
+ string resolvedEntrypoint = entrypoint
+ ?? manifest.Entrypoint
+ ?? DeriveEntrypoint(manifest.LibraryPath);
string? manifestDir =
Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+ string resolvedPath = ResolveManifestPath(manifest.LibraryPath,
manifestDir);
- // Check if this is a managed driver
- if (!string.IsNullOrEmpty(manifest.DriverTypeName))
+ return LoadByEntrypointScheme(resolvedPath, resolvedEntrypoint,
manifestPath, nameof(LoadFromManifest));
+ }
+
+ /// <summary>Returns <c>true</c> if <paramref name="entrypoint"/> uses
a managed-runtime scheme prefix.</summary>
+ private static bool HasManagedEntrypointScheme(string entrypoint) =>
+ entrypoint.StartsWith(DotnetEntrypointScheme,
StringComparison.Ordinal) ||
+ entrypoint.StartsWith(NetFxEntrypointScheme,
StringComparison.Ordinal);
+
+ /// <summary>
+ /// Resolves a path read out of a manifest: absolute paths are
validated
+ /// against the security policy as-is; relative paths are anchored to
the
+ /// manifest's directory and validated to ensure they don't escape it.
+ /// </summary>
+ private static string ResolveManifestPath(string libraryPath, string?
manifestDir)
+ {
+ if (IsAbsolutePath(libraryPath))
{
- // Managed .NET driver - resolve path
- string driverPath = manifest.DriverName!;
- if (IsAbsolutePath(driverPath))
- {
- // Absolute path - validate it doesn't contain path
traversal
- DriverManagerSecurity.ValidatePathSecurity(driverPath,
"manifest driver path");
- }
- else
- {
- // Relative path - validate it doesn't escape the manifest
directory
- if (!string.IsNullOrEmpty(manifestDir))
- {
- driverPath =
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir, driverPath);
- }
- }
+ DriverManagerSecurity.ValidatePathSecurity(libraryPath,
"manifest driver path");
+ return libraryPath;
+ }
+ if (!string.IsNullOrEmpty(manifestDir))
+ {
+ return
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir!, libraryPath);
+ }
+ return libraryPath;
+ }
+
+ /// <summary>
+ /// Dispatches a driver load by the scheme prefix on the entrypoint
value.
+ /// Plain symbol names load as native drivers; <c>dotnet:</c> /
<c>netfx:</c>
+ /// route to the managed loader and are rejected when the host process
is
+ /// running on the wrong runtime.
+ /// </summary>
+ private static AdbcDriver LoadByEntrypointScheme(
+ string driverPath,
+ string entrypoint,
+ string? manifestPath,
+ string loadMethod)
+ {
+ if (entrypoint.StartsWith(DotnetEntrypointScheme,
StringComparison.Ordinal))
+ {
+ string typeName =
entrypoint.Substring(DotnetEntrypointScheme.Length);
+ EnsureRuntime(isNetFramework: false, scheme:
DotnetEntrypointScheme);
return LoadWithSecurity(
driverPath,
- manifest.DriverTypeName!,
+ typeName,
manifestPath,
- nameof(LoadFromManifest),
- () => LoadManagedDriverCore(driverPath,
manifest.DriverTypeName!));
+ loadMethod,
+ () => LoadManagedDriverCore(driverPath, typeName));
}
- // Native driver - resolve entrypoint and path
- string resolvedEntrypoint = entrypoint ??
DeriveEntrypoint(manifest.DriverName!);
-
- // Resolve driver path
- string resolvedDriverPath = manifest.DriverName!;
- if (IsAbsolutePath(resolvedDriverPath))
- {
- // Absolute path - validate it doesn't contain path traversal
- DriverManagerSecurity.ValidatePathSecurity(resolvedDriverPath,
"manifest driver path");
- }
- else
+ if (entrypoint.StartsWith(NetFxEntrypointScheme,
StringComparison.Ordinal))
{
- // Relative path - validate it doesn't escape the manifest
directory
- if (!string.IsNullOrEmpty(manifestDir))
- {
- resolvedDriverPath =
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir,
resolvedDriverPath);
- }
+ string typeName =
entrypoint.Substring(NetFxEntrypointScheme.Length);
+ EnsureRuntime(isNetFramework: true, scheme:
NetFxEntrypointScheme);
+ return LoadWithSecurity(
+ driverPath,
+ typeName,
+ manifestPath,
+ loadMethod,
+ () => LoadManagedDriverCore(driverPath, typeName));
}
return LoadWithSecurity(
- resolvedDriverPath,
+ driverPath,
typeName: null,
manifestPath: manifestPath,
- loadMethod: nameof(LoadFromManifest),
- () => CAdbcDriverImporter.Load(resolvedDriverPath,
resolvedEntrypoint));
+ loadMethod: loadMethod,
+ () => CAdbcDriverImporter.Load(driverPath, entrypoint));
+ }
+
+ /// <summary>
+ /// Throws <see cref="AdbcException"/> when the running .NET runtime
does
+ /// not match the host implied by the entrypoint scheme. The check uses
+ /// <see cref="RuntimeInformation.FrameworkDescription"/> (a runtime
+ /// property) rather than a compile-time symbol: this library can be
+ /// targeted at <c>netstandard2.0</c> and consumed from either runtime.
+ /// </summary>
+ private static void EnsureRuntime(bool isNetFramework, string scheme)
+ {
+ bool hostIsNetFramework = RuntimeInformation.FrameworkDescription
+ .StartsWith(".NET Framework",
StringComparison.OrdinalIgnoreCase);
+
+ if (isNetFramework && !hostIsNetFramework)
+ {
+ throw new AdbcException(
+ $"Driver entrypoint scheme '{scheme}' requires .NET
Framework, but the host process is " +
+ RuntimeInformation.FrameworkDescription + ".",
+ AdbcStatusCode.NotImplemented);
+ }
+ if (!isNetFramework && hostIsNetFramework)
+ {
+ throw new AdbcException(
+ $"Driver entrypoint scheme '{scheme}' requires .NET 5 or
later, but the host process is " +
+ RuntimeInformation.FrameworkDescription + ".",
+ AdbcStatusCode.NotImplemented);
+ }
}
private static AdbcDriver? TryLoadFromDirectory(string dir, string
driverName, string? entrypoint)
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
b/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
index 371ef67c3..f84a3e1b0 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
@@ -17,6 +17,8 @@
using System;
using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
namespace Apache.Arrow.Adbc.DriverManager
{
@@ -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;
@@ -49,25 +59,24 @@ namespace Apache.Arrow.Adbc.DriverManager
/// 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)
{
@@ -86,25 +95,16 @@ namespace Apache.Arrow.Adbc.DriverManager
}
/// <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;
@@ -119,38 +119,106 @@ namespace Apache.Arrow.Adbc.DriverManager
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;
}
}
}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs
b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs
new file mode 100644
index 000000000..9ad35036b
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs
@@ -0,0 +1,331 @@
+/*
+ * 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.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ /// <summary>
+ /// A parsed ADBC driver manifest. A driver manifest is a TOML file that
+ /// describes <i>where</i> a driver shared library lives and how to load
it.
+ /// It is distinct from a <see cref="ConnectionProfile"/>, which describes
+ /// <i>how to open a database</i> with a driver and carries option
key/value
+ /// pairs.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The manifest format is defined in
+ /// <c>docs/source/format/driver_manifests.rst</c>:
+ /// </para>
+ /// <code>
+ /// manifest_version = 1
+ ///
+ /// name = "Driver Display Name"
+ /// version = "1.2.3" # the driver's own version, a string
+ /// publisher = "..."
+ /// license = "Apache-2.0"
+ /// source = "..."
+ ///
+ /// [Driver]
+ /// entrypoint = "AdbcDriverInit" # optional; defaults are derived from
the file name
+ ///
+ /// # Either a single path:
+ /// [Driver]
+ /// shared = "/path/to/libadbc_driver.so"
+ ///
+ /// # Or platform-tuple-keyed paths:
+ /// [Driver.shared]
+ /// linux_amd64 = "/path/to/libadbc_driver.so"
+ /// macos_arm64 = "/path/to/libadbc_driver.dylib"
+ /// windows_amd64 = "C:\\path\\to\\adbc_driver.dll"
+ /// </code>
+ /// <para>
+ /// The <see cref="Entrypoint"/> value may be a plain native symbol name
+ /// (e.g. <c>AdbcDriverDuckdbInit</c>) or a scheme-prefixed value such as
+ /// <c>dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver</c> /
+ /// <c>netfx:My.Driver.Class</c>. The scheme tells the driver manager which
+ /// managed-runtime host to start; values without a scheme are loaded as
+ /// native C entrypoints.
+ /// </para>
+ /// </remarks>
+ internal sealed class DriverManifest
+ {
+ private const string ManifestVersionField = "manifest_version";
+
+ private DriverManifest(
+ long manifestVersion,
+ string? name,
+ string? version,
+ string? publisher,
+ string? license,
+ string? source,
+ string? entrypoint,
+ string libraryPath)
+ {
+ ManifestVersion = manifestVersion;
+ Name = name;
+ Version = version;
+ Publisher = publisher;
+ License = license;
+ Source = source;
+ Entrypoint = entrypoint;
+ LibraryPath = libraryPath;
+ }
+
+ /// <summary>The manifest format version. Always 1 today.</summary>
+ public long ManifestVersion { get; }
+
+ /// <summary>Display name of the driver.</summary>
+ public string? Name { get; }
+
+ /// <summary>The driver's own version (a free-form string per the
spec).</summary>
+ public string? Version { get; }
+
+ /// <summary>Publisher of the driver.</summary>
+ public string? Publisher { get; }
+
+ /// <summary>License identifier of the driver.</summary>
+ public string? License { get; }
+
+ /// <summary>Where this driver came from (e.g. a package
name).</summary>
+ public string? Source { get; }
+
+ /// <summary>
+ /// The entrypoint value from <c>[Driver].entrypoint</c>. May be a
plain
+ /// symbol name, a <c>dotnet:</c>-prefixed type name, or any other
+ /// scheme-prefixed value. May be <c>null</c> if the manifest does not
+ /// specify one (in which case callers derive a default).
+ /// </summary>
+ public string? Entrypoint { get; }
+
+ /// <summary>
+ /// The driver library path for the current platform, resolved from
+ /// <c>[Driver.shared]</c>. May be an absolute path or a path relative
+ /// to the manifest's directory; callers are responsible for
resolution.
+ /// </summary>
+ public string LibraryPath { get; }
+
+ /// <summary>
+ /// Returns <c>true</c> if the given parsed TOML content looks like a
+ /// driver manifest rather than a connection profile.
+ /// </summary>
+ /// <remarks>
+ /// The discriminator is the presence of <c>manifest_version</c> at the
+ /// root level, or any <c>[Driver]</c> / <c>[Driver.shared]</c>
section.
+ /// </remarks>
+ internal static bool LooksLikeManifest(Dictionary<string,
Dictionary<string, object>> sections)
+ {
+ if (sections == null) return false;
+
+ if (sections.TryGetValue("", out Dictionary<string, object>? root)
&&
+ root.ContainsKey(ManifestVersionField))
+ {
+ return true;
+ }
+
+ return sections.ContainsKey("Driver") ||
sections.ContainsKey("Driver.shared");
+ }
+
+ /// <summary>
+ /// Parses a driver manifest from TOML content.
+ /// </summary>
+ /// <param name="tomlContent">The raw TOML text to parse.</param>
+ /// <returns>The parsed <see cref="DriverManifest"/>.</returns>
+ /// <exception cref="ArgumentNullException">If <paramref
name="tomlContent"/> is null.</exception>
+ /// <exception cref="AdbcException">
+ /// Thrown when the TOML is malformed, the manifest version is
unsupported,
+ /// or no library path can be resolved for the current platform.
+ /// </exception>
+ public static DriverManifest LoadFromContent(string tomlContent)
+ {
+ if (tomlContent == null) throw new
ArgumentNullException(nameof(tomlContent));
+
+ Dictionary<string, Dictionary<string, object>> sections;
+ try
+ {
+ sections = TomlParser.Parse(tomlContent);
+ }
+ catch (FormatException ex)
+ {
+ throw new AdbcException(
+ "Invalid TOML driver manifest: " + ex.Message,
+ AdbcStatusCode.InvalidArgument,
+ ex);
+ }
+
+ Dictionary<string, object> root = sections.TryGetValue("", out
Dictionary<string, object>? r)
+ ? r
+ : new Dictionary<string, object>();
+
+ long manifestVersion = ReadManifestVersion(root);
+ if (manifestVersion != 1)
+ {
+ throw new AdbcException(
+ $"Driver manifest version '{manifestVersion}' is not
supported by this driver manager.",
+ AdbcStatusCode.NotImplemented);
+ }
+
+ string? name = ReadOptionalString(root, "name");
+ string? version = ReadOptionalString(root, "version");
+ string? publisher = ReadOptionalString(root, "publisher");
+ string? license = ReadOptionalString(root, "license");
+ string? source = ReadOptionalString(root, "source");
+
+ string? entrypoint = null;
+ if (sections.TryGetValue("Driver", out Dictionary<string, object>?
driverSection))
+ {
+ entrypoint = ReadOptionalString(driverSection, "entrypoint");
+ }
+
+ string libraryPath = ResolveLibraryPath(sections, driverSection);
+
+ return new DriverManifest(
+ manifestVersion,
+ name,
+ version,
+ publisher,
+ license,
+ source,
+ entrypoint,
+ libraryPath);
+ }
+
+ /// <summary>
+ /// Parses a driver manifest from a file path.
+ /// </summary>
+ public static DriverManifest LoadFromFile(string filePath)
+ {
+ if (filePath == null) throw new
ArgumentNullException(nameof(filePath));
+ string content = File.ReadAllText(filePath, Encoding.UTF8);
+ return LoadFromContent(content);
+ }
+
+ private static long ReadManifestVersion(Dictionary<string, object>
root)
+ {
+ // Per the spec, manifest_version defaults to 1 when absent.
+ if (!root.TryGetValue(ManifestVersionField, out object?
versionObj))
+ {
+ return 1;
+ }
+
+ if (versionObj is long lv)
+ {
+ return lv;
+ }
+
+ throw new AdbcException(
+ $"The 'manifest_version' field has an invalid value
'{versionObj}'. It must be an integer.",
+ AdbcStatusCode.InvalidArgument);
+ }
+
+ private static string? ReadOptionalString(Dictionary<string, object>
section, string key)
+ {
+ if (section.TryGetValue(key, out object? obj) && obj is string s)
+ {
+ return s;
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Resolves the library path from the manifest. Supports the two spec
forms:
+ /// <c>[Driver].shared = "..."</c> (single string) and
+ /// <c>[Driver.shared]</c> table keyed by platform tuple.
+ /// </summary>
+ private static string ResolveLibraryPath(
+ Dictionary<string, Dictionary<string, object>> sections,
+ Dictionary<string, object>? driverSection)
+ {
+ // Form 1: [Driver].shared = "/path/to/lib"
+ if (driverSection != null &&
+ driverSection.TryGetValue("shared", out object? sharedObj) &&
+ sharedObj is string sharedPath)
+ {
+ if (string.IsNullOrEmpty(sharedPath))
+ {
+ throw new AdbcException(
+ "Driver manifest has an empty 'Driver.shared' path.",
+ AdbcStatusCode.InvalidArgument);
+ }
+ return sharedPath;
+ }
+
+ // Form 2: [Driver.shared] table keyed by platform tuple
+ if (sections.TryGetValue("Driver.shared", out Dictionary<string,
object>? platformTable))
+ {
+ string current = GetCurrentPlatformTuple();
+ if (platformTable.TryGetValue(current, out object? platObj) &&
platObj is string platPath)
+ {
+ if (string.IsNullOrEmpty(platPath))
+ {
+ throw new AdbcException(
+ $"Driver manifest has an empty path for current
platform '{current}'.",
+ AdbcStatusCode.InvalidArgument);
+ }
+ return platPath;
+ }
+
+ List<string> tuples = new List<string>(platformTable.Count);
+ foreach (KeyValuePair<string, object> kv in platformTable)
tuples.Add(kv.Key);
+ tuples.Sort(StringComparer.Ordinal);
+ throw new AdbcException(
+ $"Driver manifest has no entry for current platform
'{current}'. " +
+ $"Available platforms: {string.Join(", ", tuples)}.",
+ AdbcStatusCode.NotFound);
+ }
+
+ throw new AdbcException(
+ "Driver manifest does not specify a library path. " +
+ "Provide a 'Driver.shared' value, either as a single string or
a " +
+ "platform-tuple-keyed table.",
+ AdbcStatusCode.InvalidArgument);
+ }
+
+ /// <summary>
+ /// Returns the platform tuple identifying the current OS and
architecture,
+ /// matching the format used by the ADBC driver-manifest spec
+ /// (e.g. <c>windows_amd64</c>, <c>linux_arm64</c>,
<c>macos_amd64</c>).
+ /// </summary>
+ internal static string GetCurrentPlatformTuple()
+ {
+ string os;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) os =
"windows";
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) os =
"macos";
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) os =
"linux";
+#if NET6_0_OR_GREATER
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) os =
"freebsd";
+#endif
+ else os = "unknown";
+
+ string arch;
+ switch (RuntimeInformation.ProcessArchitecture)
+ {
+ case Architecture.X64: arch = "amd64"; break;
+ case Architecture.Arm64: arch = "arm64"; break;
+ case Architecture.X86: arch = "x86"; break;
+ case Architecture.Arm: arch = "arm"; break;
+ default: arch =
RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); break;
+ }
+
+ return os + "_" + arch;
+ }
+ }
+}
diff --git
a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
index 47e51c74e..870bbb8ae 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
@@ -131,12 +131,6 @@ namespace Apache.Arrow.Adbc.DriverManager
driverName = driverStr;
}
- string? driverTypeName = null;
- if (root.TryGetValue("driver_type", out object? driverTypeObj) &&
driverTypeObj is string driverTypeStr)
- {
- driverTypeName = driverTypeStr;
- }
-
Dictionary<string, string> stringOpts = new Dictionary<string,
string>(StringComparer.Ordinal);
Dictionary<string, long> intOpts = new Dictionary<string,
long>(StringComparer.Ordinal);
Dictionary<string, double> doubleOpts = new Dictionary<string,
double>(StringComparer.Ordinal);
@@ -173,7 +167,7 @@ namespace Apache.Arrow.Adbc.DriverManager
}
}
- return new ConnectionProfile(driverName, driverTypeName,
stringOpts, intOpts, doubleOpts);
+ return new ConnectionProfile(driverName, stringOpts, intOpts,
doubleOpts);
}
/// <summary>
diff --git a/csharp/src/Apache.Arrow.Adbc/readme.md
b/csharp/src/Apache.Arrow.Adbc/readme.md
index 75f956ccb..3b4b14a2f 100644
--- a/csharp/src/Apache.Arrow.Adbc/readme.md
+++ b/csharp/src/Apache.Arrow.Adbc/readme.md
@@ -27,42 +27,79 @@ The `Apache.Arrow.Adbc.DriverManager` namespace provides a
.NET implementation o
### Features
- **Driver discovery**: search for ADBC drivers by name across configurable
directories (environment variable, user-level, system-level).
-- **TOML manifest loading**: locate drivers via `.toml` manifest files that
specify the shared library path.
+- **TOML driver manifests**: locate drivers via `.toml` manifest files that
specify the shared library path per platform.
- **Connection profiles**: load reusable connection configurations (driver +
options) from `.toml` profile files.
+- **Managed (.NET) drivers**: load .NET drivers via a scheme-prefixed
`entrypoint` (`dotnet:` for .NET 5+, `netfx:` for .NET Framework 4.x).
- **Custom profile providers**: plug in your own `IConnectionProfileProvider`
implementation.
-### TOML Manifest / Profile Format
+### Driver Manifest Format
-#### Connection Profile Example (Snowflake)
+A *driver manifest* is a TOML file describing where a driver lives and how to
load it. The format is shared across all ADBC driver-manager implementations
and documented in `docs/source/format/driver_manifests.rst`.
-For unmanaged drivers loaded from native shared libraries:
+#### Native Driver Manifest Example (Snowflake)
```toml
-profile_version = 1
-driver = "libadbc_driver_snowflake"
+manifest_version = 1
+
+name = "Snowflake"
+version = "1.5.2"
+publisher = "snowflake.com"
+
+[Driver]
entrypoint = "AdbcDriverSnowflakeInit"
+[Driver.shared]
+windows_amd64 = "C:\\path\\to\\adbc_driver_snowflake.dll"
+linux_amd64 = "/usr/local/lib/libadbc_driver_snowflake.so"
+macos_arm64 = "/opt/homebrew/lib/libadbc_driver_snowflake.dylib"
+```
+
+#### Managed Driver Manifest Example (BigQuery)
+
+Managed .NET drivers use a scheme-prefixed `entrypoint`:
+
+- `dotnet:` for modern .NET (.NET 5 and later, including .NET 8 / .NET 10)
+- `netfx:` for .NET Framework 4.x
+
+The host process rejects a manifest whose scheme doesn't match its runtime, so
a `dotnet:` manifest on a .NET Framework process (or vice versa) fails with a
clear error rather than mysteriously failing inside the assembly loader.
+
+```toml
+manifest_version = 1
+
+name = "BigQuery"
+version = "1.2.0"
+
+[Driver]
+entrypoint = "dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
+shared = "Apache.Arrow.Adbc.Drivers.BigQuery.dll"
+```
+
+`shared` is relative to the manifest's directory. Managed .NET assemblies are
platform-neutral, so the single-string form of `shared` is usually appropriate;
the platform-tuple table is also accepted.
+
+### Connection Profile Format
+
+A *connection profile* points at a driver and supplies options to apply when
opening a database. Profiles can name a driver by manifest name (resolved
against the standard search paths), by direct path to a shared library, or by
direct path to a manifest.
+
+```toml
+profile_version = 1
+driver = "snowflake"
+
[Options]
adbc.snowflake.sql.account = "myaccount"
adbc.snowflake.sql.warehouse = "mywarehouse"
-adbc.snowflake.sql.auth_type = "auth_snowflake"
-username = "myuser"
-password = "env_var(SNOWFLAKE_PASSWORD)"
+password = "{{ env_var(SNOWFLAKE_PASSWORD) }}"
```
-#### Managed Driver Profile Example (BigQuery)
-
-For managed .NET drivers:
+If the profile points directly at a shared library that uses a non-default
entrypoint (or at a managed assembly that needs a `dotnet:` / `netfx:`
selector), supply it through the `entrypoint` option. The driver manager
consumes that option and does not forward it to the driver:
```toml
profile_version = 1
driver = "C:\\path\\to\\Apache.Arrow.Adbc.Drivers.BigQuery.dll"
-driver_type = "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
[Options]
+entrypoint = "dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
adbc.bigquery.project_id = "my-project"
-adbc.bigquery.auth_type = "service"
-adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
+adbc.bigquery.json_credential = "{{ env_var(BIGQUERY_JSON_CREDENTIAL) }}"
```
#### Format Notes
@@ -70,9 +107,7 @@ adbc.bigquery.json_credential =
"env_var(BIGQUERY_JSON_CREDENTIAL)"
- Use `profile_version = 1` for the version field (legacy `version` is also
supported for backward compatibility)
- Use `[Options]` for the options section (legacy `[options]` is also
supported for backward compatibility)
- Boolean option values are converted to the string equivalents `"true"` or
`"false"`.
-- Values of the form `env_var(ENV_VAR_NAME)` are expanded from the named
environment variable at connection time.
-- For unmanaged drivers, use `driver` for the library path and `entrypoint`
for the initialization function.
-- For managed drivers, use `driver` for the assembly path and `driver_type`
for the fully-qualified type name.
+- String values may contain `{{ env_var(NAME) }}` placeholders, which are
expanded from process environment variables when `ResolveEnvVars()` is called.
The `{{` and `}}` delimiters serve as escapes: any text outside placeholders is
treated literally. Placeholders may appear anywhere inside a value and may be
repeated. A missing environment variable expands to an empty string. Only
`env_var(NAME)` is recognized; other content inside a placeholder is an error.
### Managed Driver Loading (.NET Core / .NET 8)
diff --git
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
index 4c72c40cb..19c4ef1a5 100644
---
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
+++
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
@@ -75,6 +75,17 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
return (dllPath, tomlPath);
}
+ /// <summary>
+ /// Scheme prefix this test process must use when selecting managed
drivers
+ /// (matches the runtime hosting the test).
+ /// </summary>
+ private static string ManagedScheme =>
+#if NETFRAMEWORK
+ "netfx:";
+#else
+ "dotnet:";
+#endif
+
/// <summary>
/// Creates test files where the manifest uses a relative path to a
real assembly.
/// </summary>
@@ -98,12 +109,12 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
File.WriteAllText(placeholderDllPath, "placeholder");
_tempFiles.Add(placeholderDllPath);
- // Create manifest that uses relative path to the real assembly
- string toml = "version = 1\n"
- + "driver = \"" + realAssemblyName + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
- + "from_manifest = \"true\"\n";
+ // Driver manifest: scheme-prefixed entrypoint selects the managed
runtime,
+ // [Driver].shared = "..." carries the relative assembly path.
+ string toml = "manifest_version = 1\n"
+ + "\n[Driver]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ + "shared = \"" + realAssemblyName + "\"\n";
string tomlPath = Path.Combine(tempDir, baseName + ".toml");
File.WriteAllText(tomlPath, toml);
@@ -133,8 +144,8 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
CreateTestFilesWithRelativeDriver("test_driver", typeName);
// LoadDriver should auto-detect the co-located manifest and use
it to determine:
- // - The actual driver location (from the 'driver' field -
relative path)
- // - Whether it's a managed driver (from 'driver_type')
+ // - The actual driver location (from [Driver].shared, a relative
path here)
+ // - The managed runtime to host the driver (from the scheme
prefix on entrypoint)
AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
Assert.NotNull(driver);
// Check type name instead of IsType to avoid assembly identity
issues
@@ -195,20 +206,57 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
}
[Fact]
- public void LoadDriver_ExplicitEntrypointStillWorks()
+ public void LoadDriver_ExplicitEntrypointOverridesManifest()
{
+ // Build a manifest whose [Driver].entrypoint is something the
caller will
+ // override. The caller passes a scheme-prefixed entrypoint
pointing at
+ // the real managed driver type; that override must win over the
manifest
+ // value and select the managed loader.
string typeName = typeof(FakeAdbcDriver).FullName!;
(string dllPath, string tomlPath, string realAssemblyPath) =
- CreateTestFilesWithRelativeDriver("entrypoint_test", typeName);
+ CreateTestFilesWithManifestEntrypoint("entrypoint_test",
"AdbcDriverInit");
- // Even with a manifest, explicit entrypoint parameter should work
- // (though for managed drivers, entrypoint doesn't apply - it's
ignored)
- AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath,
"CustomEntrypoint");
+ AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath,
ManagedScheme + typeName);
Assert.NotNull(driver);
Assert.Equal(typeName, driver.GetType().FullName);
}
+ /// <summary>
+ /// Like <see cref="CreateTestFilesWithRelativeDriver"/> but lets the
caller
+ /// control the manifest's <c>[Driver].entrypoint</c> value -- useful
for
+ /// tests that verify caller-supplied entrypoint overrides win.
+ /// </summary>
+ private (string placeholderDllPath, string tomlPath, string
realAssemblyPath) CreateTestFilesWithManifestEntrypoint(
+ string baseName,
+ string manifestEntrypoint)
+ {
+ string tempDir = Path.Combine(Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string realAssemblyName = Path.GetFileName(realAssemblyPath);
+ string copiedAssemblyPath = Path.Combine(tempDir,
realAssemblyName);
+ File.Copy(realAssemblyPath, copiedAssemblyPath, overwrite: true);
+ _tempFiles.Add(copiedAssemblyPath);
+
+ string placeholderDllPath = Path.Combine(tempDir, baseName +
".dll");
+ File.WriteAllText(placeholderDllPath, "placeholder");
+ _tempFiles.Add(placeholderDllPath);
+
+ string toml = "manifest_version = 1\n"
+ + "\n[Driver]\n"
+ + "entrypoint = \"" + manifestEntrypoint + "\"\n"
+ + "shared = \"" + realAssemblyName + "\"\n";
+
+ string tomlPath = Path.Combine(tempDir, baseName + ".toml");
+ File.WriteAllText(tomlPath, toml);
+ _tempFiles.Add(tomlPath);
+
+ return (placeholderDllPath, tomlPath, copiedAssemblyPath);
+ }
+
[Fact]
public void LoadDriver_RelativePathInManifest_ResolvedCorrectly()
{
@@ -226,9 +274,10 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
_tempFiles.Add(localAssemblyPath);
// Manifest uses relative path to the driver
- string toml = "version = 1\n"
- + "driver = \"" + assemblyFileName + "\"\n"
- + "driver_type = \"" + typeName + "\"\n";
+ string toml = "manifest_version = 1\n"
+ + "\n[Driver]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ + "shared = \"" + assemblyFileName + "\"\n";
string dllPath = Path.Combine(tempDir, "wrapper.dll");
string tomlPath = Path.Combine(tempDir, "wrapper.toml");
@@ -269,9 +318,10 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
_tempFiles.Add(copiedAssemblyPath);
// Create manifest with relative path
- string toml = "version = 1\n"
- + "driver = \"" + assemblyFileName + "\"\n"
- + "driver_type = \"" + typeName + "\"\n";
+ string toml = "manifest_version = 1\n"
+ + "\n[Driver]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ + "shared = \"" + assemblyFileName + "\"\n";
string soPath = Path.Combine(tempDir, "test.driver.so");
string soToml = Path.Combine(tempDir, "test.driver.toml");
@@ -306,7 +356,7 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
// Note: LoadManagedDriver does not currently detect co-located
manifests.
// It loads directly from the specified assembly path.
// To use manifest redirection, use LoadDriver with a co-located
manifest
- // that specifies driver_type.
+ // whose [Driver].entrypoint carries a dotnet:/netfx: scheme
prefix.
string typeName = typeof(FakeAdbcDriver).FullName!;
string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
diff --git
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs
new file mode 100644
index 000000000..db1c51938
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs
@@ -0,0 +1,320 @@
+/*
+ * 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.IO;
+using System.Runtime.InteropServices;
+using Apache.Arrow.Adbc.DriverManager;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.DriverManager
+{
+ /// <summary>
+ /// Tests for <see cref="DriverManifest"/>. These cover the format
documented
+ /// in <c>docs/source/format/driver_manifests.rst</c>: the
+ /// <c>manifest_version</c>, the optional metadata fields, and the two
+ /// shapes of <c>[Driver.shared]</c> (single string vs. platform table).
+ /// Also includes a real-driver round-trip against DuckDB that originally
+ /// reproduced https://github.com/apache/arrow-adbc/issues/4329.
+ /// </summary>
+ [Collection(DriverManagerSecurityCollection.Name)]
+ public class DriverManifestTests : IDisposable
+ {
+ private readonly List<string> _tempDirs = new List<string>();
+
+ public void Dispose()
+ {
+ foreach (string d in _tempDirs)
+ {
+ try { if (Directory.Exists(d)) Directory.Delete(d, true); }
catch { }
+ }
+ }
+
+ //
-----------------------------------------------------------------------
+ // [Driver.shared] in single-string form
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_DriverSharedAsString_ReturnsThatPath()
+ {
+ const string toml = @"
+manifest_version = 1
+name = ""Example""
+
+[Driver]
+shared = ""/usr/local/lib/libadbc_driver_example.so""
+";
+ DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+ Assert.Equal("/usr/local/lib/libadbc_driver_example.so",
manifest.LibraryPath);
+ Assert.Equal("Example", manifest.Name);
+ Assert.Equal(1, manifest.ManifestVersion);
+ Assert.Null(manifest.Entrypoint);
+ }
+
+ //
-----------------------------------------------------------------------
+ // [Driver.shared] as a platform-tuple table
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void
LoadFromContent_DriverSharedAsPlatformTable_PicksCurrentPlatform()
+ {
+ string current = DriverManifest.GetCurrentPlatformTuple();
+ string toml =
+ "manifest_version = 1\n" +
+ "\n[Driver.shared]\n" +
+ current + " = \"/path/for/this/platform\"\n" +
+ "irrelevant_platform = \"/should/not/match\"\n";
+
+ DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+ Assert.Equal("/path/for/this/platform", manifest.LibraryPath);
+ }
+
+ [Fact]
+ public void LoadFromContent_NoMatchingPlatform_ThrowsNotFound()
+ {
+ // Build a table that intentionally excludes the current platform.
+ const string toml = @"
+manifest_version = 1
+
+[Driver.shared]
+made_up_os_made_up_arch = ""/nowhere""
+";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+ // Error message should mention the current platform tuple
+ Assert.Contains(DriverManifest.GetCurrentPlatformTuple(),
ex.Message);
+ }
+
+ //
-----------------------------------------------------------------------
+ // Metadata fields
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_ReadsAllMetadataFields()
+ {
+ // Mixes single- and double-quoted strings to exercise both forms.
+ const string toml = @"
+manifest_version = 1
+
+name = ""Display Name""
+version = '1.5.2'
+publisher = 'ExampleCo'
+license = 'Apache-2.0'
+source = 'pkg-manager'
+
+[Driver]
+entrypoint = ""AdbcDriverExampleInit""
+shared = ""/path/lib.so""
+";
+ DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+ Assert.Equal("Display Name", manifest.Name);
+ Assert.Equal("1.5.2", manifest.Version);
+ Assert.Equal("ExampleCo", manifest.Publisher);
+ Assert.Equal("Apache-2.0", manifest.License);
+ Assert.Equal("pkg-manager", manifest.Source);
+ Assert.Equal("AdbcDriverExampleInit", manifest.Entrypoint);
+ }
+
+ //
-----------------------------------------------------------------------
+ // manifest_version defaulting + validation
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_NoManifestVersion_DefaultsTo1()
+ {
+ const string toml = @"
+[Driver]
+shared = ""/path/lib.so""
+";
+ DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+ Assert.Equal(1, manifest.ManifestVersion);
+ }
+
+ [Fact]
+ public void LoadFromContent_ManifestVersion2_ThrowsNotImplemented()
+ {
+ const string toml = @"
+manifest_version = 2
+
+[Driver]
+shared = ""/path/lib.so""
+";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+ }
+
+ [Fact]
+ public void
LoadFromContent_ManifestVersionAsString_ThrowsInvalidArgument()
+ {
+ // Despite the docs example showing 'version = "1.5.2"' (which is
the
+ // driver's own version), manifest_version itself must be an
integer.
+ const string toml = @"
+manifest_version = ""1""
+
+[Driver]
+shared = ""/path/lib.so""
+";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ //
-----------------------------------------------------------------------
+ // Missing Driver.shared
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_NoLibraryPath_ThrowsInvalidArgument()
+ {
+ const string toml = @"
+manifest_version = 1
+name = ""Example""
+
+[Driver]
+entrypoint = ""AdbcDriverInit""
+";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ [Fact]
+ public void
LoadFromContent_EmptyDriverSharedString_ThrowsInvalidArgument()
+ {
+ const string toml = @"
+manifest_version = 1
+
+[Driver]
+shared = """"
+";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ //
-----------------------------------------------------------------------
+ // Malformed TOML wraps to AdbcException
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_MalformedToml_ThrowsAdbcException()
+ {
+ // Unterminated string -> FormatException out of TomlParser,
wrapped.
+ const string toml = "manifest_version = 1\n[Driver]\nshared =
\"unterminated\n";
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => DriverManifest.LoadFromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ //
-----------------------------------------------------------------------
+ // Round-trip with the literal-string form used in the docs example
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadFromContent_DocsExampleStyle_Parses()
+ {
+ // Mirrors docs/source/cpp/recipe_driver/driver_example.toml.in
+ // (which uses single-quoted strings throughout).
+ string current = DriverManifest.GetCurrentPlatformTuple();
+ string toml =
+ "manifest_version = 1\n" +
+ "\n" +
+ "name = 'Driver Example'\n" +
+ "publisher = 'arrow-adbc-docs'\n" +
+ "license = 'Apache-2.0'\n" +
+ "version = '1.0.0'\n" +
+ "source = 'recipe'\n" +
+ "\n" +
+ "[ADBC]\n" +
+ "version = 'v1.1.0'\n" +
+ "\n" +
+ "[Driver]\n" +
+ "[Driver.shared]\n" +
+ current + " = '/opt/adbc/libadbc_driver_example.so'\n";
+
+ DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+ Assert.Equal("Driver Example", manifest.Name);
+ Assert.Equal("1.0.0", manifest.Version);
+ Assert.Equal("/opt/adbc/libadbc_driver_example.so",
manifest.LibraryPath);
+ }
+
+ //
-----------------------------------------------------------------------
+ // End-to-end against a real driver (DuckDB). Originally written as the
+ // regression test for
https://github.com/apache/arrow-adbc/issues/4329:
+ // FindLoadDriver was routing real driver manifests through the
+ // connection-profile loader, which rejected the spec-correct
+ // `version = "1.5.2"` field as a non-integer.
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void FindLoadDriver_WithRealDriverManifest_LoadsDuckDbDriver()
+ {
+ // Locate the DuckDB native library copied next to the test
assembly
+ // by the CopyDuckDb MSBuild target (see
Apache.Arrow.Adbc.Testing.csproj).
+ string root = Directory.GetCurrentDirectory();
+ string duckdbFile;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ duckdbFile = Path.Combine(root, "duckdb.dll");
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ duckdbFile = Path.Combine(root, "libduckdb.dylib");
+ else
+ duckdbFile = Path.Combine(root, "libduckdb.so");
+
+ Assert.True(File.Exists(duckdbFile), $"DuckDB library missing at
{duckdbFile}");
+
+ string current = DriverManifest.GetCurrentPlatformTuple();
+ // TOML basic strings interpret \ as an escape, so backslashes in
+ // Windows paths must be doubled.
+ string tomlEscapedPath = duckdbFile.Replace("\\", "\\\\");
+
+ string manifest =
+ "manifest_version = 1\n" +
+ "\n" +
+ "name = \"DuckDB\"\n" +
+ "version = \"1.5.2\" # driver version - a string per
the spec\n" +
+ "publisher = \"duckdb.org\"\n" +
+ "license = \"MIT\"\n" +
+ "\n" +
+ "[ADBC]\n" +
+ "version = \"1.1.0\"\n" +
+ "\n" +
+ "[Driver]\n" +
+ "entrypoint = \"duckdb_adbc_init\"\n" +
+ "\n" +
+ "[Driver.shared]\n" +
+ current + " = \"" + tomlEscapedPath + "\"\n";
+
+ string tempDir = Path.Combine(Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string manifestPath = Path.Combine(tempDir, "duckdb.toml");
+ File.WriteAllText(manifestPath, manifest);
+
+ using AdbcDriver driver = AdbcDriverManager.FindLoadDriver(
+ "duckdb",
+ entrypoint: "duckdb_adbc_init",
+ loadOptions: AdbcLoadFlags.Default,
+ additionalSearchPathList: tempDir);
+
+ Assert.NotNull(driver);
+ }
+ }
+}
diff --git
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs
new file mode 100644
index 000000000..aafd99e6b
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs
@@ -0,0 +1,123 @@
+/*
+ * 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.IO;
+using Apache.Arrow.Adbc.DriverManager;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.DriverManager
+{
+ /// <summary>
+ /// Tests for the entrypoint-scheme dispatch in <see
cref="AdbcDriverManager"/>:
+ /// <c>dotnet:</c> and <c>netfx:</c> prefixes select the managed loader and
+ /// fail closed when the host process runs on the wrong .NET runtime.
+ /// </summary>
+ [Collection(DriverManagerSecurityCollection.Name)]
+ public class EntrypointSchemeTests : IDisposable
+ {
+ private readonly List<string> _tempDirs = new List<string>();
+
+ public void Dispose()
+ {
+ foreach (string d in _tempDirs)
+ {
+ try { if (Directory.Exists(d)) Directory.Delete(d, true); }
catch { }
+ }
+ }
+
+ /// <summary>The "other" managed scheme for this test process (the one
the host can't load).</summary>
+ private static string ForeignScheme =>
+#if NETFRAMEWORK
+ "dotnet:";
+#else
+ "netfx:";
+#endif
+
+ /// <summary>The matching managed scheme for this test
process.</summary>
+ private static string NativeScheme =>
+#if NETFRAMEWORK
+ "netfx:";
+#else
+ "dotnet:";
+#endif
+
+ private string CreateManifest(string entrypoint, string
assemblyFileName)
+ {
+ string tempDir = Path.Combine(Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ File.Copy(realAssemblyPath, Path.Combine(tempDir,
assemblyFileName), overwrite: true);
+
+ string toml = "manifest_version = 1\n"
+ + "\n[Driver]\n"
+ + "entrypoint = \"" + entrypoint + "\"\n"
+ + "shared = \"" + assemblyFileName + "\"\n";
+
+ string tomlPath = Path.Combine(tempDir, "scheme_test.toml");
+ File.WriteAllText(tomlPath, toml);
+ return tomlPath;
+ }
+
+ [Fact]
+ public void Manifest_ForeignScheme_FailsClosedWithClearError()
+ {
+ // A dotnet: manifest under .NET Framework (or netfx: under modern
.NET)
+ // should fail before any reflection happens, with a message that
names
+ // the requested scheme.
+ string assemblyFileName =
Path.GetFileName(typeof(FakeAdbcDriver).Assembly.Location);
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+ string entrypoint = ForeignScheme + typeName;
+ string tomlPath = CreateManifest(entrypoint, assemblyFileName);
+
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => AdbcDriverManager.FindLoadDriver(tomlPath));
+ Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+ Assert.Contains(ForeignScheme.TrimEnd(':'), ex.Message);
+ }
+
+ [Fact]
+ public void Manifest_NativeScheme_LoadsManagedDriver()
+ {
+ // Sanity check: the matching scheme on the same code path does
load.
+ string assemblyFileName =
Path.GetFileName(typeof(FakeAdbcDriver).Assembly.Location);
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+ string entrypoint = NativeScheme + typeName;
+ string tomlPath = CreateManifest(entrypoint, assemblyFileName);
+
+ AdbcDriver driver = AdbcDriverManager.FindLoadDriver(tomlPath);
+ Assert.NotNull(driver);
+ Assert.Equal(typeName, driver.GetType().FullName);
+ }
+
+ [Fact]
+ public void LoadDriver_ExplicitForeignSchemeEntrypoint_FailsClosed()
+ {
+ // The dispatch fires even without a manifest: an explicit
caller-supplied
+ // entrypoint with a foreign scheme prefix is rejected on this
runtime.
+ string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ AdbcException ex = Assert.Throws<AdbcException>(
+ () => AdbcDriverManager.LoadDriver(realAssemblyPath,
ForeignScheme + typeName));
+ Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+ }
+ }
+}
diff --git
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
index a6a0f35dc..c811b47ec 100644
---
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
+++
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -292,7 +292,7 @@ version = 1
driver = ""d""
[options]
-password = ""env_var(ADBC_TEST_PASSWORD_TOML)""
+password = ""{{ env_var(ADBC_TEST_PASSWORD_TOML) }}""
plain = ""notanenvvar""
";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
@@ -307,7 +307,7 @@ plain = ""notanenvvar""
}
[Fact]
- public void ResolveEnvVars_NoEnvVarValues_ReturnsSameProfile()
+ public void ResolveEnvVars_NoPlaceholders_ReturnsSameValues()
{
const string toml = @"
version = 1
@@ -320,6 +320,101 @@ key = ""value""
Assert.Equal("value", profile.StringOptions["key"]);
}
+ [Fact]
+ public void
ResolveEnvVars_PlaceholderEmbeddedInString_ExpandedInPlace()
+ {
+ // Per spec: placeholders may appear anywhere inside a value.
+ const string varName = "ADBC_TEST_EMBEDDED_HOST";
+ Environment.SetEnvironmentVariable(varName, "prod.example.com");
+ try
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+uri = ""postgres://user@{{ env_var(ADBC_TEST_EMBEDDED_HOST) }}:5432/db""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+ Assert.Equal("postgres://[email protected]:5432/db",
profile.StringOptions["uri"]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(varName, null);
+ }
+ }
+
+ [Fact]
+ public void ResolveEnvVars_MultiplePlaceholdersInOneValue_AllExpanded()
+ {
+ const string hostVar = "ADBC_TEST_MULTI_HOST";
+ const string portVar = "ADBC_TEST_MULTI_PORT";
+ Environment.SetEnvironmentVariable(hostVar, "db.local");
+ Environment.SetEnvironmentVariable(portVar, "5433");
+ try
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+uri = ""postgres://{{ env_var(ADBC_TEST_MULTI_HOST) }}:{{
env_var(ADBC_TEST_MULTI_PORT) }}/db""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+ Assert.Equal("postgres://db.local:5433/db",
profile.StringOptions["uri"]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(hostVar, null);
+ Environment.SetEnvironmentVariable(portVar, null);
+ }
+ }
+
+ [Fact]
+ public void
ResolveEnvVars_WhitespaceVariationsInsidePlaceholder_AllAccepted()
+ {
+ const string varName = "ADBC_TEST_WS_VAR";
+ Environment.SetEnvironmentVariable(varName, "X");
+ try
+ {
+ // No whitespace, lots of whitespace, asymmetric whitespace --
all valid.
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+tight = ""{{env_var(ADBC_TEST_WS_VAR)}}""
+loose = ""{{ env_var(ADBC_TEST_WS_VAR) }}""
+asymmetric = ""{{ env_var(ADBC_TEST_WS_VAR)}}""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+ Assert.Equal("X", profile.StringOptions["tight"]);
+ Assert.Equal("X", profile.StringOptions["loose"]);
+ Assert.Equal("X", profile.StringOptions["asymmetric"]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(varName, null);
+ }
+ }
+
+ [Fact]
+ public void
ResolveEnvVars_BareEnvVarSyntax_NotInterpretedAsPlaceholder()
+ {
+ // The old (pre-spec) C# implementation treated a whole-value
'env_var(NAME)'
+ // as a placeholder. The spec requires '{{ }}' delimiters, so the
bare form
+ // is now a literal string.
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+literal = ""env_var(NOT_A_PLACEHOLDER)""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+ Assert.Equal("env_var(NOT_A_PLACEHOLDER)",
profile.StringOptions["literal"]);
+ }
+
//
-----------------------------------------------------------------------
// Positive: AdbcDriverManager DeriveEntrypoint
//
-----------------------------------------------------------------------
@@ -438,12 +533,15 @@ driver = ""mydriver""
}
//
-----------------------------------------------------------------------
- // Negative: env_var expansion – variable not set
+ // env_var expansion – variable not set
//
-----------------------------------------------------------------------
[Fact]
- public void ResolveEnvVars_MissingEnvVar_ThrowsAdbcException()
+ public void ResolveEnvVars_MissingEnvVar_ExpandsToEmptyString()
{
+ // Per spec (and matching the C/C++ driver manager): a missing env
var
+ // expands to "" and processing of the rest of the value continues.
+ // Example from the spec: "foo{{ env_var(MISSING) }}bar" ->
"foobar".
const string varName = "ADBC_TEST_DEFINITELY_NOT_SET_XYZ";
Environment.SetEnvironmentVariable(varName, null);
@@ -452,12 +550,61 @@ version = 1
driver = ""d""
[options]
-password = ""env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ)""
+password = ""{{ env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ) }}""
+greeting = ""foo{{ env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ) }}bar""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+ Assert.Equal("", profile.StringOptions["password"]);
+ Assert.Equal("foobar", profile.StringOptions["greeting"]);
+ }
+
+ //
-----------------------------------------------------------------------
+ // Negative: malformed / unsupported placeholders
+ //
-----------------------------------------------------------------------
+
+ [Fact]
+ public void ResolveEnvVars_UnsupportedFunction_ThrowsInvalidArgument()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+weird = ""{{ unknown_func(FOO) }}""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
+ AdbcException ex = Assert.Throws<AdbcException>(() =>
profile.ResolveEnvVars());
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ [Fact]
+ public void ResolveEnvVars_MissingClosingParen_ThrowsInvalidArgument()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+oops = ""{{ env_var(FOO }}""
";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
AdbcException ex = Assert.Throws<AdbcException>(() =>
profile.ResolveEnvVars());
- Assert.Equal(AdbcStatusCode.InvalidState, ex.Status);
- Assert.Contains(varName, ex.Message);
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ [Fact]
+ public void ResolveEnvVars_EmptyVarName_ThrowsInvalidArgument()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+oops = ""{{ env_var() }}""
+";
+ ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
+ AdbcException ex = Assert.Throws<AdbcException>(() =>
profile.ResolveEnvVars());
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
}
//
-----------------------------------------------------------------------
@@ -606,34 +753,6 @@ driver = ""abs_driver""
Assert.Equal("abs_driver", profile!.DriverName);
}
- //
-----------------------------------------------------------------------
- // Positive: driver_type field in TOML profile
- //
-----------------------------------------------------------------------
-
- [Fact]
- public void ParseProfile_WithDriverType_ParsedCorrectly()
- {
- const string toml = @"
-version = 1
-driver = ""Apache.Arrow.Adbc.Tests.dll""
-driver_type = ""Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver""
-";
- ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
- Assert.Equal("Apache.Arrow.Adbc.Tests.dll", profile.DriverName);
-
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver",
profile.DriverTypeName);
- }
-
- [Fact]
- public void ParseProfile_WithoutDriverType_DriverTypeNameIsNull()
- {
- const string toml = @"
-version = 1
-driver = ""mydriver""
-";
- ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
- Assert.Null(profile.DriverTypeName);
- }
-
//
-----------------------------------------------------------------------
// Positive: LoadManagedDriver loads a managed .NET driver by
reflection
//
-----------------------------------------------------------------------
@@ -692,35 +811,50 @@ driver = ""d""
//
-----------------------------------------------------------------------
// Positive: OpenDatabaseFromProfile end-to-end with managed driver
+ //
+ // Managed drivers are selected by a scheme-prefixed 'entrypoint'
option:
+ // dotnet:Type for modern .NET, netfx:Type for .NET Framework. The
driver
+ // manager consumes the entrypoint option before opening the database.
//
-----------------------------------------------------------------------
+ /// <summary>
+ /// Scheme prefix this test process must use when selecting managed
drivers
+ /// -- a dotnet: entrypoint on .NET Framework (or vice versa) is a
runtime
+ /// mismatch that the driver manager intentionally rejects.
+ /// </summary>
+ private static string ManagedScheme =>
+#if NETFRAMEWORK
+ "netfx:";
+#else
+ "dotnet:";
+#endif
+
[Fact]
public void OpenDatabaseFromProfile_ManagedDriver_OpensDatabase()
{
string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- // Build TOML content; escape any backslashes in the Windows
assembly path.
string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
+ string toml = "profile_version = 1\n"
+ "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
+ + "\n[Options]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ "project_id = \"my-project\"\n"
+ "region = \"us-east1\"\n";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
AdbcDatabase db =
AdbcDriverManager.OpenDatabaseFromProfile(profile);
- // Use type name comparison to avoid assembly identity issues when
loaded via Assembly.LoadFrom
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase",
db.GetType().FullName);
- // Access parameters via reflection since the type identity differs
System.Reflection.PropertyInfo? paramsProp =
db.GetType().GetProperty("Parameters");
Assert.NotNull(paramsProp);
IReadOnlyDictionary<string, string> parameters =
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
Assert.Equal("my-project", parameters["project_id"]);
Assert.Equal("us-east1", parameters["region"]);
+ // entrypoint is consumed by the driver manager, not forwarded to
the driver
+ Assert.False(parameters.ContainsKey("entrypoint"));
}
//
-----------------------------------------------------------------------
@@ -776,14 +910,13 @@ driver = ""d""
}
[Fact]
- public void
OpenDatabaseFromProfile_ManagedDriverMissingAssemblyPath_ThrowsAdbcException()
+ public void OpenDatabaseFromProfile_NoDriver_ThrowsAdbcException()
{
- // driver_type is set but the driver (assembly path) field is
omitted.
+ // A profile with no 'driver' field cannot be opened on its own.
const string toml = @"
-version = 1
-driver_type = ""Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver""
+profile_version = 1
-[options]
+[Options]
key = ""value""
";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
@@ -876,11 +1009,11 @@ key = ""value""
}
//
-----------------------------------------------------------------------
- // ResolveEnvVars preserves DriverTypeName
+ // ResolveEnvVars preserves the driver reference and other non-env
values
//
-----------------------------------------------------------------------
[Fact]
- public void ResolveEnvVars_DriverTypeNameIsPreserved()
+ public void ResolveEnvVars_DriverNameIsPreserved()
{
const string varName = "ADBC_TEST_RESOLVE_ENVVAR_HOST";
Environment.SetEnvironmentVariable(varName, "myhost");
@@ -889,13 +1022,12 @@ key = ""value""
const string toml = @"
version = 1
driver = ""MyDriver.dll""
-driver_type = ""My.Namespace.MyDriver""
[options]
-host = ""env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST)""
+host = ""{{ env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST) }}""
";
ConnectionProfile resolved =
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
- Assert.Equal("My.Namespace.MyDriver", resolved.DriverTypeName);
+ Assert.Equal("MyDriver.dll", resolved.DriverName);
Assert.Equal("myhost", resolved.StringOptions["host"]);
}
finally
@@ -915,20 +1047,18 @@ host = ""env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST)""
string typeName = typeof(FakeAdbcDriver).FullName!;
string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
+ string toml = "profile_version = 1\n"
+ "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
+ + "\n[Options]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ "known_key = \"hello\"\n"
+ "unknown_widget = \"ignored_by_driver\"\n";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
AdbcDatabase db =
AdbcDriverManager.OpenDatabaseFromProfile(profile);
- // Use type name comparison to avoid assembly identity issues
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase",
db.GetType().FullName);
- // Access parameters via reflection since the type identity differs
System.Reflection.PropertyInfo? paramsProp =
db.GetType().GetProperty("Parameters");
Assert.NotNull(paramsProp);
IReadOnlyDictionary<string, string> parameters =
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
@@ -1051,10 +1181,10 @@ bool_key = false
string typeName = typeof(FakeAdbcDriver).FullName!;
string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
+ string toml = "profile_version = 1\n"
+ "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
+ + "\n[Options]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ "profile_option = \"from_profile\"\n"
+ "shared_option = \"profile_value\"\n";
@@ -1068,21 +1198,14 @@ bool_key = false
AdbcDatabase db =
AdbcDriverManager.OpenDatabaseFromProfile(profile, explicitOptions);
- // Use type name comparison to avoid assembly identity issues
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase",
db.GetType().FullName);
- // Access parameters via reflection since the type identity differs
System.Reflection.PropertyInfo? paramsProp =
db.GetType().GetProperty("Parameters");
Assert.NotNull(paramsProp);
IReadOnlyDictionary<string, string> parameters =
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
- // Profile-only option should be present
Assert.Equal("from_profile", parameters["profile_option"]);
-
- // Explicit-only option should be present
Assert.Equal("from_explicit", parameters["explicit_option"]);
-
- // Shared option: explicit should override profile
Assert.Equal("explicit_value", parameters["shared_option"]);
}
@@ -1093,19 +1216,17 @@ bool_key = false
string typeName = typeof(FakeAdbcDriver).FullName!;
string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
+ string toml = "profile_version = 1\n"
+ "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
+ + "\n[Options]\n"
+ + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+ "key = \"value\"\n";
ConnectionProfile profile =
FilesystemProfileProvider.LoadFromContent(toml);
AdbcDatabase db =
AdbcDriverManager.OpenDatabaseFromProfile(profile, null);
- // Use type name comparison to avoid assembly identity issues
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase",
db.GetType().FullName);
- // Access parameters via reflection since the type identity differs
System.Reflection.PropertyInfo? paramsProp =
db.GetType().GetProperty("Parameters");
Assert.NotNull(paramsProp);
IReadOnlyDictionary<string, string> parameters =
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;