From 4e7b4e75a54ab1269ee6eaf1928180f4449f7257 Mon Sep 17 00:00:00 2001
From: David Coe <>
Date: Wed, 11 Mar 2026 10:43:38 -0400
Subject: [PATCH 1/5] initial check-in
---
.../Apache.Arrow.Adbc.csproj | 5 +
.../DriverManager/AdbcDriverManager.cs | 736 +++++++++++++
.../DriverManager/AdbcLoadFlags.cs | 63 ++
.../FilesystemProfileProvider.cs | 137 +++
.../DriverManager/IConnectionProfile.cs | 72 ++
.../IConnectionProfileProvider.cs | 44 +
.../DriverManager/TomlConnectionProfile.cs | 225 ++++
.../DriverManager/TomlParser.cs | 157 +++
.../Apache.Arrow.Adbc/DriverManager/readme.md | 51 +
.../DriverManager/ColocatedManifestTests.cs | 255 +++++
.../DriverManager/FakeAdbcDriver.cs | 51 +
.../TomlConnectionProfileTests.cs | 993 ++++++++++++++++++
12 files changed, 2789 insertions(+)
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcLoadFlags.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfile.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfileProvider.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
create mode 100644 csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
create mode 100644 csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/FakeAdbcDriver.cs
create mode 100644 csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
diff --git a/csharp/src/Apache.Arrow.Adbc/Apache.Arrow.Adbc.csproj b/csharp/src/Apache.Arrow.Adbc/Apache.Arrow.Adbc.csproj
index 1a6e3ec653..cfe7084b0e 100644
--- a/csharp/src/Apache.Arrow.Adbc/Apache.Arrow.Adbc.csproj
+++ b/csharp/src/Apache.Arrow.Adbc/Apache.Arrow.Adbc.csproj
@@ -16,6 +16,11 @@
+
+ \
+ PreserveNewest
+ true
+
true
\
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
new file mode 100644
index 0000000000..d24759484c
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -0,0 +1,736 @@
+/*
+ * 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.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using Apache.Arrow.Adbc.C;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Provides methods to locate and load ADBC drivers, optionally using TOML manifest
+ /// files and connection profiles.
+ /// Mirrors the free functions declared in adbc_driver_manager.h:
+ /// AdbcLoadDriver and AdbcFindLoadDriver.
+ ///
+ public static class AdbcDriverManager
+ {
+ ///
+ /// The environment variable that specifies additional driver search paths.
+ ///
+ public const string DriverPathEnvVar = "ADBC_DRIVER_PATH";
+
+ private static readonly string[] s_nativeExtensions = { ".so", ".dll", ".dylib" };
+
+ // -----------------------------------------------------------------------
+ // AdbcLoadDriver – load a driver directly from an absolute or relative path
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Loads an ADBC driver from an explicit file path.
+ /// Mirrors AdbcLoadDriver in adbc_driver_manager.h.
+ ///
+ ///
+ ///
+ /// If a TOML manifest file with the same base name exists in the same directory
+ /// as the driver file, it will be automatically detected and used. For example,
+ /// loading libadbc_driver_snowflake.dll will automatically use
+ /// libadbc_driver_snowflake.toml if present.
+ ///
+ ///
+ /// The co-located manifest can specify default options, override the driver path,
+ /// or provide additional metadata. The parameter
+ /// always takes precedence over any entrypoint derived from the manifest or filename.
+ ///
+ ///
+ ///
+ /// The absolute or relative path to the driver shared library.
+ ///
+ ///
+ /// The symbol name of the driver initialisation function. When null the
+ /// driver manager derives a candidate entrypoint from the file name (see
+ /// ), falling back to AdbcDriverInit.
+ ///
+ /// The loaded .
+ public static AdbcDriver LoadDriver(string driverPath, string? entrypoint = null)
+ {
+ if (string.IsNullOrEmpty(driverPath))
+ throw new ArgumentException("Driver path must not be null or empty.", nameof(driverPath));
+
+ // Check for co-located TOML manifest
+ string? colocatedManifest = TryFindColocatedManifest(driverPath);
+ if (colocatedManifest != null)
+ {
+ return LoadFromManifest(colocatedManifest, entrypoint);
+ }
+
+ string resolvedEntrypoint = entrypoint ?? DeriveEntrypoint(driverPath);
+ return CAdbcDriverImporter.Load(driverPath, resolvedEntrypoint);
+ }
+
+ // -----------------------------------------------------------------------
+ // AdbcFindLoadDriver – locate a driver by name using configurable search rules
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Searches for an ADBC driver by name and loads it.
+ /// Mirrors AdbcFindLoadDriver in adbc_driver_manager.h.
+ ///
+ ///
+ ///
+ /// If is an absolute path:
+ ///
+ /// - A .toml extension triggers manifest loading.
+ /// - Any other extension loads the path as a shared library directly.
+ ///
+ ///
+ ///
+ /// If is a bare name (no extension, not absolute):
+ ///
+ /// - Each configured search directory is checked in order.
+ /// -
+ /// For each directory, <dir>/<name>.toml is attempted first;
+ /// if found the manifest is parsed and the driver loaded.
+ ///
+ /// -
+ /// Then <dir>/<name>.<ext> for each platform extension
+ /// (.dll, .so, .dylib) is attempted.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// A driver identifier: an absolute path (with or without extension) or a bare
+ /// driver name to be resolved by directory search.
+ ///
+ ///
+ /// The symbol name of the driver initialisation function, or null to
+ /// derive it automatically.
+ ///
+ ///
+ /// Flags controlling which directories are searched.
+ ///
+ ///
+ /// An optional OS path-list-separator-delimited list of extra directories to
+ /// search before the standard ones, or null.
+ ///
+ /// The loaded .
+ /// Thrown when the driver cannot be found or loaded.
+ public static AdbcDriver FindLoadDriver(
+ string driverName,
+ string? entrypoint = null,
+ AdbcLoadFlags loadOptions = AdbcLoadFlags.Default,
+ string? additionalSearchPathList = null)
+ {
+ if (string.IsNullOrEmpty(driverName))
+ throw new ArgumentException("Driver name must not be null or empty.", nameof(driverName));
+
+ // Absolute path – load directly.
+ if (Path.IsPathRooted(driverName))
+ return LoadFromAbsolutePath(driverName, entrypoint);
+
+ // Bare name with an extension but not rooted – relative path case.
+ string ext = Path.GetExtension(driverName);
+ if (!string.IsNullOrEmpty(ext) && loadOptions.HasFlag(AdbcLoadFlags.AllowRelativePaths))
+ {
+ if (string.Equals(ext, ".toml", StringComparison.OrdinalIgnoreCase))
+ return LoadFromManifest(driverName, entrypoint);
+ return LoadDriver(driverName, entrypoint);
+ }
+
+ // Bareword – search configured directories.
+ foreach (string dir in BuildSearchDirectories(loadOptions, additionalSearchPathList))
+ {
+ AdbcDriver? found = TryLoadFromDirectory(dir, driverName, entrypoint);
+ if (found != null)
+ return found;
+ }
+
+ throw new AdbcException(
+ $"Could not find ADBC driver '{driverName}' in any configured search path.",
+ AdbcStatusCode.NotFound);
+ }
+
+ // -----------------------------------------------------------------------
+ // LoadDriverFromProfile – load a driver as specified by a connection profile
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Loads an ADBC driver as specified by an .
+ ///
+ /// The profile that specifies the driver to load.
+ ///
+ /// An optional override for the driver entrypoint symbol. When null the
+ /// entrypoint is derived from the driver name in the profile.
+ ///
+ /// Flags controlling directory search behaviour.
+ ///
+ /// An optional additional search path list, overriding any search paths that may
+ /// be embedded in the profile.
+ ///
+ /// The loaded .
+ ///
+ /// Thrown when the profile does not specify a driver, or the driver cannot be found.
+ ///
+ public static AdbcDriver LoadDriverFromProfile(
+ IConnectionProfile profile,
+ string? entrypoint = null,
+ AdbcLoadFlags loadOptions = AdbcLoadFlags.Default,
+ string? additionalSearchPathList = null)
+ {
+ if (profile == null) throw new ArgumentNullException(nameof(profile));
+
+ if (string.IsNullOrEmpty(profile.DriverName))
+ throw new AdbcException(
+ "The connection profile does not specify a driver name.",
+ AdbcStatusCode.InvalidArgument);
+
+ return FindLoadDriver(profile.DriverName!, entrypoint, loadOptions, additionalSearchPathList);
+ }
+
+ // -----------------------------------------------------------------------
+ // LoadManagedDriver – load a managed .NET AdbcDriver via reflection
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Loads a managed (pure .NET) ADBC driver from a .NET assembly using reflection.
+ ///
+ ///
+ /// Use this instead of when the driver is a .NET assembly
+ /// (e.g. Apache.Arrow.Adbc.Drivers.BigQuery.dll) rather than a native shared
+ /// library. The assembly is loaded via and the
+ /// specified type is instantiated with a public parameterless constructor.
+ ///
+ ///
+ /// The absolute or relative path to the .NET assembly containing the driver.
+ ///
+ ///
+ /// The fully-qualified name of a concrete class that derives from
+ /// and has a public parameterless constructor
+ /// (e.g. Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver).
+ ///
+ /// An instance of the specified subclass.
+ ///
+ /// Thrown when the assembly cannot be loaded, the type is not found, or the type
+ /// does not derive from .
+ ///
+ public static AdbcDriver LoadManagedDriver(string assemblyPath, string typeName)
+ {
+ if (string.IsNullOrEmpty(assemblyPath))
+ throw new ArgumentException("Assembly path must not be null or empty.", nameof(assemblyPath));
+ if (string.IsNullOrEmpty(typeName))
+ throw new ArgumentException("Type name must not be null or empty.", nameof(typeName));
+
+ Assembly assembly;
+ try
+ {
+ assembly = Assembly.LoadFrom(assemblyPath);
+ }
+ catch (Exception ex)
+ {
+ throw new AdbcException(
+ $"Failed to load managed driver assembly '{assemblyPath}': {ex.Message}",
+ AdbcStatusCode.IOError);
+ }
+
+ Type? driverType = assembly.GetType(typeName, throwOnError: false);
+ if (driverType == null)
+ throw new AdbcException(
+ $"Type '{typeName}' was not found in assembly '{assemblyPath}'.",
+ AdbcStatusCode.NotFound);
+
+ if (!typeof(AdbcDriver).IsAssignableFrom(driverType))
+ throw new AdbcException(
+ $"Type '{typeName}' does not derive from {nameof(AdbcDriver)}.",
+ AdbcStatusCode.InvalidArgument);
+
+ object? instance;
+ try
+ {
+ instance = Activator.CreateInstance(driverType);
+ }
+ catch (Exception ex)
+ {
+ throw new AdbcException(
+ $"Failed to instantiate driver type '{typeName}': {ex.Message}",
+ AdbcStatusCode.InternalError);
+ }
+
+ if (instance == null)
+ throw new AdbcException(
+ $"Activator returned null for driver type '{typeName}'.",
+ AdbcStatusCode.InternalError);
+
+ return (AdbcDriver)instance;
+ }
+
+ // -----------------------------------------------------------------------
+ // OpenDatabaseFromProfile – load driver + open database in one step
+ // -----------------------------------------------------------------------
+
+ ///
+ /// 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
+ /// .
+ ///
+ ///
+ /// 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 .
+ ///
+ ///
+ /// Call on the profile before
+ /// passing it here if you want env_var(NAME) values expanded first.
+ ///
+ ///
+ /// The connection profile specifying the driver and options.
+ /// Flags controlling directory search for native drivers.
+ ///
+ /// An optional extra search path list for native driver discovery.
+ ///
+ /// An open .
+ public static AdbcDatabase OpenDatabaseFromProfile(
+ IConnectionProfile profile,
+ AdbcLoadFlags loadOptions = AdbcLoadFlags.Default,
+ string? additionalSearchPathList = null)
+ {
+ return OpenDatabaseFromProfile(profile, null, loadOptions, additionalSearchPathList);
+ }
+
+ ///
+ /// Loads the driver specified by and opens a database,
+ /// merging profile options with explicitly provided options.
+ ///
+ ///
+ ///
+ /// This overload allows specifying additional options that will be merged with the
+ /// profile options. Per the ADBC specification, profile options are applied first,
+ /// then explicit options. If the same key appears in both the profile and explicit
+ /// 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.
+ ///
+ ///
+ /// Otherwise the driver is loaded as a native shared library via
+ /// .
+ ///
+ ///
+ /// All options are merged into a single string → string 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 connection profile specifying the driver and options.
+ ///
+ /// Additional options to merge with profile options. Explicit options override profile
+ /// options for the same key. May be null or empty.
+ ///
+ /// Flags controlling directory search for native drivers.
+ ///
+ /// An optional extra search path list for native driver discovery.
+ ///
+ /// An open .
+ public static AdbcDatabase OpenDatabaseFromProfile(
+ IConnectionProfile profile,
+ IReadOnlyDictionary? explicitOptions,
+ AdbcLoadFlags loadOptions = AdbcLoadFlags.Default,
+ string? additionalSearchPathList = null)
+ {
+ if (profile == null) throw new ArgumentNullException(nameof(profile));
+
+ AdbcDriver driver;
+
+ if (!string.IsNullOrEmpty(profile.DriverTypeName))
+ {
+ // 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!);
+ }
+ else
+ {
+ // Native shared-library path
+ driver = LoadDriverFromProfile(profile, null, loadOptions, additionalSearchPathList);
+ }
+
+ return driver.Open(BuildStringOptions(profile, explicitOptions));
+ }
+
+ ///
+ /// Merges all options from a profile into a flat string → string dictionary
+ /// suitable for passing to .
+ /// Integer and double values are formatted with .
+ /// String options take precedence if the same key appears in multiple option sets.
+ ///
+ public static IReadOnlyDictionary BuildStringOptions(IConnectionProfile profile)
+ {
+ return BuildStringOptions(profile, null);
+ }
+
+ ///
+ /// Merges options from a profile with explicitly provided options into a flat
+ /// string → string dictionary suitable for passing to
+ /// .
+ ///
+ ///
+ ///
+ /// Options are merged in the following order (later values override earlier ones):
+ ///
+ /// - Profile integer options (formatted as strings)
+ /// - Profile double options (formatted as strings)
+ /// - Profile string options
+ /// - Explicit options from
+ ///
+ ///
+ ///
+ /// This ordering ensures that explicit options always take precedence over profile
+ /// options, as required by the ADBC specification.
+ ///
+ ///
+ /// The connection profile containing base options.
+ ///
+ /// Additional options that override profile options. May be null or empty.
+ ///
+ /// A merged dictionary of all options.
+ public static IReadOnlyDictionary BuildStringOptions(
+ IConnectionProfile profile,
+ IReadOnlyDictionary? explicitOptions)
+ {
+ if (profile == null) throw new ArgumentNullException(nameof(profile));
+
+ var merged = new Dictionary(StringComparer.Ordinal);
+
+ // Profile options first (in order: int, double, string)
+ foreach (var kv in profile.IntOptions)
+ merged[kv.Key] = kv.Value.ToString(CultureInfo.InvariantCulture);
+
+ foreach (var kv in profile.DoubleOptions)
+ merged[kv.Key] = kv.Value.ToString(CultureInfo.InvariantCulture);
+
+ foreach (var kv in profile.StringOptions)
+ merged[kv.Key] = kv.Value;
+
+ // Explicit options last – they override profile options
+ if (explicitOptions != null)
+ {
+ foreach (var kv in explicitOptions)
+ merged[kv.Key] = kv.Value;
+ }
+
+ return merged;
+ }
+
+ // -----------------------------------------------------------------------
+ // Helpers – derive entrypoint, search directories, manifest loading
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Derives a candidate entrypoint symbol name from a driver file path.
+ ///
+ ///
+ /// The convention used by the Go-based ADBC drivers is to strip leading
+ /// lib and any extension, then append Init. For example,
+ /// libadbc_driver_postgresql.so yields AdbcDriverPostgresqlInit.
+ /// Falls back to AdbcDriverInit when no better candidate can be formed.
+ ///
+ public static string DeriveEntrypoint(string driverPath)
+ {
+ string baseName = Path.GetFileNameWithoutExtension(driverPath);
+
+ // Strip leading "lib" prefix common on POSIX platforms.
+ if (baseName.StartsWith("lib", StringComparison.OrdinalIgnoreCase))
+ baseName = baseName.Substring(3);
+
+ // Strip "adbc_driver_" or "adbc_" prefix to get a shorter stem.
+ const string adbcDriverPrefix = "adbc_driver_";
+ const string adbcPrefix = "adbc_";
+
+ if (baseName.StartsWith(adbcDriverPrefix, StringComparison.OrdinalIgnoreCase))
+ baseName = baseName.Substring(adbcDriverPrefix.Length);
+ else if (baseName.StartsWith(adbcPrefix, StringComparison.OrdinalIgnoreCase))
+ baseName = baseName.Substring(adbcPrefix.Length);
+
+ if (string.IsNullOrEmpty(baseName))
+ return "AdbcDriverInit";
+
+ // Convert snake_case to PascalCase.
+ string pascal = ToPascalCase(baseName);
+ return $"AdbcDriver{pascal}Init";
+ }
+
+ private static AdbcDriver LoadFromAbsolutePath(string path, string? entrypoint)
+ {
+ string ext = Path.GetExtension(path);
+ if (string.Equals(ext, ".toml", StringComparison.OrdinalIgnoreCase))
+ return LoadFromManifest(path, entrypoint);
+
+ // Check for co-located TOML manifest (e.g., libadbc_driver_snowflake.toml
+ // alongside libadbc_driver_snowflake.dll)
+ string? colocatedManifest = TryFindColocatedManifest(path);
+ if (colocatedManifest != null)
+ {
+ return LoadFromManifest(colocatedManifest, entrypoint);
+ }
+
+ return LoadDriver(path, entrypoint);
+ }
+
+ ///
+ /// Checks for a TOML manifest file co-located with a driver file.
+ ///
+ ///
+ ///
+ /// Given a driver path like C:\drivers\libadbc_driver_snowflake.dll,
+ /// this checks for C:\drivers\libadbc_driver_snowflake.toml.
+ ///
+ ///
+ /// 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
+ /// .
+ ///
+ ///
+ /// The path to the driver file.
+ ///
+ /// The full path to the co-located manifest if found; otherwise null.
+ ///
+ private static string? TryFindColocatedManifest(string driverPath)
+ {
+ try
+ {
+ string fullPath = Path.GetFullPath(driverPath);
+ string? directory = Path.GetDirectoryName(fullPath);
+ if (string.IsNullOrEmpty(directory))
+ return null;
+
+ string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fullPath);
+ string manifestPath = Path.Combine(directory, fileNameWithoutExt + ".toml");
+
+ return File.Exists(manifestPath) ? manifestPath : null;
+ }
+ catch
+ {
+ // If path resolution fails, just return null
+ return null;
+ }
+ }
+
+ private static AdbcDriver LoadFromManifest(string manifestPath, string? entrypoint)
+ {
+ if (!File.Exists(manifestPath))
+ throw new AdbcException(
+ $"Driver manifest file not found: '{manifestPath}'.",
+ AdbcStatusCode.NotFound);
+
+ TomlConnectionProfile manifest = TomlConnectionProfile.FromFile(manifestPath);
+
+ if (string.IsNullOrEmpty(manifest.DriverName))
+ throw new AdbcException(
+ $"Driver manifest '{manifestPath}' does not specify a 'driver' field.",
+ AdbcStatusCode.InvalidArgument);
+
+ // Check if this is a managed driver
+ if (!string.IsNullOrEmpty(manifest.DriverTypeName))
+ {
+ // Managed .NET driver - resolve relative path relative to manifest directory
+ string driverPath = manifest.DriverName!;
+ if (!Path.IsPathRooted(driverPath))
+ {
+ string? manifestDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+ if (!string.IsNullOrEmpty(manifestDir))
+ driverPath = Path.Combine(manifestDir, driverPath);
+ }
+ return LoadManagedDriver(driverPath, manifest.DriverTypeName!);
+ }
+
+ // Native driver - resolve entrypoint and path
+ string resolvedEntrypoint = entrypoint ?? DeriveEntrypoint(manifest.DriverName!);
+
+ // Resolve relative driver path relative to the manifest directory.
+ string resolvedDriverPath = manifest.DriverName!;
+ if (!Path.IsPathRooted(resolvedDriverPath))
+ {
+ string? manifestDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+ if (!string.IsNullOrEmpty(manifestDir))
+ resolvedDriverPath = Path.Combine(manifestDir, resolvedDriverPath);
+ }
+
+ return CAdbcDriverImporter.Load(resolvedDriverPath, resolvedEntrypoint);
+ }
+
+ private static AdbcDriver? TryLoadFromDirectory(string dir, string driverName, string? entrypoint)
+ {
+ // 1. Try manifest
+ string manifestPath = Path.Combine(dir, driverName + ".toml");
+ if (File.Exists(manifestPath))
+ {
+ try { return LoadFromManifest(manifestPath, entrypoint); }
+ catch (AdbcException) { throw; } // surface manifest parse errors
+ }
+
+ // 2. Try native library extensions
+ foreach (string nativeExt in GetPlatformExtensions())
+ {
+ string libPath = Path.Combine(dir, driverName + nativeExt);
+ if (File.Exists(libPath))
+ {
+ try
+ {
+ string ep = entrypoint ?? DeriveEntrypoint(libPath);
+ return CAdbcDriverImporter.Load(libPath, ep);
+ }
+ catch (AdbcException) { /* try next extension */ }
+ }
+ }
+
+ return null;
+ }
+
+ private static IEnumerable BuildSearchDirectories(
+ AdbcLoadFlags flags,
+ string? additionalSearchPathList)
+ {
+ // 1. Caller-supplied additional paths
+ if (!string.IsNullOrEmpty(additionalSearchPathList))
+ {
+ foreach (string p in SplitPathList(additionalSearchPathList!))
+ yield return p;
+ }
+
+ // 2. ADBC_DRIVER_PATH environment variable
+ if (flags.HasFlag(AdbcLoadFlags.SearchEnv))
+ {
+ string? envPath = Environment.GetEnvironmentVariable(DriverPathEnvVar);
+ if (!string.IsNullOrEmpty(envPath))
+ {
+ foreach (string p in SplitPathList(envPath!))
+ yield return p;
+ }
+ }
+
+ // 3. User-level directory
+ if (flags.HasFlag(AdbcLoadFlags.SearchUser))
+ {
+ string userDir = GetUserDriverDirectory();
+ if (!string.IsNullOrEmpty(userDir))
+ yield return userDir;
+ }
+
+ // 4. System-level directory
+ if (flags.HasFlag(AdbcLoadFlags.SearchSystem))
+ {
+ string sysDir = GetSystemDriverDirectory();
+ if (!string.IsNullOrEmpty(sysDir))
+ yield return sysDir;
+ }
+ }
+
+ private static string[] GetPlatformExtensions()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return new[] { ".dll" };
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return new[] { ".dylib", ".so" };
+ return new[] { ".so", ".dylib" };
+ }
+
+ private static string GetUserDriverDirectory()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ string? appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ return string.IsNullOrEmpty(appData) ? string.Empty : Path.Combine(appData, "adbc", "drivers");
+ }
+ else
+ {
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ return string.IsNullOrEmpty(home) ? string.Empty : Path.Combine(home, ".config", "adbc", "drivers");
+ }
+ }
+
+ private static string GetSystemDriverDirectory()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ string? sysRoot = Environment.GetEnvironmentVariable("ProgramData");
+ return string.IsNullOrEmpty(sysRoot) ? string.Empty : Path.Combine(sysRoot, "adbc", "drivers");
+ }
+ else
+ {
+ return "/usr/lib/adbc";
+ }
+ }
+
+ private static string[] SplitPathList(string pathList) =>
+ pathList.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
+
+ private static string ToPascalCase(string snake)
+ {
+ var sb = new System.Text.StringBuilder();
+ bool upperNext = true;
+ foreach (char c in snake)
+ {
+ if (c == '_')
+ {
+ upperNext = true;
+ }
+ else if (upperNext)
+ {
+ sb.Append(char.ToUpperInvariant(c));
+ upperNext = false;
+ }
+ else
+ {
+ sb.Append(char.ToLowerInvariant(c));
+ }
+ }
+ return sb.ToString();
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcLoadFlags.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcLoadFlags.cs
new file mode 100644
index 0000000000..9812b2e00e
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcLoadFlags.cs
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Flags to control how the ADBC driver manager searches for and loads drivers.
+ /// Mirrors the AdbcLoadFlags type and ADBC_LOAD_FLAG_* constants
+ /// defined in adbc_driver_manager.h.
+ ///
+ [Flags]
+ public enum AdbcLoadFlags : uint
+ {
+ /// No search flags set.
+ None = 0,
+
+ ///
+ /// Search the ADBC_DRIVER_PATH environment variable for drivers.
+ /// Corresponds to ADBC_LOAD_FLAG_SEARCH_ENV.
+ ///
+ SearchEnv = 1,
+
+ ///
+ /// Search the user-level configuration directory for drivers.
+ /// Corresponds to ADBC_LOAD_FLAG_SEARCH_USER.
+ ///
+ SearchUser = 2,
+
+ ///
+ /// Search the system-level configuration directory for drivers.
+ /// Corresponds to ADBC_LOAD_FLAG_SEARCH_SYSTEM.
+ ///
+ SearchSystem = 4,
+
+ ///
+ /// Allow loading drivers from relative paths.
+ /// Corresponds to ADBC_LOAD_FLAG_ALLOW_RELATIVE_PATHS.
+ ///
+ AllowRelativePaths = 8,
+
+ ///
+ /// Default flags: search env, user, system, and allow relative paths.
+ /// Corresponds to ADBC_LOAD_FLAG_DEFAULT.
+ ///
+ Default = SearchEnv | SearchUser | SearchSystem | AllowRelativePaths,
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
new file mode 100644
index 0000000000..0e92dd290c
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// The default filesystem-based .
+ /// Mirrors AdbcProfileProviderFilesystem in adbc_driver_manager.h.
+ ///
+ ///
+ /// Profiles are searched in the following order:
+ ///
+ /// -
+ ///
+ /// If is an absolute path, it is used directly
+ /// (with a .toml extension appended if none is present).
+ ///
+ ///
+ /// -
+ ///
+ /// The directories supplied in additionalSearchPathList (delimited by the
+ /// OS path-list separator, e.g. ; on Windows, : on POSIX).
+ ///
+ ///
+ /// -
+ ///
+ /// The directories listed in the ADBC_PROFILE_PATH environment variable.
+ ///
+ ///
+ /// -
+ /// The user-level configuration directory (e.g. ~/.config/adbc/profiles).
+ ///
+ ///
+ ///
+ public sealed class FilesystemProfileProvider : IConnectionProfileProvider
+ {
+ ///
+ /// The environment variable that specifies additional profile search paths.
+ ///
+ public const string ProfilePathEnvVar = "ADBC_PROFILE_PATH";
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public FilesystemProfileProvider() { }
+
+ ///
+ public IConnectionProfile? GetProfile(string profileName, string? additionalSearchPathList = null)
+ {
+ if (profileName == null) throw new ArgumentNullException(nameof(profileName));
+
+ // If already an absolute path, load directly.
+ if (Path.IsPathRooted(profileName))
+ {
+ string candidate = EnsureTomlExtension(profileName);
+ if (File.Exists(candidate))
+ return TomlConnectionProfile.FromFile(candidate);
+ return null;
+ }
+
+ // Build the ordered search directories.
+ var dirs = new List();
+
+ if (!string.IsNullOrEmpty(additionalSearchPathList))
+ dirs.AddRange(SplitPathList(additionalSearchPathList!));
+
+ string? profilePathEnv = Environment.GetEnvironmentVariable(ProfilePathEnvVar);
+ if (!string.IsNullOrEmpty(profilePathEnv))
+ dirs.AddRange(SplitPathList(profilePathEnv!));
+
+ string userDir = GetUserProfileDirectory();
+ if (!string.IsNullOrEmpty(userDir))
+ dirs.Add(userDir);
+
+ string fileName = EnsureTomlExtension(profileName);
+
+ foreach (string dir in dirs)
+ {
+ string candidate = Path.Combine(dir, fileName);
+ if (File.Exists(candidate))
+ return TomlConnectionProfile.FromFile(candidate);
+ }
+
+ return null;
+ }
+
+ private static string EnsureTomlExtension(string name)
+ {
+ string ext = Path.GetExtension(name);
+ return string.IsNullOrEmpty(ext)
+ ? name + ".toml"
+ : name;
+ }
+
+ private static string[] SplitPathList(string pathList) =>
+ pathList.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
+
+ private static string GetUserProfileDirectory()
+ {
+ // ~/.config/adbc/profiles (Linux / macOS)
+ // %APPDATA%\adbc\profiles (Windows)
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ string? appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ return string.IsNullOrEmpty(appData)
+ ? string.Empty
+ : Path.Combine(appData, "adbc", "profiles");
+ }
+ else
+ {
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ return string.IsNullOrEmpty(home)
+ ? string.Empty
+ : Path.Combine(home, ".config", "adbc", "profiles");
+ }
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfile.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfile.cs
new file mode 100644
index 0000000000..e48680aba3
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfile.cs
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Represents an ADBC connection profile, which specifies a driver and a set of
+ /// options to apply when initializing a database connection.
+ /// Mirrors the AdbcConnectionProfile struct defined in adbc_driver_manager.h.
+ ///
+ ///
+ /// The profile specifies a driver (optionally) and key/value options of three types:
+ /// string, 64-bit integer, and double. String option values of the form
+ /// env_var(ENV_VAR_NAME) are expanded from the named environment variable
+ /// before being applied to the database.
+ ///
+ public interface IConnectionProfile : IDisposable
+ {
+ ///
+ /// Gets the name of the driver specified by this profile, or null if the
+ /// profile does not specify a driver.
+ /// 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.
+ ///
+ 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.
+ /// When set, is used instead of
+ /// the native NativeLibrary path.
+ ///
+ ///
+ /// Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver
+ ///
+ 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 applied.
+ ///
+ IReadOnlyDictionary StringOptions { get; }
+
+ ///
+ /// Gets the 64-bit integer options specified by this profile.
+ ///
+ IReadOnlyDictionary IntOptions { get; }
+
+ ///
+ /// Gets the double-precision floating-point options specified by this profile.
+ ///
+ IReadOnlyDictionary DoubleOptions { get; }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfileProvider.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfileProvider.cs
new file mode 100644
index 0000000000..e3cf1b4811
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/IConnectionProfileProvider.cs
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Provides instances by name.
+ /// Mirrors the AdbcConnectionProfileProvider function type defined in
+ /// adbc_driver_manager.h.
+ ///
+ public interface IConnectionProfileProvider
+ {
+ ///
+ /// Loads a connection profile by name.
+ ///
+ ///
+ /// The name of the profile to load. May be an absolute file path or a bare name
+ /// to be resolved by searching the configured directories.
+ ///
+ ///
+ /// An optional OS path-list-separator-delimited list of additional directories to
+ /// search for profiles, or null to use only the default locations.
+ ///
+ ///
+ /// The loaded , or null if no profile with
+ /// was found.
+ ///
+ IConnectionProfile? GetProfile(string profileName, string? additionalSearchPathList = null);
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
new file mode 100644
index 0000000000..453f9c0cde
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
@@ -0,0 +1,225 @@
+/*
+ * 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.Globalization;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// An loaded from a TOML file.
+ ///
+ ///
+ /// The expected file format is:
+ ///
+ /// version = 1
+ /// driver = "driver_name"
+ ///
+ /// [options]
+ /// option1 = "value1"
+ /// option2 = 42
+ /// option3 = 3.14
+ ///
+ /// Boolean option values are converted to the string equivalents "true" or
+ /// "false" and placed in .
+ /// Integer values are placed in and double values in
+ /// (integer values are also reflected in
+ /// ).
+ /// Values of the form env_var(ENV_VAR_NAME) are expanded from the named
+ /// environment variable when is called.
+ ///
+ public sealed class TomlConnectionProfile : IConnectionProfile
+ {
+ private const string OptionsSection = "options";
+ private const string EnvVarPrefix = "env_var(";
+
+ private readonly Dictionary _stringOptions;
+ private readonly Dictionary _intOptions;
+ private readonly Dictionary _doubleOptions;
+
+ private TomlConnectionProfile(
+ string? driverName,
+ string? driverTypeName,
+ Dictionary stringOptions,
+ Dictionary intOptions,
+ Dictionary doubleOptions)
+ {
+ DriverName = driverName;
+ DriverTypeName = driverTypeName;
+ _stringOptions = stringOptions;
+ _intOptions = intOptions;
+ _doubleOptions = doubleOptions;
+ }
+
+ ///
+ public string? DriverName { get; }
+
+ ///
+ public string? DriverTypeName { get; }
+
+ ///
+ public IReadOnlyDictionary StringOptions => _stringOptions;
+
+ ///
+ public IReadOnlyDictionary IntOptions => _intOptions;
+
+ ///
+ public IReadOnlyDictionary DoubleOptions => _doubleOptions;
+
+ ///
+ /// Loads a from the given TOML content string.
+ ///
+ /// The raw TOML text to parse.
+ /// A new .
+ ///
+ /// Thrown when the TOML content is missing a required version field or
+ /// has an unsupported version number.
+ ///
+ public static TomlConnectionProfile FromContent(string tomlContent)
+ {
+ if (tomlContent == null) throw new ArgumentNullException(nameof(tomlContent));
+
+ var sections = TomlParser.Parse(tomlContent);
+
+ var root = sections.TryGetValue("", out var r) ? r : new Dictionary();
+
+ ValidateVersion(root);
+
+ string? driverName = null;
+ if (root.TryGetValue("driver", out object? driverObj) && driverObj is string driverStr)
+ driverName = driverStr;
+
+ string? driverTypeName = null;
+ if (root.TryGetValue("driver_type", out object? driverTypeObj) && driverTypeObj is string driverTypeStr)
+ driverTypeName = driverTypeStr;
+
+ var stringOpts = new Dictionary(StringComparer.Ordinal);
+ var intOpts = new Dictionary(StringComparer.Ordinal);
+ var doubleOpts = new Dictionary(StringComparer.Ordinal);
+
+ if (sections.TryGetValue(OptionsSection, out var optSection))
+ {
+ foreach (var kv in optSection)
+ {
+ string key = kv.Key;
+ object val = kv.Value;
+
+ switch (val)
+ {
+ case long lv:
+ intOpts[key] = lv;
+ break;
+ case double dv:
+ doubleOpts[key] = dv;
+ break;
+ case bool bv:
+ stringOpts[key] = bv ? "true" : "false";
+ break;
+ default:
+ stringOpts[key] = Convert.ToString(val, CultureInfo.InvariantCulture) ?? string.Empty;
+ break;
+ }
+ }
+ }
+
+ return new TomlConnectionProfile(driverName, driverTypeName, stringOpts, intOpts, doubleOpts);
+ }
+
+ ///
+ /// Loads a from the given TOML file path.
+ ///
+ /// Absolute or relative path to the .toml file.
+ /// A new .
+ public static TomlConnectionProfile FromFile(string filePath)
+ {
+ if (filePath == null) throw new ArgumentNullException(nameof(filePath));
+ string content = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
+ return FromContent(content);
+ }
+
+ ///
+ /// Returns a new profile with any env_var(NAME) values in
+ /// replaced by the value of the corresponding
+ /// environment variable.
+ ///
+ ///
+ /// Thrown when a referenced environment variable is not set.
+ ///
+ public TomlConnectionProfile ResolveEnvVars()
+ {
+ var resolved = new Dictionary(StringComparer.Ordinal);
+ foreach (var 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;
+ }
+ }
+ return new TomlConnectionProfile(DriverName, DriverTypeName, resolved, _intOptions, _doubleOptions);
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ private static void ValidateVersion(Dictionary root)
+ {
+ if (!root.TryGetValue("version", out object? versionObj))
+ throw new AdbcException(
+ "TOML profile is missing the required 'version' field.",
+ AdbcStatusCode.InvalidArgument);
+
+ long version;
+ if (versionObj is long lv)
+ {
+ version = lv;
+ }
+ else
+ {
+ try
+ {
+ version = Convert.ToInt64(versionObj, CultureInfo.InvariantCulture);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is InvalidCastException || ex is OverflowException)
+ {
+ throw new AdbcException(
+ $"The 'version' field has an invalid value '{versionObj}'. It must be an integer.",
+ AdbcStatusCode.InvalidArgument);
+ }
+ }
+
+ if (version != 1)
+ throw new AdbcException(
+ $"Unsupported profile version '{version}'. Only version 1 is supported.",
+ AdbcStatusCode.NotImplemented);
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
new file mode 100644
index 0000000000..a78c350709
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
@@ -0,0 +1,157 @@
+/*
+ * 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.Globalization;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// A minimal TOML parser that handles the subset of TOML used by ADBC driver
+ /// manifests and connection profiles:
+ ///
+ /// - Root-level key = value assignments
+ /// - Table section headers: [section]
+ /// - String values (double-quoted), integer values, floating-point
+ /// values, and boolean values (true/false)
+ /// - Line comments beginning with #
+ ///
+ /// This parser intentionally does not support the full TOML specification.
+ /// A full-featured TOML library (e.g. Tomlyn) was considered but cannot be used here
+ /// because the assembly is strongly-named and Tomlyn does not publish a strongly-named
+ /// package that is compatible with the project's pinned dependency versions.
+ ///
+ internal static class TomlParser
+ {
+ private const string RootSection = "";
+
+ ///
+ /// Parses and returns a dictionary keyed by section name.
+ /// Root-level keys are stored under the empty string key.
+ /// Values are typed as , , ,
+ /// or .
+ ///
+ internal static Dictionary> Parse(string content)
+ {
+ if (content == null) throw new ArgumentNullException(nameof(content));
+
+ var result = new Dictionary>(StringComparer.OrdinalIgnoreCase)
+ {
+ [RootSection] = new Dictionary(StringComparer.OrdinalIgnoreCase),
+ };
+
+ string currentSection = RootSection;
+
+ foreach (string rawLine in content.Split('\n'))
+ {
+ string line = StripComment(rawLine).Trim();
+
+ if (line.Length == 0)
+ continue;
+
+ if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal))
+ {
+ currentSection = line.Substring(1, line.Length - 2).Trim();
+ if (!result.ContainsKey(currentSection))
+ result[currentSection] = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ continue;
+ }
+
+ int eqIndex = line.IndexOf('=');
+ if (eqIndex <= 0)
+ continue;
+
+ string key = line.Substring(0, eqIndex).Trim();
+ string valueRaw = line.Substring(eqIndex + 1).Trim();
+
+ object value = ParseValue(valueRaw);
+ result[currentSection][key] = value;
+ }
+
+ return result;
+ }
+
+ private static object ParseValue(string raw)
+ {
+ // Double-quoted string
+ if (raw.StartsWith("\"", StringComparison.Ordinal) && raw.EndsWith("\"", StringComparison.Ordinal) && raw.Length >= 2)
+ {
+ string inner = raw.Substring(1, raw.Length - 2);
+ return UnescapeString(inner);
+ }
+
+ // Boolean
+ if (string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase))
+ return true;
+ if (string.Equals(raw, "false", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ // Integer (try before float, since integers are a subset)
+ if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out long intValue))
+ return intValue;
+
+ // Float
+ if (double.TryParse(raw, NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double dblValue))
+ return dblValue;
+
+ // Fallback: treat as unquoted string
+ return raw;
+ }
+
+ private static string UnescapeString(string s)
+ {
+ var sb = new System.Text.StringBuilder(s.Length);
+ for (int i = 0; i < s.Length; i++)
+ {
+ if (s[i] == '\\' && i + 1 < s.Length)
+ {
+ i++;
+ switch (s[i])
+ {
+ case '"': sb.Append('"'); break;
+ case '\\': sb.Append('\\'); break;
+ case 'n': sb.Append('\n'); break;
+ case 'r': sb.Append('\r'); break;
+ case 't': sb.Append('\t'); break;
+ default: sb.Append('\\'); sb.Append(s[i]); break;
+ }
+ }
+ else
+ {
+ sb.Append(s[i]);
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static string StripComment(string line)
+ {
+ // Only strip # that is not inside a quoted string
+ bool inString = false;
+ for (int i = 0; i < line.Length; i++)
+ {
+ char c = line[i];
+ if (c == '"' && (i == 0 || line[i - 1] != '\\'))
+ inString = !inString;
+ if (c == '#' && !inString)
+ return line.Substring(0, i);
+ }
+ return line;
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
new file mode 100644
index 0000000000..3c8c1cdba2
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
@@ -0,0 +1,51 @@
+# Apache.Arrow.Adbc.DriverManager
+
+A .NET implementation of the ADBC Driver Manager, based on the C interface defined in `adbc_driver_manager.h`.
+
+## 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.
+- **Connection profiles**: load reusable connection configurations (driver + options) from `.toml` profile files.
+- **Custom profile providers**: plug in your own `IConnectionProfileProvider` implementation.
+
+## TOML Manifest / Profile Format
+
+### Unmanaged Driver Example (Snowflake)
+
+For unmanaged drivers loaded from native shared libraries:
+
+```toml
+version = 1
+driver = "libadbc_driver_snowflake"
+entrypoint = "AdbcDriverSnowflakeInit"
+
+[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)"
+```
+
+### Managed Driver Example (BigQuery)
+
+For managed .NET drivers:
+
+```toml
+version = 1
+driver = "C:\\path\\to\\Apache.Arrow.Adbc.Drivers.BigQuery.dll"
+driver_type = "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
+
+[options]
+adbc.bigquery.project_id = "my-project"
+adbc.bigquery.auth_type = "service"
+adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
+```
+
+### Format Notes
+
+- 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.
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
new file mode 100644
index 0000000000..708f2b3bd7
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
@@ -0,0 +1,255 @@
+/*
+ * 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 co-located manifest file auto-discovery.
+ ///
+ public class ColocatedManifestTests : IDisposable
+ {
+ private readonly List _tempFiles = new List();
+ private readonly List _tempDirs = new List();
+
+ private (string dllPath, string tomlPath) CreateTestFilePair(string baseName, string tomlContent)
+ {
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string dllPath = Path.Combine(tempDir, baseName + ".dll");
+ string tomlPath = Path.Combine(tempDir, baseName + ".toml");
+
+ File.WriteAllText(dllPath, "fake dll content");
+ File.WriteAllText(tomlPath, tomlContent);
+
+ _tempFiles.Add(dllPath);
+ _tempFiles.Add(tomlPath);
+
+ return (dllPath, tomlPath);
+ }
+
+ public void Dispose()
+ {
+ foreach (string f in _tempFiles)
+ {
+ try { if (File.Exists(f)) File.Delete(f); } catch { }
+ }
+ foreach (string d in _tempDirs)
+ {
+ try { if (Directory.Exists(d)) Directory.Delete(d, true); } catch { }
+ }
+ }
+
+ [Fact]
+ public void LoadDriver_WithColocatedManifest_LoadsFromManifest()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n"
+ + "\n[options]\n"
+ + "from_manifest = \"true\"\n"
+ + "manifest_version = \"1.0\"\n";
+
+ var (dllPath, tomlPath) = CreateTestFilePair("test_driver", toml);
+
+ // LoadDriver should auto-detect the co-located manifest and use it to determine:
+ // - The actual driver location (from the 'driver' field)
+ // - Whether it's a managed driver (from 'driver_type')
+ // - The entrypoint (if specified)
+ AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+
+ // NOTE: Manifest options are stored in the profile but NOT automatically applied here.
+ // To use manifest options, explicitly load the profile and use OpenDatabaseFromProfile,
+ // or pass options when opening the database.
+ var db = driver.Open(new Dictionary { { "test_key", "test_value" } });
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ Assert.Equal("test_value", fakeDb.Parameters["test_key"]);
+ }
+
+ [Fact]
+ public void LoadDriver_WithoutColocatedManifest_FailsAsExpected()
+ {
+ // Create a DLL without a co-located manifest
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string dllPath = Path.Combine(tempDir, "no_manifest.dll");
+ File.WriteAllText(dllPath, "fake dll");
+ _tempFiles.Add(dllPath);
+
+ // Should fail because the DLL doesn't exist as a real native library
+ // (but importantly, it should NOT fail looking for a manifest)
+ Assert.ThrowsAny(() => AdbcDriverManager.LoadDriver(dllPath));
+ }
+
+ [Fact]
+ public void FindLoadDriver_WithColocatedManifest_UsesManifest()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n" // Important: Must specify driver_type for managed drivers
+ + "\n[options]\n"
+ + "auto_discovered = \"yes\"\n";
+
+ var (dllPath, tomlPath) = CreateTestFilePair("my_driver", toml);
+
+ // FindLoadDriver should auto-detect co-located manifest and use it to load the driver
+ // NOTE: Options from the manifest are NOT automatically applied - they're only available
+ // when using OpenDatabaseFromProfile. The manifest is primarily for specifying HOW to
+ // load the driver (driver path, driver_type, entrypoint), not database configuration.
+ AdbcDriver driver = AdbcDriverManager.FindLoadDriver(dllPath);
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+
+ // To actually use the manifest options, you would need to:
+ // 1. Load the profile separately with TomlConnectionProfile.FromFile(tomlPath)
+ // 2. Use AdbcDriverManager.OpenDatabaseFromProfile(profile)
+ // Or just pass options when opening the database
+ var db = driver.Open(new Dictionary { { "manual_option", "value" } });
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ Assert.Equal("value", fakeDb.Parameters["manual_option"]);
+ }
+
+ [Fact]
+ public void LoadDriver_ManifestCanOverrideDriverPath()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ // Manifest points to the actual driver assembly
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n";
+
+ // DLL file is just a placeholder - the manifest redirects to the real driver
+ var (dllPath, tomlPath) = CreateTestFilePair("placeholder", toml);
+
+ AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+ }
+
+ [Fact]
+ public void LoadDriver_ExplicitEntrypointStillWorks()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n";
+
+ var (dllPath, tomlPath) = CreateTestFilePair("entrypoint_test", toml);
+
+ // 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");
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+ }
+
+ [Fact]
+ public void LoadDriver_RelativePathInManifest_ResolvedCorrectly()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+ string assemblyFileName = Path.GetFileName(assemblyPath);
+
+ // Copy the assembly to the test directory so we can use a relative path
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string localAssemblyPath = Path.Combine(tempDir, assemblyFileName);
+ File.Copy(assemblyPath, localAssemblyPath, overwrite: true);
+ _tempFiles.Add(localAssemblyPath);
+
+ // Manifest uses relative path to the driver
+ string toml = "version = 1\n"
+ + "driver = \"" + assemblyFileName + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n";
+
+ string dllPath = Path.Combine(tempDir, "wrapper.dll");
+ string tomlPath = Path.Combine(tempDir, "wrapper.toml");
+
+ File.WriteAllText(dllPath, "fake");
+ File.WriteAllText(tomlPath, toml);
+
+ _tempFiles.Add(dllPath);
+ _tempFiles.Add(tomlPath);
+
+ AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+ }
+
+ [Fact]
+ public void LoadDriver_DifferentExtensions_AllDetectManifest()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n";
+
+ // Test with .dll extension
+ var (dll1, _) = CreateTestFilePair("test.driver", toml);
+ AdbcDriver driver1 = AdbcDriverManager.LoadDriver(dll1);
+ Assert.NotNull(driver1);
+ Assert.IsType(driver1);
+
+ // Test with .so extension
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ string soPath = Path.Combine(tempDir, "test.driver.so");
+ string soToml = Path.Combine(tempDir, "test.driver.toml");
+ File.WriteAllText(soPath, "fake");
+ File.WriteAllText(soToml, toml);
+ _tempFiles.Add(soPath);
+ _tempFiles.Add(soToml);
+
+ // Should auto-detect manifest even with .so extension
+ AdbcDriver driver2 = AdbcDriverManager.LoadDriver(soPath);
+ Assert.NotNull(driver2);
+ Assert.IsType(driver2);
+ }
+ }
+}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/FakeAdbcDriver.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/FakeAdbcDriver.cs
new file mode 100644
index 0000000000..efe5404dd0
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/FakeAdbcDriver.cs
@@ -0,0 +1,51 @@
+/*
+ * 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.Collections.Generic;
+
+namespace Apache.Arrow.Adbc.Tests.DriverManager
+{
+ ///
+ /// Minimal in-memory AdbcDriver used to exercise the managed-driver loading path
+ /// without requiring a real driver assembly on disk.
+ ///
+ public sealed class FakeAdbcDriver : AdbcDriver
+ {
+ /// The parameters passed to the most recent call.
+ public IReadOnlyDictionary? LastOpenParameters { get; private set; }
+
+ public override AdbcDatabase Open(IReadOnlyDictionary parameters)
+ {
+ LastOpenParameters = parameters;
+ return new FakeAdbcDatabase(parameters);
+ }
+ }
+
+ /// Minimal in-memory AdbcDatabase returned by .
+ public sealed class FakeAdbcDatabase : AdbcDatabase
+ {
+ public IReadOnlyDictionary Parameters { get; }
+
+ public FakeAdbcDatabase(IReadOnlyDictionary parameters)
+ {
+ Parameters = parameters;
+ }
+
+ public override AdbcConnection Connect(IReadOnlyDictionary? options) =>
+ throw new System.NotImplementedException();
+ }
+}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
new file mode 100644
index 0000000000..a0e4700a46
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -0,0 +1,993 @@
+/*
+ * 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 and the internal .
+ ///
+ public class TomlConnectionProfileTests : IDisposable
+ {
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ private readonly List _tempFiles = new List();
+
+ private string WriteTempToml(string content)
+ {
+ string path = Path.GetTempFileName();
+ // Rename so it has the .toml extension (required by load helpers).
+ string tomlPath = Path.ChangeExtension(path, ".toml");
+ File.Move(path, tomlPath);
+ File.WriteAllText(tomlPath, content, System.Text.Encoding.UTF8);
+ _tempFiles.Add(tomlPath);
+ return tomlPath;
+ }
+
+ public void Dispose()
+ {
+ foreach (string f in _tempFiles)
+ {
+ try { if (File.Exists(f)) File.Delete(f); } catch { /* best-effort */ }
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: basic profile with all option types
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_WithAllOptionTypes_SetsCorrectValues()
+ {
+ const string toml = @"
+version = 1
+driver = ""libadbc_driver_postgresql""
+
+[options]
+uri = ""postgresql://localhost/mydb""
+port = 5432
+timeout = 30.5
+use_ssl = true
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ Assert.Equal("libadbc_driver_postgresql", profile.DriverName);
+ Assert.Equal("postgresql://localhost/mydb", profile.StringOptions["uri"]);
+ Assert.Equal(5432L, profile.IntOptions["port"]);
+ Assert.Equal(30.5, profile.DoubleOptions["timeout"]);
+ // Booleans are converted to string
+ Assert.Equal("true", profile.StringOptions["use_ssl"]);
+ }
+
+ [Fact]
+ public void ParseProfile_WithFalseBoolean_SetsStringFalse()
+ {
+ const string toml = @"
+version = 1
+driver = ""mydriver""
+
+[options]
+verify_cert = false
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal("false", profile.StringOptions["verify_cert"]);
+ }
+
+ [Fact]
+ public void ParseProfile_WithoutDriver_DriverNameIsNull()
+ {
+ const string toml = @"
+version = 1
+
+[options]
+uri = ""jdbc:something""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Null(profile.DriverName);
+ Assert.Equal("jdbc:something", profile.StringOptions["uri"]);
+ }
+
+ [Fact]
+ public void ParseProfile_WithoutOptionsSection_OptionDictionariesAreEmpty()
+ {
+ const string toml = @"
+version = 1
+driver = ""mydriver""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal("mydriver", profile.DriverName);
+ Assert.Empty(profile.StringOptions);
+ Assert.Empty(profile.IntOptions);
+ Assert.Empty(profile.DoubleOptions);
+ }
+
+ [Fact]
+ public void ParseProfile_WithLineComments_CommentsAreIgnored()
+ {
+ const string toml = @"
+# This is a comment
+version = 1
+driver = ""mydriver"" # inline comment
+
+[options]
+# another comment
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal("mydriver", profile.DriverName);
+ Assert.Equal("value", profile.StringOptions["key"]);
+ }
+
+ [Fact]
+ public void ParseProfile_WithNegativeInteger_ParsedCorrectly()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+offset = -100
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal(-100L, profile.IntOptions["offset"]);
+ }
+
+ [Fact]
+ public void ParseProfile_WithNegativeDouble_ParsedCorrectly()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+delta = -0.5
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal(-0.5, profile.DoubleOptions["delta"]);
+ }
+
+ [Fact]
+ public void ParseProfile_FromFile_LoadsCorrectly()
+ {
+ const string toml = @"
+version = 1
+driver = ""file_driver""
+
+[options]
+server = ""localhost""
+";
+ string path = WriteTempToml(toml);
+ TomlConnectionProfile profile = TomlConnectionProfile.FromFile(path);
+
+ Assert.Equal("file_driver", profile.DriverName);
+ Assert.Equal("localhost", profile.StringOptions["server"]);
+ }
+
+ [Fact]
+ public void ParseProfile_EscapedQuotesInString_ParsedCorrectly()
+ {
+ const string toml = "version = 1\ndriver = \"d\"\n\n[options]\nmsg = \"say \\\"hello\\\"\"\n";
+
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal("say \"hello\"", profile.StringOptions["msg"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: env_var expansion
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ResolveEnvVars_ExpandsEnvVarValues()
+ {
+ const string varName = "ADBC_TEST_PASSWORD_TOML";
+ Environment.SetEnvironmentVariable(varName, "secret123");
+ try
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+password = ""env_var(ADBC_TEST_PASSWORD_TOML)""
+plain = ""notanenvvar""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml).ResolveEnvVars();
+
+ Assert.Equal("secret123", profile.StringOptions["password"]);
+ Assert.Equal("notanenvvar", profile.StringOptions["plain"]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(varName, null);
+ }
+ }
+
+ [Fact]
+ public void ResolveEnvVars_NoEnvVarValues_ReturnsSameProfile()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml).ResolveEnvVars();
+ Assert.Equal("value", profile.StringOptions["key"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: AdbcDriverManager DeriveEntrypoint
+ // -----------------------------------------------------------------------
+
+ [Theory]
+ [InlineData("libadbc_driver_postgresql.so", "AdbcDriverPostgresqlInit")]
+ [InlineData("libadbc_driver_snowflake.dll", "AdbcDriverSnowflakeInit")]
+ [InlineData("libadbc_driver_flightsql.dylib", "AdbcDriverFlightsqlInit")]
+ [InlineData("adbc_driver_sqlite.dll", "AdbcDriverSqliteInit")]
+ [InlineData("mydriver.dll", "AdbcDriverMydriverInit")]
+ [InlineData("unknown", "AdbcDriverUnknownInit")]
+ public void DeriveEntrypoint_ReturnsExpectedSymbol(string driverPath, string expected)
+ {
+ string actual = AdbcDriverManager.DeriveEntrypoint(driverPath);
+ Assert.Equal(expected, actual);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: AdbcLoadFlags values
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void AdbcLoadFlags_DefaultIncludesAllSearchFlags()
+ {
+ AdbcLoadFlags flags = AdbcLoadFlags.Default;
+ Assert.True(flags.HasFlag(AdbcLoadFlags.SearchEnv));
+ Assert.True(flags.HasFlag(AdbcLoadFlags.SearchUser));
+ Assert.True(flags.HasFlag(AdbcLoadFlags.SearchSystem));
+ Assert.True(flags.HasFlag(AdbcLoadFlags.AllowRelativePaths));
+ }
+
+ [Fact]
+ public void AdbcLoadFlags_NoneHasNoFlags()
+ {
+ AdbcLoadFlags flags = AdbcLoadFlags.None;
+ Assert.False(flags.HasFlag(AdbcLoadFlags.SearchEnv));
+ Assert.False(flags.HasFlag(AdbcLoadFlags.SearchUser));
+ Assert.False(flags.HasFlag(AdbcLoadFlags.SearchSystem));
+ Assert.False(flags.HasFlag(AdbcLoadFlags.AllowRelativePaths));
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: missing version field
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_MissingVersion_ThrowsAdbcException()
+ {
+ const string toml = @"
+driver = ""mydriver""
+
+[options]
+key = ""value""
+";
+ AdbcException ex = Assert.Throws(() => TomlConnectionProfile.FromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ Assert.Contains("version", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: unsupported version
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_UnsupportedVersion_ThrowsAdbcException()
+ {
+ const string toml = @"
+version = 99
+driver = ""mydriver""
+";
+ AdbcException ex = Assert.Throws(() => TomlConnectionProfile.FromContent(toml));
+ Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+ Assert.Contains("99", ex.Message);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: null / empty content
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_NullContent_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => TomlConnectionProfile.FromContent(null!));
+ }
+
+ [Fact]
+ public void ParseProfile_EmptyContent_ThrowsAdbcException()
+ {
+ AdbcException ex = Assert.Throws(() => TomlConnectionProfile.FromContent(string.Empty));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: file not found
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_FileNotFound_ThrowsIOException()
+ {
+ string fakePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".toml");
+ Assert.ThrowsAny(
+ () => TomlConnectionProfile.FromFile(fakePath));
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: env_var expansion – variable not set
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ResolveEnvVars_MissingEnvVar_ThrowsAdbcException()
+ {
+ const string varName = "ADBC_TEST_DEFINITELY_NOT_SET_XYZ";
+ Environment.SetEnvironmentVariable(varName, null);
+
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+password = ""env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ)""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcException ex = Assert.Throws(() => profile.ResolveEnvVars());
+ Assert.Equal(AdbcStatusCode.InvalidState, ex.Status);
+ Assert.Contains(varName, ex.Message);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: AdbcDriverManager argument validation
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadDriver_NullPath_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.LoadDriver(null!));
+ }
+
+ [Fact]
+ public void LoadDriver_EmptyPath_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.LoadDriver(string.Empty));
+ }
+
+ [Fact]
+ public void FindLoadDriver_NullName_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.FindLoadDriver(null!));
+ }
+
+ [Fact]
+ public void FindLoadDriver_EmptyName_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.FindLoadDriver(string.Empty));
+ }
+
+ [Fact]
+ public void FindLoadDriver_NonExistentBareName_ThrowsAdbcException()
+ {
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.FindLoadDriver(
+ "driver_that_does_not_exist_anywhere",
+ loadOptions: AdbcLoadFlags.None));
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+ }
+
+ [Fact]
+ public void FindLoadDriver_AbsolutePathNotFound_ThrowsException()
+ {
+ // Absolute non-toml path that does not exist.
+ string fakePath = Path.Combine(Path.GetTempPath(), "fake_adbc_driver_notexist.dll");
+ Assert.ThrowsAny(() => AdbcDriverManager.FindLoadDriver(fakePath));
+ }
+
+ [Fact]
+ public void FindLoadDriver_AbsoluteTomlPathNotFound_ThrowsAdbcException()
+ {
+ string fakePath = Path.Combine(Path.GetTempPath(), "fake_manifest_notexist.toml");
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.FindLoadDriver(fakePath));
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+ }
+
+ [Fact]
+ public void LoadDriverFromProfile_NullProfile_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => AdbcDriverManager.LoadDriverFromProfile(null!));
+ }
+
+ [Fact]
+ public void LoadDriverFromProfile_ProfileWithNoDriver_ThrowsAdbcException()
+ {
+ const string toml = @"
+version = 1
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.LoadDriverFromProfile(profile));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: FilesystemProfileProvider with no candidate
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void FilesystemProfileProvider_UnknownProfile_ReturnsNull()
+ {
+ var provider = new FilesystemProfileProvider();
+ IConnectionProfile? result = provider.GetProfile(
+ "definitely_not_a_real_profile_xyz",
+ additionalSearchPathList: Path.GetTempPath());
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void FilesystemProfileProvider_NullName_ThrowsArgumentNullException()
+ {
+ var provider = new FilesystemProfileProvider();
+ Assert.Throws(() => provider.GetProfile(null!));
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: FilesystemProfileProvider finds a profile on disk
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void FilesystemProfileProvider_ProfileExistsInAdditionalPath_ReturnsProfile()
+ {
+ const string toml = @"
+version = 1
+driver = ""test_driver""
+
+[options]
+key = ""found""
+";
+ string dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ string filePath = Path.Combine(dir, "myprofile.toml");
+ File.WriteAllText(filePath, toml, System.Text.Encoding.UTF8);
+ try
+ {
+ var provider = new FilesystemProfileProvider();
+ IConnectionProfile? profile = provider.GetProfile("myprofile", additionalSearchPathList: dir);
+
+ Assert.NotNull(profile);
+ Assert.Equal("test_driver", profile!.DriverName);
+ Assert.Equal("found", profile.StringOptions["key"]);
+ }
+ finally
+ {
+ try { File.Delete(filePath); } catch { }
+ try { Directory.Delete(dir); } catch { }
+ }
+ }
+
+ [Fact]
+ public void FilesystemProfileProvider_AbsoluteTomlPath_LoadsDirectly()
+ {
+ const string toml = @"
+version = 1
+driver = ""abs_driver""
+";
+ string path = WriteTempToml(toml);
+ var provider = new FilesystemProfileProvider();
+ IConnectionProfile? profile = provider.GetProfile(path);
+
+ Assert.NotNull(profile);
+ 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""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(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""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Null(profile.DriverTypeName);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: LoadManagedDriver loads a managed .NET driver by reflection
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadManagedDriver_ValidType_ReturnsDriverInstance()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ AdbcDriver driver = AdbcDriverManager.LoadManagedDriver(assemblyPath, typeName);
+
+ Assert.NotNull(driver);
+ Assert.IsType(driver);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: BuildStringOptions merges all option types into string dictionary
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void BuildStringOptions_MergesAllOptionTypes()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+str_key = ""hello""
+int_key = 42
+dbl_key = 1.5
+bool_key = true
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ IReadOnlyDictionary opts = AdbcDriverManager.BuildStringOptions(profile);
+
+ Assert.Equal("hello", opts["str_key"]);
+ Assert.Equal("42", opts["int_key"]);
+ Assert.Equal("1.5", opts["dbl_key"]);
+ Assert.Equal("true", opts["bool_key"]);
+ }
+
+ [Fact]
+ public void BuildStringOptions_NoOptions_ReturnsEmptyDictionary()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ IReadOnlyDictionary opts = AdbcDriverManager.BuildStringOptions(profile);
+
+ Assert.Empty(opts);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: OpenDatabaseFromProfile end-to-end with managed driver
+ // -----------------------------------------------------------------------
+
+ [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"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n"
+ + "\n[options]\n"
+ + "project_id = \"my-project\"\n"
+ + "region = \"us-east1\"\n";
+
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile);
+
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ Assert.Equal("my-project", fakeDb.Parameters["project_id"]);
+ Assert.Equal("us-east1", fakeDb.Parameters["region"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: LoadManagedDriver argument validation
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadManagedDriver_NullAssemblyPath_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.LoadManagedDriver(null!, "SomeType"));
+ }
+
+ [Fact]
+ public void LoadManagedDriver_EmptyAssemblyPath_ThrowsArgumentException()
+ {
+ Assert.Throws(() => AdbcDriverManager.LoadManagedDriver(string.Empty, "SomeType"));
+ }
+
+ [Fact]
+ public void LoadManagedDriver_NullTypeName_ThrowsArgumentException()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ Assert.Throws(() => AdbcDriverManager.LoadManagedDriver(assemblyPath, null!));
+ }
+
+ [Fact]
+ public void LoadManagedDriver_TypeNotFound_ThrowsAdbcExceptionWithNotFoundStatus()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.LoadManagedDriver(assemblyPath, "NoSuchType.DoesNotExist"));
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+ }
+
+ [Fact]
+ public void LoadManagedDriver_TypeNotAdbcDriver_ThrowsAdbcExceptionWithInvalidArgumentStatus()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string nonDriverTypeName = typeof(TomlConnectionProfileTests).FullName!;
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.LoadManagedDriver(assemblyPath, nonDriverTypeName));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: OpenDatabaseFromProfile managed path validation
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void OpenDatabaseFromProfile_NullProfile_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => AdbcDriverManager.OpenDatabaseFromProfile(null!));
+ }
+
+ [Fact]
+ public void OpenDatabaseFromProfile_ManagedDriverMissingAssemblyPath_ThrowsAdbcException()
+ {
+ // driver_type is set but the driver (assembly path) field is omitted.
+ const string toml = @"
+version = 1
+driver_type = ""Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.OpenDatabaseFromProfile(profile));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Negative: BuildStringOptions null validation
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void BuildStringOptions_NullProfile_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => AdbcDriverManager.BuildStringOptions(null!));
+ }
+
+ // -----------------------------------------------------------------------
+ // Bad driver: LoadManagedDriver with assembly file that does not exist
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void LoadManagedDriver_NonExistentAssemblyFile_ThrowsAdbcExceptionWithIOErrorStatus()
+ {
+ string missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".dll");
+ AdbcException ex = Assert.Throws(
+ () => AdbcDriverManager.LoadManagedDriver(missingPath, "Some.Type"));
+ Assert.Equal(AdbcStatusCode.IOError, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Invalid types: version field is an unquoted non-numeric string
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_VersionIsNonNumericString_ThrowsAdbcException()
+ {
+ // The parser falls back to treating unquoted, non-bool, non-numeric tokens as
+ // raw strings. ValidateVersion must not leak a raw FormatException.
+ const string toml = "version = notanumber\ndriver = \"d\"\n";
+ AdbcException ex = Assert.Throws(
+ () => TomlConnectionProfile.FromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ [Fact]
+ public void ParseProfile_VersionIsQuotedNonNumericString_ThrowsAdbcException()
+ {
+ // A quoted string that cannot be converted to long must not leak FormatException.
+ const string toml = "version = \"abc\"\ndriver = \"d\"\n";
+ AdbcException ex = Assert.Throws(
+ () => TomlConnectionProfile.FromContent(toml));
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Unknown TOML sections are silently ignored
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_UnknownSection_SectionIsIgnored()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[metadata]
+author = ""someone""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ // [metadata] must not bleed into options
+ Assert.Equal("value", profile.StringOptions["key"]);
+ Assert.False(profile.StringOptions.ContainsKey("author"));
+ }
+
+ // -----------------------------------------------------------------------
+ // Duplicate keys: last value wins
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_DuplicateOptionKey_LastValueWins()
+ {
+ const string toml = "version = 1\ndriver = \"d\"\n\n[options]\nkey = \"first\"\nkey = \"second\"\n";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ Assert.Equal("second", profile.StringOptions["key"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // ResolveEnvVars preserves DriverTypeName
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ResolveEnvVars_DriverTypeNameIsPreserved()
+ {
+ const string varName = "ADBC_TEST_RESOLVE_ENVVAR_HOST";
+ Environment.SetEnvironmentVariable(varName, "myhost");
+ try
+ {
+ const string toml = @"
+version = 1
+driver = ""MyDriver.dll""
+driver_type = ""My.Namespace.MyDriver""
+
+[options]
+host = ""env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST)""
+";
+ TomlConnectionProfile resolved = TomlConnectionProfile.FromContent(toml).ResolveEnvVars();
+ Assert.Equal("My.Namespace.MyDriver", resolved.DriverTypeName);
+ Assert.Equal("myhost", resolved.StringOptions["host"]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(varName, null);
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Extra options not known to the driver pass through transparently
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void OpenDatabaseFromProfile_UnknownOptionsPassThroughToDriver()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n"
+ + "\n[options]\n"
+ + "known_key = \"hello\"\n"
+ + "unknown_widget = \"ignored_by_driver\"\n";
+
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile);
+
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ // Both keys arrive at the driver; the driver decides what to do with them.
+ Assert.Equal("hello", fakeDb.Parameters["known_key"]);
+ Assert.Equal("ignored_by_driver", fakeDb.Parameters["unknown_widget"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: BuildStringOptions with explicit options merges correctly
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void BuildStringOptions_WithExplicitOptions_MergesCorrectly()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+profile_key = ""from_profile""
+shared_key = ""profile_value""
+port = 5432
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ var explicitOptions = new Dictionary
+ {
+ { "explicit_key", "from_explicit" },
+ { "shared_key", "explicit_value" }
+ };
+
+ IReadOnlyDictionary merged = AdbcDriverManager.BuildStringOptions(profile, explicitOptions);
+
+ // Profile-only option should be present
+ Assert.Equal("from_profile", merged["profile_key"]);
+ Assert.Equal("5432", merged["port"]);
+
+ // Explicit-only option should be present
+ Assert.Equal("from_explicit", merged["explicit_key"]);
+
+ // Shared key: explicit should override profile
+ Assert.Equal("explicit_value", merged["shared_key"]);
+ }
+
+ [Fact]
+ public void BuildStringOptions_NullExplicitOptions_UsesOnlyProfileOptions()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ IReadOnlyDictionary opts = AdbcDriverManager.BuildStringOptions(profile, null);
+
+ Assert.Equal("value", opts["key"]);
+ }
+
+ [Fact]
+ public void BuildStringOptions_EmptyExplicitOptions_UsesOnlyProfileOptions()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ var empty = new Dictionary();
+ IReadOnlyDictionary opts = AdbcDriverManager.BuildStringOptions(profile, empty);
+
+ Assert.Equal("value", opts["key"]);
+ }
+
+ [Fact]
+ public void BuildStringOptions_ExplicitOptionsOverrideAllProfileTypes()
+ {
+ const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+str_key = ""profile_string""
+int_key = 100
+dbl_key = 2.5
+bool_key = false
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ var explicitOptions = new Dictionary
+ {
+ { "str_key", "explicit_string" },
+ { "int_key", "999" },
+ { "dbl_key", "9.9" },
+ { "bool_key", "true" }
+ };
+
+ IReadOnlyDictionary merged = AdbcDriverManager.BuildStringOptions(profile, explicitOptions);
+
+ // All explicit values should override profile values
+ Assert.Equal("explicit_string", merged["str_key"]);
+ Assert.Equal("999", merged["int_key"]);
+ Assert.Equal("9.9", merged["dbl_key"]);
+ Assert.Equal("true", merged["bool_key"]);
+ }
+
+ // -----------------------------------------------------------------------
+ // Positive: OpenDatabaseFromProfile with explicit options
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void OpenDatabaseFromProfile_WithExplicitOptions_MergesCorrectly()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n"
+ + "\n[options]\n"
+ + "profile_option = \"from_profile\"\n"
+ + "shared_option = \"profile_value\"\n";
+
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ var explicitOptions = new Dictionary
+ {
+ { "explicit_option", "from_explicit" },
+ { "shared_option", "explicit_value" }
+ };
+
+ AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile, explicitOptions);
+
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+
+ // Profile-only option should be present
+ Assert.Equal("from_profile", fakeDb.Parameters["profile_option"]);
+
+ // Explicit-only option should be present
+ Assert.Equal("from_explicit", fakeDb.Parameters["explicit_option"]);
+
+ // Shared option: explicit should override profile
+ Assert.Equal("explicit_value", fakeDb.Parameters["shared_option"]);
+ }
+
+ [Fact]
+ public void OpenDatabaseFromProfile_NullExplicitOptions_UsesOnlyProfile()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ string escapedPath = assemblyPath.Replace("\\", "\\\\");
+ string toml = "version = 1\n"
+ + "driver = \"" + escapedPath + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n"
+ + "\n[options]\n"
+ + "key = \"value\"\n";
+
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+ AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile, null);
+
+ FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ Assert.Equal("value", fakeDb.Parameters["key"]);
+ }
+ }
+}
From 3d64362eb6b76709c18c756a229dfd29ffefc268 Mon Sep 17 00:00:00 2001
From: David Coe <>
Date: Wed, 11 Mar 2026 17:10:47 -0400
Subject: [PATCH 2/5] update readme header
---
.../Apache.Arrow.Adbc/DriverManager/readme.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
index 3c8c1cdba2..324fbf0bad 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
@@ -1,3 +1,21 @@
+
# Apache.Arrow.Adbc.DriverManager
A .NET implementation of the ADBC Driver Manager, based on the C interface defined in `adbc_driver_manager.h`.
From 6d48037d6aa4a666383dfacdf70fd07a41d5ec4f Mon Sep 17 00:00:00 2001
From: David Coe <>
Date: Thu, 12 Mar 2026 22:59:47 -0400
Subject: [PATCH 3/5] refactor; add security
---
.../DriverManager/AdbcDriverManager.cs | 42 +-
.../DriverManager/DriverManagerSecurity.cs | 497 +++++++++++++++
.../FilesystemProfileProvider.cs | 17 +-
.../DriverManager/TomlConnectionProfile.cs | 38 +-
.../DriverManager/TomlParser.cs | 27 +-
.../Apache.Arrow.Adbc/DriverManager/readme.md | 74 +++
.../DriverManager/ColocatedManifestTests.cs | 195 +++---
.../DriverManagerSecurityTests.cs | 575 ++++++++++++++++++
.../TomlConnectionProfileTests.cs | 16 +-
9 files changed, 1377 insertions(+), 104 deletions(-)
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManagerSecurity.cs
create mode 100644 csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
index d24759484c..23fcd97544 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -275,9 +275,11 @@ public static AdbcDriver LoadManagedDriver(string assemblyPath, string typeName)
}
if (instance == null)
+ {
throw new AdbcException(
$"Activator returned null for driver type '{typeName}'.",
AdbcStatusCode.InternalError);
+ }
return (AdbcDriver)instance;
}
@@ -564,27 +566,40 @@ private static AdbcDriver LoadFromAbsolutePath(string path, string? entrypoint)
private static AdbcDriver LoadFromManifest(string manifestPath, string? entrypoint)
{
if (!File.Exists(manifestPath))
+ {
throw new AdbcException(
- $"Driver manifest file not found: '{manifestPath}'.",
+ $"Driver manifest file not found.",
AdbcStatusCode.NotFound);
+ }
TomlConnectionProfile manifest = TomlConnectionProfile.FromFile(manifestPath);
if (string.IsNullOrEmpty(manifest.DriverName))
+ {
throw new AdbcException(
- $"Driver manifest '{manifestPath}' does not specify a 'driver' field.",
+ $"Driver manifest does not specify a 'driver' field.",
AdbcStatusCode.InvalidArgument);
+ }
+
+ string? manifestDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
// Check if this is a managed driver
if (!string.IsNullOrEmpty(manifest.DriverTypeName))
{
- // Managed .NET driver - resolve relative path relative to manifest directory
+ // Managed .NET driver - resolve path
string driverPath = manifest.DriverName!;
- if (!Path.IsPathRooted(driverPath))
+ if (Path.IsPathRooted(driverPath))
{
- string? manifestDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+ // 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 = Path.Combine(manifestDir, driverPath);
+ {
+ driverPath = DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir, driverPath);
+ }
}
return LoadManagedDriver(driverPath, manifest.DriverTypeName!);
}
@@ -592,13 +607,20 @@ private static AdbcDriver LoadFromManifest(string manifestPath, string? entrypoi
// Native driver - resolve entrypoint and path
string resolvedEntrypoint = entrypoint ?? DeriveEntrypoint(manifest.DriverName!);
- // Resolve relative driver path relative to the manifest directory.
+ // Resolve driver path
string resolvedDriverPath = manifest.DriverName!;
- if (!Path.IsPathRooted(resolvedDriverPath))
+ if (Path.IsPathRooted(resolvedDriverPath))
+ {
+ // Absolute path - validate it doesn't contain path traversal
+ DriverManagerSecurity.ValidatePathSecurity(resolvedDriverPath, "manifest driver path");
+ }
+ else
{
- string? manifestDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+ // Relative path - validate it doesn't escape the manifest directory
if (!string.IsNullOrEmpty(manifestDir))
- resolvedDriverPath = Path.Combine(manifestDir, resolvedDriverPath);
+ {
+ resolvedDriverPath = DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir, resolvedDriverPath);
+ }
}
return CAdbcDriverImporter.Load(resolvedDriverPath, resolvedEntrypoint);
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManagerSecurity.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManagerSecurity.cs
new file mode 100644
index 0000000000..9e57a44328
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManagerSecurity.cs
@@ -0,0 +1,497 @@
+/*
+ * 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;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Provides security-related functionality for the ADBC driver manager,
+ /// including path validation, allowlist management, and audit logging.
+ ///
+ public static class DriverManagerSecurity
+ {
+ private static readonly object s_lock = new object();
+ private static IDriverLoadAuditLogger? s_auditLogger;
+ private static IDriverAllowlist? s_allowlist;
+
+ ///
+ /// Gets or sets the audit logger for driver loading operations.
+ /// When set, all driver load attempts will be logged.
+ ///
+ ///
+ /// This property is thread-safe. Set to null to disable audit logging.
+ ///
+ public static IDriverLoadAuditLogger? AuditLogger
+ {
+ get
+ {
+ lock (s_lock)
+ {
+ return s_auditLogger;
+ }
+ }
+ set
+ {
+ lock (s_lock)
+ {
+ s_auditLogger = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the driver allowlist. When set, only drivers matching
+ /// the allowlist criteria will be permitted to load.
+ ///
+ ///
+ ///
+ /// This property is thread-safe. Set to null to disable allowlist
+ /// validation (all drivers will be permitted).
+ ///
+ ///
+ /// Security Note: In production environments, consider using an
+ /// allowlist to restrict which drivers can be loaded, preventing
+ /// potential arbitrary code execution attacks.
+ ///
+ ///
+ public static IDriverAllowlist? Allowlist
+ {
+ get
+ {
+ lock (s_lock)
+ {
+ return s_allowlist;
+ }
+ }
+ set
+ {
+ lock (s_lock)
+ {
+ s_allowlist = value;
+ }
+ }
+ }
+
+ ///
+ /// Validates that a path does not contain path traversal sequences
+ /// that could escape intended directories.
+ ///
+ /// The path to validate.
+ /// The parameter name for error messages.
+ ///
+ /// Thrown when the path contains potentially dangerous sequences.
+ ///
+ public static void ValidatePathSecurity(string path, string paramName)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return;
+ }
+
+ // Check for path traversal attempts
+ if (path.Contains(".."))
+ {
+ throw new AdbcException(
+ $"Path '{paramName}' contains potentially dangerous path traversal sequences (..).",
+ AdbcStatusCode.InvalidArgument);
+ }
+
+ // Check for null bytes (can be used to truncate paths in some systems)
+ if (path.Contains("\0"))
+ {
+ throw new AdbcException(
+ $"Path '{paramName}' contains invalid null characters.",
+ AdbcStatusCode.InvalidArgument);
+ }
+ }
+
+ ///
+ /// Validates that a relative path in a manifest does not attempt to
+ /// escape the manifest's directory.
+ ///
+ /// The directory containing the manifest.
+ /// The relative path specified in the manifest.
+ /// The validated, canonicalized full path.
+ ///
+ /// Thrown when the resolved path escapes the manifest directory.
+ ///
+ public static string ValidateAndResolveManifestPath(string manifestDirectory, string relativePath)
+ {
+ ValidatePathSecurity(relativePath, "manifest driver path");
+
+ // Resolve the full path
+ string fullPath = Path.GetFullPath(Path.Combine(manifestDirectory, relativePath));
+ string canonicalManifestDir = Path.GetFullPath(manifestDirectory);
+
+ // Ensure the resolved path is within or below the manifest directory
+ // This prevents path traversal attacks via symbolic links or other means
+ if (!fullPath.StartsWith(canonicalManifestDir, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new AdbcException(
+ $"Manifest driver path resolves outside the manifest directory. " +
+ $"This may indicate a path traversal attack.",
+ AdbcStatusCode.InvalidArgument);
+ }
+
+ return fullPath;
+ }
+
+ ///
+ /// Logs a driver load attempt if an audit logger is configured.
+ ///
+ /// The driver load attempt details to log.
+ public static void LogDriverLoadAttempt(DriverLoadAttempt attempt)
+ {
+ IDriverLoadAuditLogger? logger = AuditLogger;
+ if (logger != null)
+ {
+ try
+ {
+ logger.LogDriverLoadAttempt(attempt);
+ }
+ catch
+ {
+ // Don't let logging failures affect driver loading
+ }
+ }
+ }
+
+ ///
+ /// Checks if a driver is allowed to be loaded based on the configured allowlist.
+ ///
+ /// The path to the driver.
+ /// The type name for managed drivers, or null for native drivers.
+ ///
+ /// Thrown when the driver is not permitted by the allowlist.
+ ///
+ public static void ValidateAllowlist(string driverPath, string? typeName)
+ {
+ IDriverAllowlist? allowlist = Allowlist;
+ if (allowlist == null)
+ {
+ return; // No allowlist configured, all drivers permitted
+ }
+
+ if (!allowlist.IsDriverAllowed(driverPath, typeName))
+ {
+ string driverDescription = typeName != null
+ ? $"managed driver '{typeName}' from '{driverPath}'"
+ : $"native driver '{driverPath}'";
+
+ throw new AdbcException(
+ $"Loading {driverDescription} is not permitted by the configured allowlist.",
+ AdbcStatusCode.Unauthorized);
+ }
+ }
+ }
+
+ ///
+ /// Represents an attempt to load a driver, for audit logging purposes.
+ ///
+ public sealed class DriverLoadAttempt
+ {
+ ///
+ /// Gets the UTC timestamp when the load attempt occurred.
+ ///
+ public DateTime TimestampUtc { get; }
+
+ ///
+ /// Gets the path to the driver being loaded.
+ ///
+ public string DriverPath { get; }
+
+ ///
+ /// Gets the type name for managed drivers, or null for native drivers.
+ ///
+ public string? TypeName { get; }
+
+ ///
+ /// Gets the manifest path if a manifest was used, or null otherwise.
+ ///
+ public string? ManifestPath { get; }
+
+ ///
+ /// Gets a value indicating whether the load attempt succeeded.
+ ///
+ public bool Success { get; }
+
+ ///
+ /// Gets the error message if the load attempt failed, or null if it succeeded.
+ ///
+ public string? ErrorMessage { get; }
+
+ ///
+ /// Gets the method used to load the driver (e.g., "LoadDriver", "LoadManagedDriver", "FindLoadDriver").
+ ///
+ public string LoadMethod { get; }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public DriverLoadAttempt(
+ string driverPath,
+ string? typeName,
+ string? manifestPath,
+ bool success,
+ string? errorMessage,
+ string loadMethod)
+ {
+ TimestampUtc = DateTime.UtcNow;
+ DriverPath = driverPath ?? throw new ArgumentNullException(nameof(driverPath));
+ TypeName = typeName;
+ ManifestPath = manifestPath;
+ Success = success;
+ ErrorMessage = errorMessage;
+ LoadMethod = loadMethod ?? throw new ArgumentNullException(nameof(loadMethod));
+ }
+ }
+
+ ///
+ /// Interface for audit logging of driver load attempts.
+ ///
+ ///
+ /// Implement this interface to receive notifications about all driver
+ /// loading operations for security monitoring and compliance purposes.
+ ///
+ public interface IDriverLoadAuditLogger
+ {
+ ///
+ /// Logs a driver load attempt.
+ ///
+ /// Details about the load attempt.
+ void LogDriverLoadAttempt(DriverLoadAttempt attempt);
+ }
+
+ ///
+ /// Interface for validating whether a driver is allowed to be loaded.
+ ///
+ ///
+ ///
+ /// Implement this interface to restrict which drivers can be loaded.
+ /// This is useful for preventing arbitrary code execution attacks where
+ /// a malicious manifest could redirect to a malicious driver.
+ ///
+ ///
+ /// Example implementations:
+ ///
+ /// - Allow only drivers from specific directories
+ /// - Allow only drivers with specific file names
+ /// - Allow only signed assemblies
+ /// - Allow only specific type names for managed drivers
+ ///
+ ///
+ ///
+ public interface IDriverAllowlist
+ {
+ ///
+ /// Determines whether a driver is allowed to be loaded.
+ ///
+ /// The full path to the driver file.
+ ///
+ /// For managed drivers, the fully-qualified type name. For native drivers, null.
+ ///
+ ///
+ /// true if the driver is allowed to be loaded; otherwise, false.
+ ///
+ bool IsDriverAllowed(string driverPath, string? typeName);
+ }
+
+ ///
+ /// A simple directory-based driver allowlist that permits drivers
+ /// only from specific directories.
+ ///
+ ///
+ ///
+ /// This allowlist implementation checks whether the driver path starts
+ /// with one of the allowed directory paths. It uses case-insensitive
+ /// comparison on Windows and case-sensitive on other platforms.
+ ///
+ ///
+ /// Example usage:
+ ///
+ /// var allowlist = new DirectoryAllowlist(new[]
+ /// {
+ /// @"C:\Program Files\ADBC\Drivers",
+ /// @"C:\MyApp\Drivers"
+ /// });
+ /// DriverManagerSecurity.Allowlist = allowlist;
+ ///
+ ///
+ ///
+ public sealed class DirectoryAllowlist : IDriverAllowlist
+ {
+ private readonly List _allowedDirectories;
+ private readonly StringComparison _comparison;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// The directories from which drivers may be loaded. Paths are
+ /// canonicalized during construction.
+ ///
+ public DirectoryAllowlist(IEnumerable allowedDirectories)
+ {
+ if (allowedDirectories == null)
+ {
+ throw new ArgumentNullException(nameof(allowedDirectories));
+ }
+
+ _allowedDirectories = new List();
+ foreach (string dir in allowedDirectories)
+ {
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ // Canonicalize and ensure trailing separator for proper prefix matching
+ string canonical = Path.GetFullPath(dir);
+ if (!canonical.EndsWith(Path.DirectorySeparatorChar.ToString()))
+ {
+ canonical += Path.DirectorySeparatorChar;
+ }
+ _allowedDirectories.Add(canonical);
+ }
+ }
+
+ // Use case-insensitive comparison on Windows
+ _comparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal;
+ }
+
+ ///
+ public bool IsDriverAllowed(string driverPath, string? typeName)
+ {
+ if (string.IsNullOrEmpty(driverPath))
+ {
+ return false;
+ }
+
+ string canonicalPath = Path.GetFullPath(driverPath);
+
+ foreach (string allowedDir in _allowedDirectories)
+ {
+ if (canonicalPath.StartsWith(allowedDir, _comparison))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// A type-based driver allowlist that permits only specific driver types
+ /// for managed drivers.
+ ///
+ ///
+ ///
+ /// This allowlist implementation checks whether the driver type name
+ /// matches one of the explicitly allowed type names. For native drivers,
+ /// it delegates to an optional inner allowlist or permits all native drivers.
+ ///
+ ///
+ /// Example usage:
+ ///
+ /// var allowlist = new TypeAllowlist(new[]
+ /// {
+ /// "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver",
+ /// "Apache.Arrow.Adbc.Drivers.Snowflake.SnowflakeDriver"
+ /// });
+ /// DriverManagerSecurity.Allowlist = allowlist;
+ ///
+ ///
+ ///
+ public sealed class TypeAllowlist : IDriverAllowlist
+ {
+ private readonly HashSet _allowedTypes;
+ private readonly IDriverAllowlist? _nativeDriverAllowlist;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// The fully-qualified type names that are allowed for managed drivers.
+ ///
+ ///
+ /// Optional allowlist for native drivers. If null, all native drivers are permitted.
+ ///
+ public TypeAllowlist(
+ IEnumerable allowedTypeNames,
+ IDriverAllowlist? nativeDriverAllowlist = null)
+ {
+ if (allowedTypeNames == null)
+ {
+ throw new ArgumentNullException(nameof(allowedTypeNames));
+ }
+
+ _allowedTypes = new HashSet(allowedTypeNames, StringComparer.Ordinal);
+ _nativeDriverAllowlist = nativeDriverAllowlist;
+ }
+
+ ///
+ public bool IsDriverAllowed(string driverPath, string? typeName)
+ {
+ if (typeName != null)
+ {
+ // Managed driver - check type allowlist
+ return _allowedTypes.Contains(typeName);
+ }
+ else
+ {
+ // Native driver - delegate to inner allowlist or permit
+ return _nativeDriverAllowlist?.IsDriverAllowed(driverPath, null) ?? true;
+ }
+ }
+ }
+
+ ///
+ /// Combines multiple allowlists, requiring all of them to permit a driver.
+ ///
+ public sealed class CompositeAllowlist : IDriverAllowlist
+ {
+ private readonly IDriverAllowlist[] _allowlists;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// The allowlists to combine. All must return true for a driver to be permitted.
+ ///
+ public CompositeAllowlist(params IDriverAllowlist[] allowlists)
+ {
+ _allowlists = allowlists ?? throw new ArgumentNullException(nameof(allowlists));
+ }
+
+ ///
+ public bool IsDriverAllowed(string driverPath, string? typeName)
+ {
+ foreach (IDriverAllowlist allowlist in _allowlists)
+ {
+ if (!allowlist.IsDriverAllowed(driverPath, typeName))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
index 0e92dd290c..8e2ea02155 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
@@ -66,30 +66,41 @@ public FilesystemProfileProvider() { }
///
public IConnectionProfile? GetProfile(string profileName, string? additionalSearchPathList = null)
{
- if (profileName == null) throw new ArgumentNullException(nameof(profileName));
+ if (profileName == null)
+ {
+ throw new ArgumentNullException(nameof(profileName));
+ }
// If already an absolute path, load directly.
if (Path.IsPathRooted(profileName))
{
string candidate = EnsureTomlExtension(profileName);
if (File.Exists(candidate))
+ {
return TomlConnectionProfile.FromFile(candidate);
+ }
return null;
}
// Build the ordered search directories.
- var dirs = new List();
+ List dirs = new List();
if (!string.IsNullOrEmpty(additionalSearchPathList))
+ {
dirs.AddRange(SplitPathList(additionalSearchPathList!));
+ }
string? profilePathEnv = Environment.GetEnvironmentVariable(ProfilePathEnvVar);
if (!string.IsNullOrEmpty(profilePathEnv))
+ {
dirs.AddRange(SplitPathList(profilePathEnv!));
+ }
string userDir = GetUserProfileDirectory();
if (!string.IsNullOrEmpty(userDir))
+ {
dirs.Add(userDir);
+ }
string fileName = EnsureTomlExtension(profileName);
@@ -97,7 +108,9 @@ public FilesystemProfileProvider() { }
{
string candidate = Path.Combine(dir, fileName);
if (File.Exists(candidate))
+ {
return TomlConnectionProfile.FromFile(candidate);
+ }
}
return null;
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
index 453f9c0cde..f1888507fb 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
@@ -92,29 +92,36 @@ private TomlConnectionProfile(
///
public static TomlConnectionProfile FromContent(string tomlContent)
{
- if (tomlContent == null) throw new ArgumentNullException(nameof(tomlContent));
+ if (tomlContent == null)
+ {
+ throw new ArgumentNullException(nameof(tomlContent));
+ }
- var sections = TomlParser.Parse(tomlContent);
+ Dictionary> sections = TomlParser.Parse(tomlContent);
- var root = sections.TryGetValue("", out var r) ? r : new Dictionary();
+ Dictionary root = sections.TryGetValue("", out Dictionary? r) ? r : new Dictionary();
ValidateVersion(root);
string? driverName = null;
if (root.TryGetValue("driver", out object? driverObj) && driverObj is string driverStr)
+ {
driverName = driverStr;
+ }
string? driverTypeName = null;
if (root.TryGetValue("driver_type", out object? driverTypeObj) && driverTypeObj is string driverTypeStr)
+ {
driverTypeName = driverTypeStr;
+ }
- var stringOpts = new Dictionary(StringComparer.Ordinal);
- var intOpts = new Dictionary(StringComparer.Ordinal);
- var doubleOpts = new Dictionary(StringComparer.Ordinal);
+ Dictionary stringOpts = new Dictionary(StringComparer.Ordinal);
+ Dictionary intOpts = new Dictionary(StringComparer.Ordinal);
+ Dictionary doubleOpts = new Dictionary(StringComparer.Ordinal);
- if (sections.TryGetValue(OptionsSection, out var optSection))
+ if (sections.TryGetValue(OptionsSection, out Dictionary? optSection))
{
- foreach (var kv in optSection)
+ foreach (KeyValuePair kv in optSection)
{
string key = kv.Key;
object val = kv.Value;
@@ -147,7 +154,10 @@ public static TomlConnectionProfile FromContent(string tomlContent)
/// A new .
public static TomlConnectionProfile FromFile(string filePath)
{
- if (filePath == null) throw new ArgumentNullException(nameof(filePath));
+ if (filePath == null)
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
string content = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
return FromContent(content);
}
@@ -162,8 +172,8 @@ public static TomlConnectionProfile FromFile(string filePath)
///
public TomlConnectionProfile ResolveEnvVars()
{
- var resolved = new Dictionary(StringComparer.Ordinal);
- foreach (var kv in _stringOptions)
+ Dictionary resolved = new Dictionary(StringComparer.Ordinal);
+ foreach (KeyValuePair kv in _stringOptions)
{
string value = kv.Value;
if (value.StartsWith(EnvVarPrefix, StringComparison.Ordinal) &&
@@ -172,9 +182,11 @@ public TomlConnectionProfile ResolveEnvVars()
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
@@ -193,9 +205,11 @@ public void Dispose()
private static void ValidateVersion(Dictionary root)
{
if (!root.TryGetValue("version", out object? versionObj))
+ {
throw new AdbcException(
"TOML profile is missing the required 'version' field.",
AdbcStatusCode.InvalidArgument);
+ }
long version;
if (versionObj is long lv)
@@ -217,9 +231,11 @@ private static void ValidateVersion(Dictionary root)
}
if (version != 1)
+ {
throw new AdbcException(
$"Unsupported profile version '{version}'. Only version 1 is supported.",
AdbcStatusCode.NotImplemented);
+ }
}
}
}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
index a78c350709..1619fc2674 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
@@ -48,9 +48,12 @@ internal static class TomlParser
///
internal static Dictionary> Parse(string content)
{
- if (content == null) throw new ArgumentNullException(nameof(content));
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
- var result = new Dictionary>(StringComparer.OrdinalIgnoreCase)
+ Dictionary> result = new Dictionary>(StringComparer.OrdinalIgnoreCase)
{
[RootSection] = new Dictionary(StringComparer.OrdinalIgnoreCase),
};
@@ -62,19 +65,25 @@ internal static Dictionary> Parse(string cont
string line = StripComment(rawLine).Trim();
if (line.Length == 0)
+ {
continue;
+ }
if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal))
{
currentSection = line.Substring(1, line.Length - 2).Trim();
if (!result.ContainsKey(currentSection))
+ {
result[currentSection] = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
continue;
}
int eqIndex = line.IndexOf('=');
if (eqIndex <= 0)
+ {
continue;
+ }
string key = line.Substring(0, eqIndex).Trim();
string valueRaw = line.Substring(eqIndex + 1).Trim();
@@ -97,17 +106,25 @@ private static object ParseValue(string raw)
// Boolean
if (string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase))
+ {
return true;
+ }
if (string.Equals(raw, "false", StringComparison.OrdinalIgnoreCase))
+ {
return false;
+ }
// Integer (try before float, since integers are a subset)
if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out long intValue))
+ {
return intValue;
+ }
// Float
if (double.TryParse(raw, NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double dblValue))
+ {
return dblValue;
+ }
// Fallback: treat as unquoted string
return raw;
@@ -115,7 +132,7 @@ private static object ParseValue(string raw)
private static string UnescapeString(string s)
{
- var sb = new System.Text.StringBuilder(s.Length);
+ System.Text.StringBuilder sb = new System.Text.StringBuilder(s.Length);
for (int i = 0; i < s.Length; i++)
{
if (s[i] == '\\' && i + 1 < s.Length)
@@ -147,9 +164,13 @@ private static string StripComment(string line)
{
char c = line[i];
if (c == '"' && (i == 0 || line[i - 1] != '\\'))
+ {
inString = !inString;
+ }
if (c == '#' && !inString)
+ {
return line.Substring(0, i);
+ }
}
return line;
}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
index 324fbf0bad..f82e3c792b 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
@@ -67,3 +67,77 @@ adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
- 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.
+
+## Security Features
+
+The Driver Manager includes security features to protect against common attacks when loading drivers dynamically.
+
+### Path Traversal Protection
+
+The driver manager validates all paths from manifest files to prevent path traversal attacks:
+
+- Paths containing `..` sequences are rejected
+- Paths containing null bytes are rejected
+- Relative paths in manifests are validated to ensure they don't escape the manifest directory
+
+```csharp
+// Manual path validation
+DriverManagerSecurity.ValidatePathSecurity(userProvidedPath, "driverPath");
+
+// Validate and resolve a relative path against a base directory
+string resolvedPath = DriverManagerSecurity.ValidateAndResolveManifestPath(
+ manifestDirectory, relativePath);
+```
+
+### Driver Allowlist
+
+Restrict which drivers can be loaded by configuring an allowlist:
+
+```csharp
+// Only allow drivers from specific directories
+DriverManagerSecurity.Allowlist = new DirectoryAllowlist(new[]
+{
+ @"C:\Program Files\ADBC\Drivers",
+ @"C:\MyApp\TrustedDrivers"
+});
+
+// Only allow specific managed driver types
+DriverManagerSecurity.Allowlist = new TypeAllowlist(new[]
+{
+ "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver",
+ "Apache.Arrow.Adbc.Drivers.Snowflake.SnowflakeDriver"
+});
+
+// Combine multiple restrictions (all must pass)
+DriverManagerSecurity.Allowlist = new CompositeAllowlist(
+ new DirectoryAllowlist(trustedDirectories),
+ new TypeAllowlist(trustedTypes)
+);
+```
+
+### Audit Logging
+
+Log all driver load attempts for security monitoring:
+
+```csharp
+public class MyAuditLogger : IDriverLoadAuditLogger
+{
+ public void LogDriverLoadAttempt(DriverLoadAttempt attempt)
+ {
+ Console.WriteLine($"[{attempt.TimestampUtc:O}] {attempt.LoadMethod}: " +
+ $"{attempt.DriverPath} - {(attempt.Success ? "SUCCESS" : "FAILED: " + attempt.ErrorMessage)}");
+ }
+}
+
+// Enable audit logging
+DriverManagerSecurity.AuditLogger = new MyAuditLogger();
+```
+
+The `DriverLoadAttempt` class captures:
+- `TimestampUtc` - When the load was attempted
+- `DriverPath` - Path to the driver
+- `TypeName` - Type name for managed drivers (null for native)
+- `ManifestPath` - Path to manifest if used
+- `Success` - Whether the load succeeded
+- `ErrorMessage` - Error details if failed
+- `LoadMethod` - Which method was used (LoadDriver, LoadManagedDriver, etc.)
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
index 708f2b3bd7..e9427b4732 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
@@ -31,7 +31,14 @@ public class ColocatedManifestTests : IDisposable
private readonly List _tempFiles = new List();
private readonly List _tempDirs = new List();
- private (string dllPath, string tomlPath) CreateTestFilePair(string baseName, string tomlContent)
+ ///
+ /// Creates a test directory with a placeholder DLL, a co-located TOML manifest,
+ /// and optionally copies the actual driver assembly.
+ ///
+ private (string dllPath, string tomlPath) CreateTestFilePair(
+ string baseName,
+ string tomlContent,
+ bool copyRealAssembly = false)
{
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
@@ -40,7 +47,17 @@ public class ColocatedManifestTests : IDisposable
string dllPath = Path.Combine(tempDir, baseName + ".dll");
string tomlPath = Path.Combine(tempDir, baseName + ".toml");
- File.WriteAllText(dllPath, "fake dll content");
+ if (copyRealAssembly)
+ {
+ // Copy the real FakeAdbcDriver assembly to the temp directory
+ string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ File.Copy(realAssemblyPath, dllPath, overwrite: true);
+ }
+ else
+ {
+ File.WriteAllText(dllPath, "fake dll content");
+ }
+
File.WriteAllText(tomlPath, tomlContent);
_tempFiles.Add(dllPath);
@@ -49,6 +66,43 @@ public class ColocatedManifestTests : IDisposable
return (dllPath, tomlPath);
}
+ ///
+ /// Creates test files where the manifest uses a relative path to a real assembly.
+ ///
+ private (string placeholderDllPath, string tomlPath, string realAssemblyPath) CreateTestFilesWithRelativeDriver(
+ string baseName,
+ string typeName)
+ {
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ _tempDirs.Add(tempDir);
+
+ // Copy the real assembly to the temp directory
+ 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);
+
+ // Create a placeholder DLL with a different name
+ string placeholderDllPath = Path.Combine(tempDir, baseName + ".dll");
+ 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";
+
+ string tomlPath = Path.Combine(tempDir, baseName + ".toml");
+ File.WriteAllText(tomlPath, toml);
+ _tempFiles.Add(tomlPath);
+
+ return (placeholderDllPath, tomlPath, copiedAssemblyPath);
+ }
+
public void Dispose()
{
foreach (string f in _tempFiles)
@@ -64,33 +118,23 @@ public void Dispose()
[Fact]
public void LoadDriver_WithColocatedManifest_LoadsFromManifest()
{
- string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
- + "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n"
- + "\n[options]\n"
- + "from_manifest = \"true\"\n"
- + "manifest_version = \"1.0\"\n";
-
- var (dllPath, tomlPath) = CreateTestFilePair("test_driver", toml);
+ (string dllPath, string tomlPath, string realAssemblyPath) =
+ 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)
+ // - The actual driver location (from the 'driver' field - relative path)
// - Whether it's a managed driver (from 'driver_type')
- // - The entrypoint (if specified)
AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
Assert.NotNull(driver);
- Assert.IsType(driver);
+ // Check type name instead of IsType to avoid assembly identity issues
+ Assert.Equal(typeName, driver.GetType().FullName);
// NOTE: Manifest options are stored in the profile but NOT automatically applied here.
- // To use manifest options, explicitly load the profile and use OpenDatabaseFromProfile,
- // or pass options when opening the database.
- var db = driver.Open(new Dictionary { { "test_key", "test_value" } });
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
- Assert.Equal("test_value", fakeDb.Parameters["test_key"]);
+ AdbcDatabase db = driver.Open(new Dictionary { { "test_key", "test_value" } });
+ // FakeAdbcDatabase full name
+ Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase", db.GetType().FullName);
}
[Fact]
@@ -113,73 +157,47 @@ public void LoadDriver_WithoutColocatedManifest_FailsAsExpected()
[Fact]
public void FindLoadDriver_WithColocatedManifest_UsesManifest()
{
- string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
- + "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n" // Important: Must specify driver_type for managed drivers
- + "\n[options]\n"
- + "auto_discovered = \"yes\"\n";
-
- var (dllPath, tomlPath) = CreateTestFilePair("my_driver", toml);
+ (string dllPath, string tomlPath, string realAssemblyPath) =
+ CreateTestFilesWithRelativeDriver("my_driver", typeName);
// FindLoadDriver should auto-detect co-located manifest and use it to load the driver
- // NOTE: Options from the manifest are NOT automatically applied - they're only available
- // when using OpenDatabaseFromProfile. The manifest is primarily for specifying HOW to
- // load the driver (driver path, driver_type, entrypoint), not database configuration.
AdbcDriver driver = AdbcDriverManager.FindLoadDriver(dllPath);
Assert.NotNull(driver);
- Assert.IsType(driver);
-
- // To actually use the manifest options, you would need to:
- // 1. Load the profile separately with TomlConnectionProfile.FromFile(tomlPath)
- // 2. Use AdbcDriverManager.OpenDatabaseFromProfile(profile)
- // Or just pass options when opening the database
- var db = driver.Open(new Dictionary { { "manual_option", "value" } });
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
- Assert.Equal("value", fakeDb.Parameters["manual_option"]);
+ Assert.Equal(typeName, driver.GetType().FullName);
+
+ AdbcDatabase db = driver.Open(new Dictionary { { "manual_option", "value" } });
+ Assert.NotNull(db);
}
[Fact]
public void LoadDriver_ManifestCanOverrideDriverPath()
{
- string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- // Manifest points to the actual driver assembly
- string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
- + "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n";
-
// DLL file is just a placeholder - the manifest redirects to the real driver
- var (dllPath, tomlPath) = CreateTestFilePair("placeholder", toml);
+ (string dllPath, string tomlPath, string realAssemblyPath) =
+ CreateTestFilesWithRelativeDriver("placeholder", typeName);
AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
Assert.NotNull(driver);
- Assert.IsType(driver);
+ Assert.Equal(typeName, driver.GetType().FullName);
}
[Fact]
public void LoadDriver_ExplicitEntrypointStillWorks()
{
- string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
- + "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n";
-
- var (dllPath, tomlPath) = CreateTestFilePair("entrypoint_test", toml);
+ (string dllPath, string tomlPath, string realAssemblyPath) =
+ CreateTestFilesWithRelativeDriver("entrypoint_test", typeName);
// 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");
Assert.NotNull(driver);
- Assert.IsType(driver);
+ Assert.Equal(typeName, driver.GetType().FullName);
}
[Fact]
@@ -214,31 +232,38 @@ public void LoadDriver_RelativePathInManifest_ResolvedCorrectly()
AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
Assert.NotNull(driver);
- Assert.IsType(driver);
+ Assert.Equal(typeName, driver.GetType().FullName);
}
[Fact]
public void LoadDriver_DifferentExtensions_AllDetectManifest()
{
- string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
string typeName = typeof(FakeAdbcDriver).FullName!;
- string escapedPath = assemblyPath.Replace("\\", "\\\\");
- string toml = "version = 1\n"
- + "driver = \"" + escapedPath + "\"\n"
- + "driver_type = \"" + typeName + "\"\n";
-
- // Test with .dll extension
- var (dll1, _) = CreateTestFilePair("test.driver", toml);
+ // Test with .dll extension - use relative path helper
+ (string dll1, string toml1, string real1) = CreateTestFilesWithRelativeDriver("test.driver", typeName);
AdbcDriver driver1 = AdbcDriverManager.LoadDriver(dll1);
Assert.NotNull(driver1);
- Assert.IsType(driver1);
+ Assert.Equal(typeName, driver1.GetType().FullName);
+
+ // Test with .so extension - also with relative path
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string assemblyFileName = Path.GetFileName(assemblyPath);
- // Test with .so extension
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
_tempDirs.Add(tempDir);
+ // Copy the real assembly
+ string copiedAssemblyPath = Path.Combine(tempDir, assemblyFileName);
+ File.Copy(assemblyPath, copiedAssemblyPath, overwrite: true);
+ _tempFiles.Add(copiedAssemblyPath);
+
+ // Create manifest with relative path
+ string toml = "version = 1\n"
+ + "driver = \"" + assemblyFileName + "\"\n"
+ + "driver_type = \"" + typeName + "\"\n";
+
string soPath = Path.Combine(tempDir, "test.driver.so");
string soToml = Path.Combine(tempDir, "test.driver.toml");
File.WriteAllText(soPath, "fake");
@@ -249,7 +274,37 @@ public void LoadDriver_DifferentExtensions_AllDetectManifest()
// Should auto-detect manifest even with .so extension
AdbcDriver driver2 = AdbcDriverManager.LoadDriver(soPath);
Assert.NotNull(driver2);
- Assert.IsType(driver2);
+ Assert.Equal(typeName, driver2.GetType().FullName);
+ }
+
+ [Fact]
+ public void LoadManagedDriver_LoadsDirectly()
+ {
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+
+ // LoadManagedDriver loads directly from the provided assembly path and type name
+ // Note: This bypasses manifest entirely and loads directly from absolute path
+ AdbcDriver driver = AdbcDriverManager.LoadManagedDriver(assemblyPath, typeName);
+ Assert.NotNull(driver);
+ // Use type name comparison to avoid assembly identity issues when loaded from different path
+ Assert.Equal(typeName, driver.GetType().FullName);
+ }
+
+ [Fact]
+ 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.
+ string typeName = typeof(FakeAdbcDriver).FullName!;
+ string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+
+ // LoadManagedDriver loads directly from the provided path
+ AdbcDriver driver = AdbcDriverManager.LoadManagedDriver(assemblyPath, typeName);
+ Assert.NotNull(driver);
+ Assert.Equal(typeName, driver.GetType().FullName);
}
}
}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
new file mode 100644
index 0000000000..d641e16d39
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
@@ -0,0 +1,575 @@
+/*
+ * 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 functionality.
+ ///
+ public class DriverManagerSecurityTests : IDisposable
+ {
+ private readonly List _tempDirs = new List();
+
+ public void Dispose()
+ {
+ // Reset global state after each test
+ DriverManagerSecurity.AuditLogger = null;
+ DriverManagerSecurity.Allowlist = null;
+
+ foreach (string dir in _tempDirs)
+ {
+ try
+ {
+ if (Directory.Exists(dir))
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+ catch { }
+ }
+ }
+
+ private string CreateTempDirectory()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ _tempDirs.Add(dir);
+ return dir;
+ }
+
+ // -----------------------------------------------------------------------
+ // Path Traversal Protection Tests
+ // -----------------------------------------------------------------------
+
+ [Theory]
+ [InlineData("..\\malicious.dll")]
+ [InlineData("../malicious.dll")]
+ [InlineData("subdir\\..\\..\\malicious.dll")]
+ [InlineData("subdir/../../../malicious.dll")]
+ [InlineData("foo\\..\\bar\\..\\..\\evil.dll")]
+ public void ValidatePathSecurity_PathTraversal_ThrowsException(string maliciousPath)
+ {
+ AdbcException ex = Assert.Throws(
+ () => DriverManagerSecurity.ValidatePathSecurity(maliciousPath, "testPath"));
+
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ Assert.Contains("..", ex.Message);
+ }
+
+ [Theory]
+ [InlineData("driver.dll")]
+ [InlineData("subdir\\driver.dll")]
+ [InlineData("subdir/driver.dll")]
+ [InlineData("C:\\Program Files\\Driver\\driver.dll")]
+ [InlineData("/usr/lib/driver.so")]
+ public void ValidatePathSecurity_ValidPath_DoesNotThrow(string validPath)
+ {
+ // Should not throw
+ DriverManagerSecurity.ValidatePathSecurity(validPath, "testPath");
+ }
+
+ [Fact]
+ public void ValidatePathSecurity_NullByte_ThrowsException()
+ {
+ string pathWithNull = "driver\0.dll";
+
+ AdbcException ex = Assert.Throws(
+ () => DriverManagerSecurity.ValidatePathSecurity(pathWithNull, "testPath"));
+
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ Assert.Contains("null", ex.Message.ToLower());
+ }
+
+ [Fact]
+ public void ValidatePathSecurity_NullOrEmpty_DoesNotThrow()
+ {
+ // Null and empty should be allowed (validation happens elsewhere)
+ DriverManagerSecurity.ValidatePathSecurity(null!, "testPath");
+ DriverManagerSecurity.ValidatePathSecurity(string.Empty, "testPath");
+ }
+
+ // -----------------------------------------------------------------------
+ // Manifest Path Resolution Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ValidateAndResolveManifestPath_ValidRelativePath_ResolvesCorrectly()
+ {
+ string manifestDir = CreateTempDirectory();
+ string subDir = Path.Combine(manifestDir, "drivers");
+ Directory.CreateDirectory(subDir);
+
+ string resolved = DriverManagerSecurity.ValidateAndResolveManifestPath(
+ manifestDir, "drivers\\mydriver.dll");
+
+ Assert.Equal(Path.Combine(manifestDir, "drivers", "mydriver.dll"), resolved);
+ }
+
+ [Fact]
+ public void ValidateAndResolveManifestPath_PathTraversal_ThrowsException()
+ {
+ string manifestDir = CreateTempDirectory();
+
+ AdbcException ex = Assert.Throws(
+ () => DriverManagerSecurity.ValidateAndResolveManifestPath(
+ manifestDir, "..\\..\\malicious.dll"));
+
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ [Fact]
+ public void ValidateAndResolveManifestPath_EscapesDirectory_ThrowsException()
+ {
+ string manifestDir = CreateTempDirectory();
+
+ // Even without .. in the literal string, symlinks or other means
+ // could cause path escape - the method should catch this
+ AdbcException ex = Assert.Throws(
+ () => DriverManagerSecurity.ValidateAndResolveManifestPath(
+ manifestDir, "subdir\\..\\..\\escape.dll"));
+
+ Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+ }
+
+ // -----------------------------------------------------------------------
+ // Directory Allowlist Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void DirectoryAllowlist_DriverInAllowedDirectory_ReturnsTrue()
+ {
+ string allowedDir = CreateTempDirectory();
+ DirectoryAllowlist allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ string driverPath = Path.Combine(allowedDir, "mydriver.dll");
+
+ Assert.True(allowlist.IsDriverAllowed(driverPath, null));
+ Assert.True(allowlist.IsDriverAllowed(driverPath, "Some.Type.Name"));
+ }
+
+ [Fact]
+ public void DirectoryAllowlist_DriverInSubdirectory_ReturnsTrue()
+ {
+ string allowedDir = CreateTempDirectory();
+ string subDir = Path.Combine(allowedDir, "subdir");
+ Directory.CreateDirectory(subDir);
+
+ DirectoryAllowlist allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ string driverPath = Path.Combine(subDir, "mydriver.dll");
+
+ Assert.True(allowlist.IsDriverAllowed(driverPath, null));
+ }
+
+ [Fact]
+ public void DirectoryAllowlist_DriverNotInAllowedDirectory_ReturnsFalse()
+ {
+ string allowedDir = CreateTempDirectory();
+ string otherDir = CreateTempDirectory();
+
+ DirectoryAllowlist allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ string driverPath = Path.Combine(otherDir, "mydriver.dll");
+
+ Assert.False(allowlist.IsDriverAllowed(driverPath, null));
+ }
+
+ [Fact]
+ public void DirectoryAllowlist_MultipleDirectories_WorksCorrectly()
+ {
+ string dir1 = CreateTempDirectory();
+ string dir2 = CreateTempDirectory();
+ string dir3 = CreateTempDirectory();
+
+ DirectoryAllowlist allowlist = new DirectoryAllowlist(new[] { dir1, dir2 });
+
+ Assert.True(allowlist.IsDriverAllowed(Path.Combine(dir1, "a.dll"), null));
+ Assert.True(allowlist.IsDriverAllowed(Path.Combine(dir2, "b.dll"), null));
+ Assert.False(allowlist.IsDriverAllowed(Path.Combine(dir3, "c.dll"), null));
+ }
+
+ [Fact]
+ public void DirectoryAllowlist_EmptyPath_ReturnsFalse()
+ {
+ string allowedDir = CreateTempDirectory();
+ DirectoryAllowlist allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ Assert.False(allowlist.IsDriverAllowed(string.Empty, null));
+ Assert.False(allowlist.IsDriverAllowed(null!, null));
+ }
+
+ // -----------------------------------------------------------------------
+ // Type Allowlist Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void TypeAllowlist_AllowedType_ReturnsTrue()
+ {
+ TypeAllowlist allowlist = new TypeAllowlist(new[]
+ {
+ "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver",
+ "Apache.Arrow.Adbc.Drivers.Snowflake.SnowflakeDriver"
+ });
+
+ Assert.True(allowlist.IsDriverAllowed(
+ "any_path.dll",
+ "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"));
+ }
+
+ [Fact]
+ public void TypeAllowlist_NotAllowedType_ReturnsFalse()
+ {
+ TypeAllowlist allowlist = new TypeAllowlist(new[]
+ {
+ "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
+ });
+
+ Assert.False(allowlist.IsDriverAllowed(
+ "any_path.dll",
+ "Evil.Malware.Driver"));
+ }
+
+ [Fact]
+ public void TypeAllowlist_NativeDriver_PermittedByDefault()
+ {
+ TypeAllowlist allowlist = new TypeAllowlist(new[]
+ {
+ "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
+ });
+
+ // Native drivers (typeName = null) are permitted by default when no inner allowlist
+ Assert.True(allowlist.IsDriverAllowed("native_driver.dll", null));
+ }
+
+ [Fact]
+ public void TypeAllowlist_NativeDriverWithInnerAllowlist_DelegatesToInner()
+ {
+ string allowedDir = CreateTempDirectory();
+ string otherDir = CreateTempDirectory();
+
+ DirectoryAllowlist dirAllowlist = new DirectoryAllowlist(new[] { allowedDir });
+ TypeAllowlist allowlist = new TypeAllowlist(
+ new[] { "Some.Type" },
+ nativeDriverAllowlist: dirAllowlist);
+
+ // Native driver in allowed directory
+ Assert.True(allowlist.IsDriverAllowed(
+ Path.Combine(allowedDir, "native.dll"), null));
+
+ // Native driver in disallowed directory
+ Assert.False(allowlist.IsDriverAllowed(
+ Path.Combine(otherDir, "native.dll"), null));
+ }
+
+ // -----------------------------------------------------------------------
+ // Composite Allowlist Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void CompositeAllowlist_AllAllowlistsPermit_ReturnsTrue()
+ {
+ string allowedDir = CreateTempDirectory();
+
+ DirectoryAllowlist dirAllowlist = new DirectoryAllowlist(new[] { allowedDir });
+ TypeAllowlist typeAllowlist = new TypeAllowlist(new[] { "Good.Driver" });
+
+ CompositeAllowlist composite = new CompositeAllowlist(dirAllowlist, typeAllowlist);
+
+ Assert.True(composite.IsDriverAllowed(
+ Path.Combine(allowedDir, "driver.dll"),
+ "Good.Driver"));
+ }
+
+ [Fact]
+ public void CompositeAllowlist_OneAllowlistDenies_ReturnsFalse()
+ {
+ string allowedDir = CreateTempDirectory();
+ string otherDir = CreateTempDirectory();
+
+ DirectoryAllowlist dirAllowlist = new DirectoryAllowlist(new[] { allowedDir });
+ TypeAllowlist typeAllowlist = new TypeAllowlist(new[] { "Good.Driver" });
+
+ CompositeAllowlist composite = new CompositeAllowlist(dirAllowlist, typeAllowlist);
+
+ // Wrong directory
+ Assert.False(composite.IsDriverAllowed(
+ Path.Combine(otherDir, "driver.dll"),
+ "Good.Driver"));
+
+ // Wrong type
+ Assert.False(composite.IsDriverAllowed(
+ Path.Combine(allowedDir, "driver.dll"),
+ "Bad.Driver"));
+ }
+
+ // -----------------------------------------------------------------------
+ // Audit Logger Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void AuditLogger_CanBeSetAndRetrieved()
+ {
+ TestAuditLogger logger = new TestAuditLogger();
+
+ DriverManagerSecurity.AuditLogger = logger;
+
+ Assert.Same(logger, DriverManagerSecurity.AuditLogger);
+ }
+
+ [Fact]
+ public void AuditLogger_SetToNull_DisablesLogging()
+ {
+ TestAuditLogger logger = new TestAuditLogger();
+ DriverManagerSecurity.AuditLogger = logger;
+
+ DriverManagerSecurity.AuditLogger = null;
+
+ Assert.Null(DriverManagerSecurity.AuditLogger);
+ }
+
+ [Fact]
+ public void LogDriverLoadAttempt_WithLogger_LogsAttempt()
+ {
+ TestAuditLogger logger = new TestAuditLogger();
+ DriverManagerSecurity.AuditLogger = logger;
+
+ DriverLoadAttempt attempt = new DriverLoadAttempt(
+ driverPath: "C:\\test\\driver.dll",
+ typeName: "Test.Driver",
+ manifestPath: "C:\\test\\driver.toml",
+ success: true,
+ errorMessage: null,
+ loadMethod: "LoadDriver");
+
+ DriverManagerSecurity.LogDriverLoadAttempt(attempt);
+
+ Assert.Single(logger.Attempts);
+ Assert.Equal("C:\\test\\driver.dll", logger.Attempts[0].DriverPath);
+ Assert.Equal("Test.Driver", logger.Attempts[0].TypeName);
+ Assert.True(logger.Attempts[0].Success);
+ }
+
+ [Fact]
+ public void LogDriverLoadAttempt_WithoutLogger_DoesNotThrow()
+ {
+ DriverManagerSecurity.AuditLogger = null;
+
+ DriverLoadAttempt attempt = new DriverLoadAttempt(
+ driverPath: "C:\\test\\driver.dll",
+ typeName: null,
+ manifestPath: null,
+ success: false,
+ errorMessage: "Test error",
+ loadMethod: "LoadDriver");
+
+ // Should not throw
+ DriverManagerSecurity.LogDriverLoadAttempt(attempt);
+ }
+
+ [Fact]
+ public void LogDriverLoadAttempt_LoggerThrows_DoesNotPropagate()
+ {
+ ThrowingAuditLogger logger = new ThrowingAuditLogger();
+ DriverManagerSecurity.AuditLogger = logger;
+
+ DriverLoadAttempt attempt = new DriverLoadAttempt(
+ driverPath: "test.dll",
+ typeName: null,
+ manifestPath: null,
+ success: true,
+ errorMessage: null,
+ loadMethod: "Test");
+
+ // Should not throw even though logger throws
+ DriverManagerSecurity.LogDriverLoadAttempt(attempt);
+ }
+
+ // -----------------------------------------------------------------------
+ // Allowlist Validation Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ValidateAllowlist_NoAllowlistConfigured_DoesNotThrow()
+ {
+ DriverManagerSecurity.Allowlist = null;
+
+ // Should not throw when no allowlist is configured
+ DriverManagerSecurity.ValidateAllowlist("any_path.dll", null);
+ DriverManagerSecurity.ValidateAllowlist("any_path.dll", "Any.Type");
+ }
+
+ [Fact]
+ public void ValidateAllowlist_DriverAllowed_DoesNotThrow()
+ {
+ string allowedDir = CreateTempDirectory();
+ DriverManagerSecurity.Allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ string driverPath = Path.Combine(allowedDir, "driver.dll");
+
+ // Should not throw
+ DriverManagerSecurity.ValidateAllowlist(driverPath, null);
+ }
+
+ [Fact]
+ public void ValidateAllowlist_DriverNotAllowed_ThrowsUnauthorized()
+ {
+ string allowedDir = CreateTempDirectory();
+ string otherDir = CreateTempDirectory();
+ DriverManagerSecurity.Allowlist = new DirectoryAllowlist(new[] { allowedDir });
+
+ string driverPath = Path.Combine(otherDir, "driver.dll");
+
+ AdbcException ex = Assert.Throws(
+ () => DriverManagerSecurity.ValidateAllowlist(driverPath, null));
+
+ Assert.Equal(AdbcStatusCode.Unauthorized, ex.Status);
+ Assert.Contains("allowlist", ex.Message.ToLower());
+ }
+
+ // -----------------------------------------------------------------------
+ // DriverLoadAttempt Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void DriverLoadAttempt_Constructor_SetsAllProperties()
+ {
+ DateTime before = DateTime.UtcNow;
+
+ DriverLoadAttempt attempt = new DriverLoadAttempt(
+ driverPath: "C:\\drivers\\test.dll",
+ typeName: "Test.Driver.Type",
+ manifestPath: "C:\\drivers\\test.toml",
+ success: false,
+ errorMessage: "Test error message",
+ loadMethod: "LoadManagedDriver");
+
+ DateTime after = DateTime.UtcNow;
+
+ Assert.Equal("C:\\drivers\\test.dll", attempt.DriverPath);
+ Assert.Equal("Test.Driver.Type", attempt.TypeName);
+ Assert.Equal("C:\\drivers\\test.toml", attempt.ManifestPath);
+ Assert.False(attempt.Success);
+ Assert.Equal("Test error message", attempt.ErrorMessage);
+ Assert.Equal("LoadManagedDriver", attempt.LoadMethod);
+ Assert.True(attempt.TimestampUtc >= before && attempt.TimestampUtc <= after);
+ }
+
+ [Fact]
+ public void DriverLoadAttempt_NullDriverPath_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new DriverLoadAttempt(
+ driverPath: null!,
+ typeName: null,
+ manifestPath: null,
+ success: true,
+ errorMessage: null,
+ loadMethod: "Test"));
+ }
+
+ [Fact]
+ public void DriverLoadAttempt_NullLoadMethod_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new DriverLoadAttempt(
+ driverPath: "test.dll",
+ typeName: null,
+ manifestPath: null,
+ success: true,
+ errorMessage: null,
+ loadMethod: null!));
+ }
+
+ // -----------------------------------------------------------------------
+ // Thread Safety Tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void AuditLogger_ConcurrentAccess_IsThreadSafe()
+ {
+ TestAuditLogger logger1 = new TestAuditLogger();
+ TestAuditLogger logger2 = new TestAuditLogger();
+
+ System.Threading.Tasks.Parallel.For(0, 100, i =>
+ {
+ if (i % 2 == 0)
+ {
+ DriverManagerSecurity.AuditLogger = logger1;
+ }
+ else
+ {
+ DriverManagerSecurity.AuditLogger = logger2;
+ }
+
+ IDriverLoadAuditLogger? current = DriverManagerSecurity.AuditLogger;
+ // Just verify we can read without exception
+ Assert.True(current == null || current == logger1 || current == logger2);
+ });
+ }
+
+ [Fact]
+ public void Allowlist_ConcurrentAccess_IsThreadSafe()
+ {
+ string dir1 = CreateTempDirectory();
+ string dir2 = CreateTempDirectory();
+
+ DirectoryAllowlist allowlist1 = new DirectoryAllowlist(new[] { dir1 });
+ DirectoryAllowlist allowlist2 = new DirectoryAllowlist(new[] { dir2 });
+
+ System.Threading.Tasks.Parallel.For(0, 100, i =>
+ {
+ if (i % 2 == 0)
+ {
+ DriverManagerSecurity.Allowlist = allowlist1;
+ }
+ else
+ {
+ DriverManagerSecurity.Allowlist = allowlist2;
+ }
+
+ IDriverAllowlist? current = DriverManagerSecurity.Allowlist;
+ Assert.True(current == null || current == allowlist1 || current == allowlist2);
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Helper Classes
+ // -----------------------------------------------------------------------
+
+ private class TestAuditLogger : IDriverLoadAuditLogger
+ {
+ public List Attempts { get; } = new List();
+
+ public void LogDriverLoadAttempt(DriverLoadAttempt attempt)
+ {
+ Attempts.Add(attempt);
+ }
+ }
+
+ private class ThrowingAuditLogger : IDriverLoadAuditLogger
+ {
+ public void LogDriverLoadAttempt(DriverLoadAttempt attempt)
+ {
+ throw new InvalidOperationException("Intentional test exception");
+ }
+ }
+ }
+}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
index a0e4700a46..2116591d7e 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -447,7 +447,7 @@ public void LoadDriverFromProfile_ProfileWithNoDriver_ThrowsAdbcException()
[Fact]
public void FilesystemProfileProvider_UnknownProfile_ReturnsNull()
{
- var provider = new FilesystemProfileProvider();
+ FilesystemProfileProvider provider = new FilesystemProfileProvider();
IConnectionProfile? result = provider.GetProfile(
"definitely_not_a_real_profile_xyz",
additionalSearchPathList: Path.GetTempPath());
@@ -457,7 +457,7 @@ public void FilesystemProfileProvider_UnknownProfile_ReturnsNull()
[Fact]
public void FilesystemProfileProvider_NullName_ThrowsArgumentNullException()
{
- var provider = new FilesystemProfileProvider();
+ FilesystemProfileProvider provider = new FilesystemProfileProvider();
Assert.Throws(() => provider.GetProfile(null!));
}
@@ -481,7 +481,7 @@ public void FilesystemProfileProvider_ProfileExistsInAdditionalPath_ReturnsProfi
File.WriteAllText(filePath, toml, System.Text.Encoding.UTF8);
try
{
- var provider = new FilesystemProfileProvider();
+ FilesystemProfileProvider provider = new FilesystemProfileProvider();
IConnectionProfile? profile = provider.GetProfile("myprofile", additionalSearchPathList: dir);
Assert.NotNull(profile);
@@ -503,7 +503,7 @@ public void FilesystemProfileProvider_AbsoluteTomlPath_LoadsDirectly()
driver = ""abs_driver""
";
string path = WriteTempToml(toml);
- var provider = new FilesystemProfileProvider();
+ FilesystemProfileProvider provider = new FilesystemProfileProvider();
IConnectionProfile? profile = provider.GetProfile(path);
Assert.NotNull(profile);
@@ -846,7 +846,7 @@ public void BuildStringOptions_WithExplicitOptions_MergesCorrectly()
";
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
- var explicitOptions = new Dictionary
+ Dictionary explicitOptions = new Dictionary
{
{ "explicit_key", "from_explicit" },
{ "shared_key", "explicit_value" }
@@ -892,7 +892,7 @@ public void BuildStringOptions_EmptyExplicitOptions_UsesOnlyProfileOptions()
key = ""value""
";
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
- var empty = new Dictionary();
+ Dictionary empty = new Dictionary();
IReadOnlyDictionary opts = AdbcDriverManager.BuildStringOptions(profile, empty);
Assert.Equal("value", opts["key"]);
@@ -913,7 +913,7 @@ public void BuildStringOptions_ExplicitOptionsOverrideAllProfileTypes()
";
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
- var explicitOptions = new Dictionary
+ Dictionary explicitOptions = new Dictionary
{
{ "str_key", "explicit_string" },
{ "int_key", "999" },
@@ -950,7 +950,7 @@ public void OpenDatabaseFromProfile_WithExplicitOptions_MergesCorrectly()
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
- var explicitOptions = new Dictionary
+ Dictionary explicitOptions = new Dictionary
{
{ "explicit_option", "from_explicit" },
{ "shared_option", "explicit_value" }
From 14063227979877f8ffcf0e792be65e3269392217 Mon Sep 17 00:00:00 2001
From: David Coe <>
Date: Thu, 12 Mar 2026 23:27:04 -0400
Subject: [PATCH 4/5] fix tests for GitHub
---
.../DriverManagerSecurityTests.cs | 12 +++--
.../TomlConnectionProfileTests.cs | 52 ++++++++++++++-----
2 files changed, 48 insertions(+), 16 deletions(-)
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
index d641e16d39..2c836dede4 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManagerSecurityTests.cs
@@ -119,8 +119,10 @@ public void ValidateAndResolveManifestPath_ValidRelativePath_ResolvesCorrectly()
string subDir = Path.Combine(manifestDir, "drivers");
Directory.CreateDirectory(subDir);
+ // Use Path.Combine for the relative path to be cross-platform
+ string relativePath = Path.Combine("drivers", "mydriver.dll");
string resolved = DriverManagerSecurity.ValidateAndResolveManifestPath(
- manifestDir, "drivers\\mydriver.dll");
+ manifestDir, relativePath);
Assert.Equal(Path.Combine(manifestDir, "drivers", "mydriver.dll"), resolved);
}
@@ -130,9 +132,11 @@ public void ValidateAndResolveManifestPath_PathTraversal_ThrowsException()
{
string manifestDir = CreateTempDirectory();
+ // Use ".." with platform-specific separator
+ string maliciousPath = ".." + Path.DirectorySeparatorChar + ".." + Path.DirectorySeparatorChar + "malicious.dll";
AdbcException ex = Assert.Throws(
() => DriverManagerSecurity.ValidateAndResolveManifestPath(
- manifestDir, "..\\..\\malicious.dll"));
+ manifestDir, maliciousPath));
Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
}
@@ -144,9 +148,11 @@ public void ValidateAndResolveManifestPath_EscapesDirectory_ThrowsException()
// Even without .. in the literal string, symlinks or other means
// could cause path escape - the method should catch this
+ string escapePath = "subdir" + Path.DirectorySeparatorChar + ".." +
+ Path.DirectorySeparatorChar + ".." + Path.DirectorySeparatorChar + "escape.dll";
AdbcException ex = Assert.Throws(
() => DriverManagerSecurity.ValidateAndResolveManifestPath(
- manifestDir, "subdir\\..\\..\\escape.dll"));
+ manifestDir, escapePath));
Assert.Equal(AdbcStatusCode.InvalidArgument, 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 2116591d7e..1fbae9a5be 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -551,7 +551,8 @@ public void LoadManagedDriver_ValidType_ReturnsDriverInstance()
AdbcDriver driver = AdbcDriverManager.LoadManagedDriver(assemblyPath, typeName);
Assert.NotNull(driver);
- Assert.IsType(driver);
+ // Use type name comparison to avoid assembly identity issues when loaded via Assembly.LoadFrom
+ Assert.Equal(typeName, driver.GetType().FullName);
}
// -----------------------------------------------------------------------
@@ -615,9 +616,15 @@ public void OpenDatabaseFromProfile_ManagedDriver_OpensDatabase()
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile);
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
- Assert.Equal("my-project", fakeDb.Parameters["project_id"]);
- Assert.Equal("us-east1", fakeDb.Parameters["region"]);
+ // 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"]);
}
// -----------------------------------------------------------------------
@@ -822,10 +829,17 @@ public void OpenDatabaseFromProfile_UnknownOptionsPassThroughToDriver()
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile);
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ // 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)!;
+
// Both keys arrive at the driver; the driver decides what to do with them.
- Assert.Equal("hello", fakeDb.Parameters["known_key"]);
- Assert.Equal("ignored_by_driver", fakeDb.Parameters["unknown_widget"]);
+ Assert.Equal("hello", parameters["known_key"]);
+ Assert.Equal("ignored_by_driver", parameters["unknown_widget"]);
}
// -----------------------------------------------------------------------
@@ -958,16 +972,22 @@ public void OpenDatabaseFromProfile_WithExplicitOptions_MergesCorrectly()
AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile, explicitOptions);
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
+ // 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", fakeDb.Parameters["profile_option"]);
+ Assert.Equal("from_profile", parameters["profile_option"]);
// Explicit-only option should be present
- Assert.Equal("from_explicit", fakeDb.Parameters["explicit_option"]);
+ Assert.Equal("from_explicit", parameters["explicit_option"]);
// Shared option: explicit should override profile
- Assert.Equal("explicit_value", fakeDb.Parameters["shared_option"]);
+ Assert.Equal("explicit_value", parameters["shared_option"]);
}
[Fact]
@@ -986,8 +1006,14 @@ public void OpenDatabaseFromProfile_NullExplicitOptions_UsesOnlyProfile()
TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
AdbcDatabase db = AdbcDriverManager.OpenDatabaseFromProfile(profile, null);
- FakeAdbcDatabase fakeDb = Assert.IsType(db);
- Assert.Equal("value", fakeDb.Parameters["key"]);
+ // 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)!;
+ Assert.Equal("value", parameters["key"]);
}
}
}
From 7e1391209639a3d40364b3282e6c8f36d05cf40a Mon Sep 17 00:00:00 2001
From: David Coe <>
Date: Fri, 13 Mar 2026 13:03:10 -0400
Subject: [PATCH 5/5] update profile parsing
---
.../DriverManager/AdbcDriverManager.cs | 24 ++++-
.../DriverManager/ManagedDriverLoader.cs | 95 +++++++++++++++++++
.../DriverManager/TomlConnectionProfile.cs | 48 ++++++++--
.../Apache.Arrow.Adbc/DriverManager/readme.md | 50 ++++++++--
.../TomlConnectionProfileTests.cs | 82 +++++++++++++++-
5 files changed, 276 insertions(+), 23 deletions(-)
create mode 100644 csharp/src/Apache.Arrow.Adbc/DriverManager/ManagedDriverLoader.cs
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
index 23fcd97544..70f4dace21 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -235,27 +235,45 @@ public static AdbcDriver LoadDriverFromProfile(
public static AdbcDriver LoadManagedDriver(string assemblyPath, string typeName)
{
if (string.IsNullOrEmpty(assemblyPath))
+ {
throw new ArgumentException("Assembly path must not be null or empty.", nameof(assemblyPath));
+ }
if (string.IsNullOrEmpty(typeName))
+ {
throw new ArgumentException("Type name must not be null or empty.", nameof(typeName));
+ }
Assembly assembly;
try
{
- assembly = Assembly.LoadFrom(assemblyPath);
+ assembly = ManagedDriverLoader.LoadAssembly(assemblyPath);
+ }
+ catch (FileNotFoundException ex)
+ {
+ throw new AdbcException(
+ $"Driver assembly not found: {ex.FileName}",
+ AdbcStatusCode.NotFound);
+ }
+ catch (BadImageFormatException ex)
+ {
+ throw new AdbcException(
+ $"Driver assembly has an invalid format (possibly wrong platform or not a .NET assembly): {ex.Message}",
+ AdbcStatusCode.InvalidArgument);
}
catch (Exception ex)
{
throw new AdbcException(
- $"Failed to load managed driver assembly '{assemblyPath}': {ex.Message}",
+ $"Failed to load managed driver assembly: {ex.Message}",
AdbcStatusCode.IOError);
}
Type? driverType = assembly.GetType(typeName, throwOnError: false);
if (driverType == null)
+ {
throw new AdbcException(
- $"Type '{typeName}' was not found in assembly '{assemblyPath}'.",
+ $"Type '{typeName}' was not found in the assembly.",
AdbcStatusCode.NotFound);
+ }
if (!typeof(AdbcDriver).IsAssignableFrom(driverType))
throw new AdbcException(
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/ManagedDriverLoader.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/ManagedDriverLoader.cs
new file mode 100644
index 0000000000..27815a0a97
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/ManagedDriverLoader.cs
@@ -0,0 +1,95 @@
+/*
+ * 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.IO;
+using System.Reflection;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+ ///
+ /// Provides cross-platform assembly loading functionality for managed ADBC drivers.
+ ///
+ ///
+ ///
+ /// This class abstracts the differences between .NET Framework and .NET Core/.NET 8
+ /// assembly loading, providing a consistent interface for loading driver assemblies.
+ ///
+ ///
+ /// Important Notes for .NET Core/.NET 8:
+ ///
+ ///
+ /// -
+ ///
+ /// Driver assemblies should include a .deps.json file for proper dependency
+ /// resolution. This file is automatically generated when building with the .NET SDK.
+ ///
+ ///
+ /// -
+ ///
+ /// Driver dependencies should be located in the same directory as the driver assembly
+ /// or in a subdirectory following standard .NET runtime conventions.
+ ///
+ ///
+ /// -
+ ///
+ /// For self-contained drivers with native dependencies, ensure all native libraries
+ /// are in the appropriate runtime-specific subdirectories.
+ ///
+ ///
+ ///
+ ///
+ internal static class ManagedDriverLoader
+ {
+ ///
+ /// Loads an assembly from the specified path.
+ ///
+ /// The path to the assembly to load.
+ /// The loaded assembly.
+ /// The assembly file was not found.
+ /// The file is not a valid assembly.
+ ///
+ ///
+ /// On .NET Core/.NET 8, this method uses which loads
+ /// the assembly into the default AssemblyLoadContext. Dependencies are resolved
+ /// using the standard .NET Core dependency resolution mechanism, which includes:
+ ///
+ ///
+ /// - The .deps.json file if present
+ /// - The assembly's directory
+ /// - The application's probing paths
+ ///
+ ///
+ /// For advanced scenarios requiring isolated loading or explicit dependency resolution,
+ /// consider using AssemblyLoadContext directly in .NET Core/.NET 8 applications.
+ ///
+ ///
+ internal static Assembly LoadAssembly(string assemblyPath)
+ {
+ string fullPath = Path.GetFullPath(assemblyPath);
+
+ if (!File.Exists(fullPath))
+ {
+ throw new FileNotFoundException($"Assembly not found: {fullPath}", fullPath);
+ }
+
+ // Use LoadFrom which works on both .NET Framework and .NET Core
+ // On .NET Core, this loads into AssemblyLoadContext.Default
+ return Assembly.LoadFrom(fullPath);
+ }
+ }
+}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
index f1888507fb..6f993dccdd 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlConnectionProfile.cs
@@ -25,27 +25,41 @@ namespace Apache.Arrow.Adbc.DriverManager
/// An loaded from a TOML file.
///
///
- /// The expected file format is:
+ ///
+ /// The expected file format for profiles is:
+ ///
///
- /// version = 1
+ /// profile_version = 1
/// driver = "driver_name"
///
- /// [options]
+ /// [Options]
/// option1 = "value1"
/// option2 = 42
/// option3 = 3.14
///
+ ///
+ /// For backward compatibility, the following legacy field names are also supported:
+ ///
+ ///
+ /// - version (use profile_version instead)
+ /// - [options] section (use [Options] instead)
+ ///
+ ///
/// Boolean option values are converted to the string equivalents "true" or
/// "false" and placed in .
/// Integer values are placed in and double values in
- /// (integer values are also reflected in
- /// ).
+ /// .
/// Values of the form env_var(ENV_VAR_NAME) are expanded from the named
/// environment variable when is called.
+ ///
///
public sealed class TomlConnectionProfile : IConnectionProfile
{
- private const string OptionsSection = "options";
+ // New spec names
+ private const string OptionsSectionNew = "Options";
+ // Legacy names (for backward compatibility)
+ private const string OptionsSectionLegacy = "options";
+
private const string EnvVarPrefix = "env_var(";
private readonly Dictionary _stringOptions;
@@ -119,7 +133,14 @@ public static TomlConnectionProfile FromContent(string tomlContent)
Dictionary intOpts = new Dictionary(StringComparer.Ordinal);
Dictionary doubleOpts = new Dictionary(StringComparer.Ordinal);
- if (sections.TryGetValue(OptionsSection, out Dictionary? optSection))
+ // Try new section name first, fall back to legacy
+ Dictionary? optSection = null;
+ if (!sections.TryGetValue(OptionsSectionNew, out optSection))
+ {
+ sections.TryGetValue(OptionsSectionLegacy, out optSection);
+ }
+
+ if (optSection != null)
{
foreach (KeyValuePair kv in optSection)
{
@@ -204,10 +225,17 @@ public void Dispose()
private static void ValidateVersion(Dictionary root)
{
- if (!root.TryGetValue("version", out object? versionObj))
+ // Try new field name first, fall back to legacy
+ object? versionObj = null;
+ if (!root.TryGetValue("profile_version", out versionObj))
+ {
+ root.TryGetValue("version", out versionObj);
+ }
+
+ if (versionObj == null)
{
throw new AdbcException(
- "TOML profile is missing the required 'version' field.",
+ "TOML profile is missing the required 'profile_version' field.",
AdbcStatusCode.InvalidArgument);
}
@@ -225,7 +253,7 @@ private static void ValidateVersion(Dictionary root)
catch (Exception ex) when (ex is FormatException || ex is InvalidCastException || ex is OverflowException)
{
throw new AdbcException(
- $"The 'version' field has an invalid value '{versionObj}'. It must be an integer.",
+ $"The 'profile_version' field has an invalid value '{versionObj}'. It must be an integer.",
AdbcStatusCode.InvalidArgument);
}
}
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
index f82e3c792b..ea74f20e82 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/readme.md
@@ -29,16 +29,16 @@ A .NET implementation of the ADBC Driver Manager, based on the C interface defin
## TOML Manifest / Profile Format
-### Unmanaged Driver Example (Snowflake)
+### Connection Profile Example (Snowflake)
For unmanaged drivers loaded from native shared libraries:
```toml
-version = 1
+profile_version = 1
driver = "libadbc_driver_snowflake"
entrypoint = "AdbcDriverSnowflakeInit"
-[options]
+[Options]
adbc.snowflake.sql.account = "myaccount"
adbc.snowflake.sql.warehouse = "mywarehouse"
adbc.snowflake.sql.auth_type = "auth_snowflake"
@@ -46,16 +46,16 @@ username = "myuser"
password = "env_var(SNOWFLAKE_PASSWORD)"
```
-### Managed Driver Example (BigQuery)
+### Managed Driver Profile Example (BigQuery)
For managed .NET drivers:
```toml
-version = 1
+profile_version = 1
driver = "C:\\path\\to\\Apache.Arrow.Adbc.Drivers.BigQuery.dll"
driver_type = "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
-[options]
+[Options]
adbc.bigquery.project_id = "my-project"
adbc.bigquery.auth_type = "service"
adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
@@ -63,11 +63,49 @@ adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
### Format Notes
+- 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.
+## Managed Driver Loading (.NET Core / .NET 8)
+
+When loading managed .NET drivers using `LoadManagedDriver`, the driver manager uses `Assembly.LoadFrom()` which has different behavior on .NET Core/.NET 8 compared to .NET Framework:
+
+### Dependency Resolution
+
+On .NET Core/.NET 8, dependencies are resolved in the following order:
+
+1. **The `.deps.json` file** - If present alongside the driver assembly, this file describes all dependencies and their locations. This is automatically generated when building with the .NET SDK.
+
+2. **The assembly's directory** - Dependencies in the same directory as the driver are discovered automatically.
+
+3. **The application's probing paths** - Standard .NET Core probing paths are searched.
+
+### Best Practices for Driver Authors
+
+1. **Include a `.deps.json` file** - Build your driver with the .NET SDK to automatically generate this file. It ensures all dependencies are properly resolved.
+
+2. **Deploy dependencies alongside the driver** - Place all required DLLs in the same directory as your driver assembly.
+
+3. **Consider self-contained deployment** - For maximum compatibility, publish your driver as a self-contained deployment with all dependencies included.
+
+4. **Test on both .NET Framework and .NET 8** - Assembly loading behavior differs, so test on both platforms if you support both.
+
+### Advanced Scenarios
+
+For applications requiring isolated loading or explicit dependency resolution, consider using `AssemblyLoadContext` directly:
+
+```csharp
+// Example of isolated loading (requires .NET Core 3.0+)
+var loadContext = new AssemblyLoadContext("MyDriverContext", isCollectible: true);
+var assembly = loadContext.LoadFromAssemblyPath(driverPath);
+// ... use the driver ...
+loadContext.Unload(); // Unload when done (if collectible)
+```
+
## Security Features
The Driver Manager includes security features to protect against common attacks when loading drivers dynamically.
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
index 1fbae9a5be..745ad8e3dd 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -80,6 +80,79 @@ public void ParseProfile_WithAllOptionTypes_SetsCorrectValues()
Assert.Equal("true", profile.StringOptions["use_ssl"]);
}
+ // -----------------------------------------------------------------------
+ // New spec format: profile_version and [Options]
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ParseProfile_NewFormat_WithProfileVersionAndOptions_SetsCorrectValues()
+ {
+ const string toml = @"
+profile_version = 1
+driver = ""libadbc_driver_postgresql""
+
+[Options]
+uri = ""postgresql://localhost/mydb""
+port = 5432
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ Assert.Equal("libadbc_driver_postgresql", profile.DriverName);
+ Assert.Equal("postgresql://localhost/mydb", profile.StringOptions["uri"]);
+ Assert.Equal(5432L, profile.IntOptions["port"]);
+ }
+
+ [Fact]
+ public void ParseProfile_LegacyFormat_WithVersionAndLowercaseOptions_SetsCorrectValues()
+ {
+ // Legacy format should still work for backward compatibility
+ const string toml = @"
+version = 1
+driver = ""mydriver""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ Assert.Equal("mydriver", profile.DriverName);
+ Assert.Equal("value", profile.StringOptions["key"]);
+ }
+
+ [Fact]
+ public void ParseProfile_MixedFormat_ProfileVersionWithLowercaseOptions_Works()
+ {
+ // Mix of new and old should work
+ const string toml = @"
+profile_version = 1
+driver = ""mydriver""
+
+[options]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ Assert.Equal("mydriver", profile.DriverName);
+ Assert.Equal("value", profile.StringOptions["key"]);
+ }
+
+ [Fact]
+ public void ParseProfile_OptionsSectionIsCaseInsensitive()
+ {
+ // The TOML parser treats section names case-insensitively
+ // so [Options], [options], [OPTIONS] all work the same
+ const string toml = @"
+profile_version = 1
+driver = ""mydriver""
+
+[OPTIONS]
+key = ""value""
+";
+ TomlConnectionProfile profile = TomlConnectionProfile.FromContent(toml);
+
+ Assert.Equal("value", profile.StringOptions["key"]);
+ }
+
[Fact]
public void ParseProfile_WithFalseBoolean_SetsStringFalse()
{
@@ -278,6 +351,7 @@ public void AdbcLoadFlags_NoneHasNoFlags()
Assert.False(flags.HasFlag(AdbcLoadFlags.AllowRelativePaths));
}
+
// -----------------------------------------------------------------------
// Negative: missing version field
// -----------------------------------------------------------------------
@@ -293,7 +367,7 @@ public void ParseProfile_MissingVersion_ThrowsAdbcException()
";
AdbcException ex = Assert.Throws(() => TomlConnectionProfile.FromContent(toml));
Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
- Assert.Contains("version", ex.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("profile_version", ex.Message, StringComparison.OrdinalIgnoreCase);
}
// -----------------------------------------------------------------------
@@ -304,7 +378,7 @@ public void ParseProfile_MissingVersion_ThrowsAdbcException()
public void ParseProfile_UnsupportedVersion_ThrowsAdbcException()
{
const string toml = @"
-version = 99
+profile_version = 99
driver = ""mydriver""
";
AdbcException ex = Assert.Throws(() => TomlConnectionProfile.FromContent(toml));
@@ -711,12 +785,12 @@ public void BuildStringOptions_NullProfile_ThrowsArgumentNullException()
// -----------------------------------------------------------------------
[Fact]
- public void LoadManagedDriver_NonExistentAssemblyFile_ThrowsAdbcExceptionWithIOErrorStatus()
+ public void LoadManagedDriver_NonExistentAssemblyFile_ThrowsAdbcExceptionWithNotFoundStatus()
{
string missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".dll");
AdbcException ex = Assert.Throws(
() => AdbcDriverManager.LoadManagedDriver(missingPath, "Some.Type"));
- Assert.Equal(AdbcStatusCode.IOError, ex.Status);
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
}
// -----------------------------------------------------------------------