diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
index 88adeddfef..3badc3e457 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -64,10 +64,11 @@ public static class AdbcDriverManager
///
/// Managed (.NET) drivers always require a manifest. This method loads the
/// driver as a native shared library unless a co-located TOML manifest is present
- /// that specifies a managed driver_type. To load a managed .NET driver,
- /// either point at a directory containing a
- /// co-located .toml manifest, call directly,
- /// or use to load a .NET assembly without a manifest.
+ /// whose [Driver].entrypoint begins with a managed-runtime scheme prefix
+ /// (e.g. dotnet:My.Driver.Type). To load a managed .NET driver, either
+ /// point at a directory containing such a co-located
+ /// manifest or use to load a .NET assembly
+ /// without a manifest.
///
///
/// Security: must be an absolute, fully
@@ -112,12 +113,19 @@ public static AdbcDriver LoadDriver(string driverPath, string? entrypoint = null
}
///
- /// 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 is scheme-prefixed
+ /// (dotnet:, netfx:) 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.
///
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 @@ private static AdbcDriver LoadManagedDriverCore(string assemblyPath, string type
// OpenDatabaseFromProfile – load driver + open database in one step
// -----------------------------------------------------------------------
+ /// The option key the connection profile uses to override the driver entrypoint.
+ internal const string EntrypointOptionKey = "entrypoint";
+
///
/// Loads the driver specified by and opens a database,
/// applying all options from the profile as connection parameters.
///
///
///
- /// If the profile has a non-null , the
- /// driver is loaded as a managed .NET assembly via and
- /// is used as the assembly path.
- ///
- ///
- /// Otherwise the driver is loaded as a native shared library via
- /// .
+ /// The driver is located by name via . If a driver
+ /// manifest is found, its [Driver].entrypoint determines whether the
+ /// driver loads natively or via the managed (.NET) host; a scheme-prefixed
+ /// entrypoint such as dotnet:My.Driver.Type selects the managed loader.
+ /// The profile's [Options] table may carry an entrypoint value
+ /// that overrides anything in the manifest -- useful when driver is a
+ /// bare shared-library path with no companion manifest.
///
///
/// All options (string, integer, and double) are merged into a single
/// string → string dictionary. Integer and double values are formatted
/// using . The merged dictionary is
/// passed to .
+ /// The entrypoint option (if any) is consumed by the driver manager
+ /// and is not forwarded to the driver.
///
///
/// Call on the profile before
@@ -475,24 +488,24 @@ public static AdbcDatabase OpenDatabaseFromProfile(
/// options, the explicit value takes precedence.
///
///
- /// If the profile has a non-null , the
- /// driver is loaded as a managed .NET assembly via and
- /// is used as the assembly path.
+ /// The driver is located by name via ; the
+ /// entrypoint option (if present in either the profile's [Options]
+ /// or ) is consumed by the driver manager
+ /// and overrides any entrypoint specified by a discovered driver manifest.
+ /// Scheme-prefixed values such as dotnet:My.Driver.Type route the load
+ /// through the managed (.NET) host.
///
///
- /// Otherwise the driver is loaded as a native shared library via
- /// .
- ///
- ///
- /// 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):
///
/// - Profile integer options (formatted as strings)
/// - Profile double options (formatted as strings)
/// - Profile string options
/// - Explicit options from
///
- /// The merged dictionary is passed to .
+ /// The merged dictionary, minus any entrypoint entry, is passed to
+ /// .
///
///
/// The connection profile specifying the driver and options.
@@ -513,25 +526,24 @@ public static AdbcDatabase OpenDatabaseFromProfile(
{
if (profile == null) throw new ArgumentNullException(nameof(profile));
- AdbcDriver driver;
-
- if (!string.IsNullOrEmpty(profile.DriverTypeName))
+ Dictionary mergedOptions = new Dictionary(StringComparer.Ordinal);
+ foreach (KeyValuePair 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);
}
///
@@ -658,15 +670,9 @@ private static AdbcDriver LoadFromAbsolutePath(string path, string? entrypoint)
///
///
/// Co-located manifests allow drivers to ship with metadata about how they should
- /// be loaded (e.g., specifying they're managed .NET drivers via driver_type,
- /// or redirecting to the actual driver location via the driver field).
- ///
- ///
- /// Important: 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
- /// and use
- /// .
+ /// be loaded -- the symbol name to invoke (or, for managed .NET drivers, the type
+ /// to instantiate via a dotnet: / netfx: scheme on entrypoint),
+ /// and platform-specific shared library paths under [Driver.shared].
///
///
/// The path to the driver file.
@@ -694,6 +700,12 @@ private static AdbcDriver LoadFromAbsolutePath(string path, string? entrypoint)
}
}
+ /// The entrypoint scheme prefix for managed .NET (Core / 5+) drivers.
+ internal const string DotnetEntrypointScheme = "dotnet:";
+
+ /// The entrypoint scheme prefix for managed .NET Framework 4.x drivers.
+ internal const string NetFxEntrypointScheme = "netfx:";
+
private static AdbcDriver LoadFromManifest(string manifestPath, string? entrypoint)
{
if (!File.Exists(manifestPath))
@@ -703,68 +715,114 @@ private static AdbcDriver LoadFromManifest(string manifestPath, string? entrypoi
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));
+ }
+
+ /// Returns true if uses a managed-runtime scheme prefix.
+ private static bool HasManagedEntrypointScheme(string entrypoint) =>
+ entrypoint.StartsWith(DotnetEntrypointScheme, StringComparison.Ordinal) ||
+ entrypoint.StartsWith(NetFxEntrypointScheme, StringComparison.Ordinal);
+
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ ///
+ /// Dispatches a driver load by the scheme prefix on the entrypoint value.
+ /// Plain symbol names load as native drivers; dotnet: / netfx:
+ /// route to the managed loader and are rejected when the host process is
+ /// running on the wrong runtime.
+ ///
+ 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));
+ }
+
+ ///
+ /// Throws when the running .NET runtime does
+ /// not match the host implied by the entrypoint scheme. The check uses
+ /// (a runtime
+ /// property) rather than a compile-time symbol: this library can be
+ /// targeted at netstandard2.0 and consumed from either runtime.
+ ///
+ 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 371ef67c34..f84a3e1b02 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
///
///
/// Options come in three typed flavors: string, 64-bit integer, and double.
- /// String option values of the form env_var(ENV_VAR_NAME) are expanded
- /// from the named environment variable by .
+ /// String option values may contain {{ env_var(NAME) }} placeholders that
+ /// expands using process environment variables.
///
///
public sealed class ConnectionProfile
{
+ // Per docs/source/format/connection_profiles.rst, dynamic substitutions
+ // are written as `{{ }}` 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 _stringOptions;
@@ -49,25 +59,24 @@ public sealed class ConnectionProfile
/// Initializes a new .
///
///
- /// 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.
- ///
- ///
- /// The fully-qualified .NET type name of the subclass
- /// to instantiate for managed (pure .NET) drivers, or null 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 .toml file. For
+ /// managed (.NET) drivers, the manifest at this location selects the
+ /// runtime via [Driver].entrypoint; alternatively, a profile that
+ /// points directly at a managed assembly can supply the type name through
+ /// an entrypoint option.
///
/// String options, or null for none.
/// Integer options, or null for none.
/// Double options, or null for none.
public ConnectionProfile(
string? driverName = null,
- string? driverTypeName = null,
IReadOnlyDictionary? stringOptions = null,
IReadOnlyDictionary? intOptions = null,
IReadOnlyDictionary? doubleOptions = null)
{
DriverName = driverName;
- DriverTypeName = driverTypeName;
_stringOptions = new Dictionary(StringComparer.Ordinal);
if (stringOptions != null)
{
@@ -86,25 +95,16 @@ public ConnectionProfile(
}
///
- /// Gets the name of the driver specified by this profile, or null if
- /// the profile does not specify a driver.
+ /// Gets the driver reference specified by this profile, or null if
+ /// the profile does not specify one. May be a bare driver name, a shared
+ /// library path, or a driver manifest path.
///
public string? DriverName { get; }
///
- /// Gets the fully-qualified .NET type name of the
- /// subclass to instantiate for managed (pure .NET) drivers, or null
- /// for native drivers.
- ///
- ///
- /// Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver
- ///
- public string? DriverTypeName { get; }
-
- ///
- /// Gets the string options specified by this profile. Values of the form
- /// env_var(ENV_VAR_NAME) will be expanded from the named environment
- /// variable when is called.
+ /// Gets the string options specified by this profile. Values may contain
+ /// {{ env_var(NAME) }} placeholders that
+ /// expands using process environment variables.
///
public IReadOnlyDictionary StringOptions => _stringOptions;
@@ -119,38 +119,106 @@ public ConnectionProfile(
public IReadOnlyDictionary DoubleOptions => _doubleOptions;
///
- /// Returns a new profile with any env_var(NAME) values in
- /// replaced by the value of the corresponding
- /// environment variable.
+ /// Returns a new profile with any {{ env_var(NAME) }} placeholders
+ /// in expanded using process environment
+ /// variables.
///
+ ///
+ ///
+ /// Placeholder syntax matches the ADBC spec (see
+ /// docs/source/format/connection_profiles.rst):
+ ///
+ ///
+ /// -
+ ///
+ /// Placeholders use {{ }} 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. "jdbc://{{ env_var(HOST) }}:{{ env_var(PORT) }}/db").
+ ///
+ ///
+ /// -
+ ///
+ /// A missing environment variable expands to an empty string and
+ /// processing continues; this matches the C/C++ driver manager.
+ ///
+ ///
+ /// -
+ ///
+ /// The only supported function inside a placeholder is
+ /// env_var(NAME). Any other content -- including a literal
+ /// {{ in a value -- is rejected with
+ /// .
+ ///
+ ///
+ ///
+ ///
///
- /// 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).
///
public ConnectionProfile ResolveEnvVars()
{
Dictionary resolved = new Dictionary(StringComparer.Ordinal);
foreach (KeyValuePair 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);
+ }
+
+ ///
+ /// Substitutes every {{ ... }} placeholder in
+ /// with its expansion. The only recognized function is env_var(NAME);
+ /// anything else is an error.
+ ///
+ 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 0000000000..9ad35036b6
--- /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
+{
+ ///
+ /// A parsed ADBC driver manifest. A driver manifest is a TOML file that
+ /// describes where a driver shared library lives and how to load it.
+ /// It is distinct from a , which describes
+ /// how to open a database with a driver and carries option key/value
+ /// pairs.
+ ///
+ ///
+ ///
+ /// The manifest format is defined in
+ /// docs/source/format/driver_manifests.rst:
+ ///
+ ///
+ /// 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"
+ ///
+ ///
+ /// The value may be a plain native symbol name
+ /// (e.g. AdbcDriverDuckdbInit) or a scheme-prefixed value such as
+ /// dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver /
+ /// netfx:My.Driver.Class. The scheme tells the driver manager which
+ /// managed-runtime host to start; values without a scheme are loaded as
+ /// native C entrypoints.
+ ///
+ ///
+ 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;
+ }
+
+ /// The manifest format version. Always 1 today.
+ public long ManifestVersion { get; }
+
+ /// Display name of the driver.
+ public string? Name { get; }
+
+ /// The driver's own version (a free-form string per the spec).
+ public string? Version { get; }
+
+ /// Publisher of the driver.
+ public string? Publisher { get; }
+
+ /// License identifier of the driver.
+ public string? License { get; }
+
+ /// Where this driver came from (e.g. a package name).
+ public string? Source { get; }
+
+ ///
+ /// The entrypoint value from [Driver].entrypoint. May be a plain
+ /// symbol name, a dotnet:-prefixed type name, or any other
+ /// scheme-prefixed value. May be null if the manifest does not
+ /// specify one (in which case callers derive a default).
+ ///
+ public string? Entrypoint { get; }
+
+ ///
+ /// The driver library path for the current platform, resolved from
+ /// [Driver.shared]. May be an absolute path or a path relative
+ /// to the manifest's directory; callers are responsible for resolution.
+ ///
+ public string LibraryPath { get; }
+
+ ///
+ /// Returns true if the given parsed TOML content looks like a
+ /// driver manifest rather than a connection profile.
+ ///
+ ///
+ /// The discriminator is the presence of manifest_version at the
+ /// root level, or any [Driver] / [Driver.shared] section.
+ ///
+ internal static bool LooksLikeManifest(Dictionary> sections)
+ {
+ if (sections == null) return false;
+
+ if (sections.TryGetValue("", out Dictionary? root) &&
+ root.ContainsKey(ManifestVersionField))
+ {
+ return true;
+ }
+
+ return sections.ContainsKey("Driver") || sections.ContainsKey("Driver.shared");
+ }
+
+ ///
+ /// Parses a driver manifest from TOML content.
+ ///
+ /// The raw TOML text to parse.
+ /// The parsed .
+ /// If is null.
+ ///
+ /// Thrown when the TOML is malformed, the manifest version is unsupported,
+ /// or no library path can be resolved for the current platform.
+ ///
+ public static DriverManifest LoadFromContent(string tomlContent)
+ {
+ if (tomlContent == null) throw new ArgumentNullException(nameof(tomlContent));
+
+ Dictionary> sections;
+ try
+ {
+ sections = TomlParser.Parse(tomlContent);
+ }
+ catch (FormatException ex)
+ {
+ throw new AdbcException(
+ "Invalid TOML driver manifest: " + ex.Message,
+ AdbcStatusCode.InvalidArgument,
+ ex);
+ }
+
+ Dictionary root = sections.TryGetValue("", out Dictionary? r)
+ ? r
+ : new Dictionary();
+
+ 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? driverSection))
+ {
+ entrypoint = ReadOptionalString(driverSection, "entrypoint");
+ }
+
+ string libraryPath = ResolveLibraryPath(sections, driverSection);
+
+ return new DriverManifest(
+ manifestVersion,
+ name,
+ version,
+ publisher,
+ license,
+ source,
+ entrypoint,
+ libraryPath);
+ }
+
+ ///
+ /// Parses a driver manifest from a file path.
+ ///
+ 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 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 section, string key)
+ {
+ if (section.TryGetValue(key, out object? obj) && obj is string s)
+ {
+ return s;
+ }
+ return null;
+ }
+
+ ///
+ /// Resolves the library path from the manifest. Supports the two spec forms:
+ /// [Driver].shared = "..." (single string) and
+ /// [Driver.shared] table keyed by platform tuple.
+ ///
+ private static string ResolveLibraryPath(
+ Dictionary> sections,
+ Dictionary? 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? 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 tuples = new List(platformTable.Count);
+ foreach (KeyValuePair 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);
+ }
+
+ ///
+ /// Returns the platform tuple identifying the current OS and architecture,
+ /// matching the format used by the ADBC driver-manifest spec
+ /// (e.g. windows_amd64, linux_arm64, macos_amd64).
+ ///
+ 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 47e51c74e8..870bbb8aeb 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
@@ -131,12 +131,6 @@ public static ConnectionProfile LoadFromContent(string tomlContent)
driverName = driverStr;
}
- string? driverTypeName = null;
- if (root.TryGetValue("driver_type", out object? driverTypeObj) && driverTypeObj is string driverTypeStr)
- {
- driverTypeName = driverTypeStr;
- }
-
Dictionary stringOpts = new Dictionary(StringComparer.Ordinal);
Dictionary intOpts = new Dictionary(StringComparer.Ordinal);
Dictionary doubleOpts = new Dictionary(StringComparer.Ordinal);
@@ -173,7 +167,7 @@ public static ConnectionProfile LoadFromContent(string tomlContent)
}
}
- return new ConnectionProfile(driverName, driverTypeName, stringOpts, intOpts, doubleOpts);
+ return new ConnectionProfile(driverName, stringOpts, intOpts, doubleOpts);
}
///
diff --git a/csharp/src/Apache.Arrow.Adbc/readme.md b/csharp/src/Apache.Arrow.Adbc/readme.md
index 75f956ccb0..3b4b14a2fa 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 4c72c40cbf..19c4ef1a5e 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 @@ public class ColocatedManifestTests : IDisposable
return (dllPath, tomlPath);
}
+ ///
+ /// Scheme prefix this test process must use when selecting managed drivers
+ /// (matches the runtime hosting the test).
+ ///
+ private static string ManagedScheme =>
+#if NETFRAMEWORK
+ "netfx:";
+#else
+ "dotnet:";
+#endif
+
///
/// Creates test files where the manifest uses a relative path to a real assembly.
///
@@ -98,12 +109,12 @@ public class ColocatedManifestTests : IDisposable
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 @@ public void LoadDriver_WithColocatedManifest_LoadsFromManifest()
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 @@ public void LoadDriver_ManifestCanOverrideDriverPath()
}
[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);
}
+ ///
+ /// Like but lets the caller
+ /// control the manifest's [Driver].entrypoint value -- useful for
+ /// tests that verify caller-supplied entrypoint overrides win.
+ ///
+ 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 @@ public void LoadDriver_RelativePathInManifest_ResolvedCorrectly()
_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 @@ public void LoadDriver_DifferentExtensions_AllDetectManifest()
_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 @@ public void LoadManagedDriver_WithColocatedManifest_LoadsDirectly()
// 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 0000000000..db1c51938f
--- /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
+{
+ ///
+ /// Tests for . These cover the format documented
+ /// in docs/source/format/driver_manifests.rst: the
+ /// manifest_version, the optional metadata fields, and the two
+ /// shapes of [Driver.shared] (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.
+ ///
+ [Collection(DriverManagerSecurityCollection.Name)]
+ public class DriverManifestTests : IDisposable
+ {
+ private readonly List _tempDirs = new List();
+
+ 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(
+ () => 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(
+ () => 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(
+ () => 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(
+ () => 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(
+ () => 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(
+ () => 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 0000000000..aafd99e6bb
--- /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
+{
+ ///
+ /// Tests for the entrypoint-scheme dispatch in :
+ /// dotnet: and netfx: prefixes select the managed loader and
+ /// fail closed when the host process runs on the wrong .NET runtime.
+ ///
+ [Collection(DriverManagerSecurityCollection.Name)]
+ public class EntrypointSchemeTests : IDisposable
+ {
+ private readonly List _tempDirs = new List();
+
+ public void Dispose()
+ {
+ foreach (string d in _tempDirs)
+ {
+ try { if (Directory.Exists(d)) Directory.Delete(d, true); } catch { }
+ }
+ }
+
+ /// The "other" managed scheme for this test process (the one the host can't load).
+ private static string ForeignScheme =>
+#if NETFRAMEWORK
+ "dotnet:";
+#else
+ "netfx:";
+#endif
+
+ /// The matching managed scheme for this test process.
+ 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(
+ () => 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(
+ () => 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 a6a0f35dcd..c811b47ec1 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 @@ public void ResolveEnvVars_ExpandsEnvVarValues()
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 @@ public void ResolveEnvVars_ExpandsEnvVarValues()
}
[Fact]
- public void ResolveEnvVars_NoEnvVarValues_ReturnsSameProfile()
+ public void ResolveEnvVars_NoPlaceholders_ReturnsSameValues()
{
const string toml = @"
version = 1
@@ -320,6 +320,101 @@ public void ResolveEnvVars_NoEnvVarValues_ReturnsSameProfile()
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://user@prod.example.com: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 @@ public void ParseProfile_FileNotFound_ThrowsIOException()
}
// -----------------------------------------------------------------------
- // 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 @@ public void ResolveEnvVars_MissingEnvVar_ThrowsAdbcException()
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(() => 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(() => 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(() => profile.ResolveEnvVars());
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
}
// -----------------------------------------------------------------------
@@ -606,34 +753,6 @@ public void FilesystemProfileProvider_AbsoluteTomlPath_LoadsDirectly()
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 @@ public void BuildStringOptions_NoOptions_ReturnsEmptyDictionary()
// -----------------------------------------------------------------------
// 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.
// -----------------------------------------------------------------------
+ ///
+ /// 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.
+ ///
+ 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 parameters = (IReadOnlyDictionary)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 @@ public void OpenDatabaseFromProfile_NullProfile_ThrowsArgumentNullException()
}
[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 @@ public void ParseProfile_DuplicateOptionKey_LastValueWins()
}
// -----------------------------------------------------------------------
- // 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 @@ public void ResolveEnvVars_DriverTypeNameIsPreserved()
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 @@ public void OpenDatabaseFromProfile_UnknownOptionsPassThroughToDriver()
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 parameters = (IReadOnlyDictionary)paramsProp!.GetValue(db)!;
@@ -1051,10 +1181,10 @@ public void OpenDatabaseFromProfile_WithExplicitOptions_MergesCorrectly()
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 @@ public void OpenDatabaseFromProfile_WithExplicitOptions_MergesCorrectly()
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 parameters = (IReadOnlyDictionary)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 @@ public void OpenDatabaseFromProfile_NullExplicitOptions_UsesOnlyProfile()
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 parameters = (IReadOnlyDictionary)paramsProp!.GetValue(db)!;