From 88e616c9509da46caf8c2e4b7fda8dd19a80c8b5 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 13:47:59 +0900 Subject: [PATCH 1/6] feat(rust/driver_manager): reconcile with C++ driver manager Closes #4089. --- docs/source/format/connection_profiles.rst | 83 +++--- docs/source/format/driver_manifests.rst | 6 +- docs/source/glossary.rst | 5 + rust/driver_manager/src/lib.rs | 8 +- rust/driver_manager/src/profile.rs | 120 ++++++-- rust/driver_manager/src/search.rs | 14 +- rust/driver_manager/tests/common/mod.rs | 27 ++ .../tests/connection_profile.rs | 280 ++++++++++++------ .../tests/test_env_var_profiles.rs | 45 +-- 9 files changed, 389 insertions(+), 199 deletions(-) diff --git a/docs/source/format/connection_profiles.rst b/docs/source/format/connection_profiles.rst index 87d8ac142c..795e7c95c4 100644 --- a/docs/source/format/connection_profiles.rst +++ b/docs/source/format/connection_profiles.rst @@ -15,30 +15,37 @@ .. specific language governing permissions and limitations .. under the License. -================================== -Driver Manager Connection Profiles -================================== +=========================================== +ADBC Driver Manager and Connection Profiles +=========================================== -Overview -======== +.. note:: This document describes using the :term:`driver manager` to load + drivers. The driver manager is not required to use ADBC in general + but it allows loading drivers written in a different language from the + application and improves the experience when using multiple drivers in + a single application. For more information on how the driver manager + works see :doc:`how_manager`. -There are two ways to pass connection options to driver managers: +There are two ways to pass driver options through the driver manager: -1. Directly specifying all connection options as arguments to driver manager functions in your - application code. (see the `SetOption` family of functions in :doc:`specification` for details) -2. Referring to a **connection profile** which contains connection options, and optionally overriding - some options in your application code. +1. Directly specifying all options as arguments to the driver manager in your + application code (see the `SetOption` family of functions in + :doc:`specification` for details). +2. Referring to a :term:`connection profile` which contains options, and + optionally overriding some options by setting them through the above + method. -The ADBC driver manager supports **connection profiles** that specify a driver and connection options -in a reusable configuration. This allows users to: +Connection profiles combine a driver and driver options in a reusable +configuration. This allows users to: - Define connection information in files or environment variables - Share connection configurations across applications - Distribute standardized connection settings - Avoid hardcoding driver names and credentials in application code -Profiles are loaded during ``AdbcDatabaseInit()`` before initializing the driver. Options -from the profile are applied automatically but do not override options already set via ``AdbcDatabaseSetOption()``. +Profiles are loaded during ``AdbcDatabaseInit()`` before initializing the +driver. Options from the profile are applied automatically but do not override +options already set via ``AdbcDatabaseSetOption()``. Quick Start =========== @@ -74,9 +81,12 @@ Filesystem-based profiles use TOML format with the following structure: .. code-block:: toml + # The version is required. profile_version = 1 + # It is optional to provide the driver. driver = "snowflake" + # The Options table is required, even if empty [Options] # String options adbc.snowflake.sql.account = "mycompany" @@ -111,18 +121,19 @@ driver The ``driver`` field specifies which ADBC driver to load. This can be: -- A driver name (e.g., ``"snowflake"``) +- A driver or driver manifest name (e.g., ``"snowflake"``) - A path to a shared library (e.g., ``"/usr/local/lib/libadbc_driver_snowflake.so"``) - A path to a driver manifest (e.g., ``"/etc/adbc/drivers/snowflake.toml"``) If omitted, the driver must be specified through other means (e.g., the ``driver`` option or ``uri`` parameter). +If the application specifies a driver, and specifies a profile that itself references a driver, the two must match exactly, or it is an error. The driver will be loaded identically to if it was specified via ``AdbcDatabaseSetOption("driver", "")``. For more detils, see :doc:`driver_manifests`. Options Section --------------- -The ``[Options]`` section contains driver-specific configuration options. Options can be of the following types: +The ``[Options]`` section contains driver-specific configuration options. This section must be present, even if empty. Options can be of the following types: **String values** Applied using ``AdbcDatabaseSetOption()`` @@ -153,6 +164,10 @@ The ``[Options]`` section contains driver-specific configuration options. Option adbc.snowflake.sql.client_session_keep_alive = true +.. warning:: If the application overrides option values but uses a different + type for the value than the profile does, it is undefined which + will take effect. + Value Substitution ------------------ @@ -190,7 +205,7 @@ Profile Search Locations When using a profile name (not an absolute path), the driver manager searches for ``.toml`` in the following locations: -1. **Additional Search Paths** (if configured via ``AdbcDriverManagerDatabaseSetAdditionalSearchPathList()``) +1. **Additional Search Paths** (if configured via ``additional_profile_search_path_list`` option) 2. **ADBC_PROFILE_PATH** environment variable (colon-separated on Unix, semicolon-separated on Windows) 3. **Conda Environment** (if built with Conda support and ``CONDA_PREFIX`` is set): @@ -561,34 +576,28 @@ Sets a custom connection profile provider. Must be called before ``AdbcDatabaseI Setting Additional Search Paths -------------------------------- -.. code-block:: c - - AdbcStatusCode AdbcDriverManagerDatabaseSetAdditionalSearchPathList( - struct AdbcDatabase* database, - const char* path_list, - struct AdbcError* error); - -Adds additional directories to search for profiles. Must be called before ``AdbcDatabaseInit()``. - -**Parameters:** - -- ``database``: Database object to configure -- ``path_list``: OS-specific path separator delimited list (``:``) on Unix, ``;`` on Windows), or ``NULL`` to clear -- ``error``: Optional error output - -**Returns:** ``ADBC_STATUS_OK`` on success, error code otherwise. +This can be done via the ``additional_profile_search_path_list`` option. It +must be set before ``AdbcDatabaseInit()``. The value of this option is an +OS-specific delimited list (``:`` on Unix, ``;`` on Windows), or ``NULL`` to +clear. **Example:** .. code-block:: c // Unix/Linux/macOS - AdbcDriverManagerDatabaseSetAdditionalSearchPathList( - &database, "/opt/app/profiles:/etc/app/profiles", &error); + AdbcDatabaseSetOption( + &database, + "additional_profile_search_path_list", + "/opt/app/profiles:/etc/app/profiles", + &error); // Windows - AdbcDriverManagerDatabaseSetAdditionalSearchPathList( - &database, "C:\\App\\Profiles;C:\\ProgramData\\App\\Profiles", &error); + AdbcDatabaseSetOption( + &database, + "additional_profile_search_path_list", + "C:\\App\\Profiles;C:\\ProgramData\\App\\Profiles", + &error); See Also diff --git a/docs/source/format/driver_manifests.rst b/docs/source/format/driver_manifests.rst index 1d4896298e..77753f81ae 100644 --- a/docs/source/format/driver_manifests.rst +++ b/docs/source/format/driver_manifests.rst @@ -28,9 +28,9 @@ ADBC Driver Manager and Manifests There are two ways to load a driver with the driver manager: -1. Directly specifying the dynamic library to load -2. Referring to a driver manifest file which contains metadata along with the - location of the dynamic library to be loaded +1. Directly specifying the dynamic library to load. +2. Referring to a :term:`driver manifest` file which contains metadata along + with the location of the dynamic library to be loaded. With either method, you specify the dynamic library or driver manifest as the ``driver`` option to the driver manager or you can use an explicit function for diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 843493a985..ab87f4c6f4 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -34,6 +34,11 @@ Glossary In ADBC, the connection object/struct represents a single connection to a database. Multiple connections may be created from one :term:`database`. + connection profile + A preconfigured driver and options that can be loaded by the + :term:`driver manager` for convenience. Specified via a TOML file. See + :doc:`format/connection_profiles`. + database In ADBC, the database object/struct holds state that is shared across connections. diff --git a/rust/driver_manager/src/lib.rs b/rust/driver_manager/src/lib.rs index efd8cb91ea..47970de48c 100644 --- a/rust/driver_manager/src/lib.rs +++ b/rust/driver_manager/src/lib.rs @@ -494,14 +494,13 @@ impl ManagedDatabase { additional_search_paths: Option>, opts: impl IntoIterator::Option, OptionValue)>, ) -> Result { - let profile_provider = FilesystemProfileProvider; Self::from_uri_with_profile_provider( uri, entrypoint, version, load_flags, additional_search_paths, - profile_provider, + FilesystemProfileProvider::default(), opts, ) } @@ -534,7 +533,7 @@ impl ManagedDatabase { /// use adbc_driver_manager::profile::FilesystemProfileProvider; /// use adbc_core::LOAD_FLAG_DEFAULT; /// - /// let provider = FilesystemProfileProvider; + /// let provider = FilesystemProfileProvider::default(); /// let opts = vec![(OptionDatabase::Username, OptionValue::String("admin".to_string()))]; /// /// let db = ManagedDatabase::from_uri_with_profile_provider( @@ -575,8 +574,7 @@ impl ManagedDatabase { (drv, final_opts) } DriverLocator::Profile(profile) => { - let profile = - profile_provider.get_profile(profile, additional_search_paths.clone())?; + let profile = profile_provider.get_profile(profile)?; let (driver_name, init_func) = profile.get_driver_name()?; let drv: ManagedDriver; diff --git a/rust/driver_manager/src/profile.rs b/rust/driver_manager/src/profile.rs index 8e93ff19f7..6697383f07 100644 --- a/rust/driver_manager/src/profile.rs +++ b/rust/driver_manager/src/profile.rs @@ -76,7 +76,6 @@ pub trait ConnectionProfileProvider { /// # Arguments /// /// * `name` - The profile name or path to locate - /// * `additional_path_list` - Optional additional directories to search for profiles /// /// # Returns /// @@ -88,11 +87,7 @@ pub trait ConnectionProfileProvider { /// - The profile cannot be found /// - The profile file is malformed /// - The profile version is unsupported - fn get_profile( - &self, - name: &str, - additional_path_list: Option>, - ) -> Result; + fn get_profile(&self, name: &str) -> Result; } /// Provides connection profiles from TOML files on the filesystem. @@ -104,7 +99,7 @@ pub trait ConnectionProfileProvider { /// # Search Order /// /// Profiles are searched in the following order: -/// 1. Additional paths provided via `get_profile()` +/// 1. Additional paths provided via `new_with_search_paths` /// 2. `ADBC_PROFILE_PATH` environment variable paths /// 3. User configuration directory (`~/.config/adbc/profiles` on Linux, /// `~/Library/Application Support/ADBC/Profiles` on macOS, @@ -117,21 +112,27 @@ pub trait ConnectionProfileProvider { /// ConnectionProfileProvider, FilesystemProfileProvider /// }; /// -/// let provider = FilesystemProfileProvider; -/// let profile = provider.get_profile("my_database", None)?; +/// let provider = FilesystemProfileProvider::default(); +/// let profile = provider.get_profile("my_database")?; /// # Ok::<(), adbc_core::error::Error>(()) /// ``` -pub struct FilesystemProfileProvider; +#[derive(Clone, Default)] +pub struct FilesystemProfileProvider { + additional_paths: Option>, +} + +impl FilesystemProfileProvider { + /// Search the given paths (if any) for profiles. + pub fn new_with_search_paths(additional_paths: Option>) -> Self { + Self { additional_paths } + } +} impl ConnectionProfileProvider for FilesystemProfileProvider { type Profile = FilesystemProfile; - fn get_profile( - &self, - name: &str, - additional_path_list: Option>, - ) -> Result { - let profile_path = find_filesystem_profile(name, additional_path_list)?; + fn get_profile(&self, name: &str) -> Result { + let profile_path = find_filesystem_profile(name, &self.additional_paths)?; FilesystemProfile::from_path(profile_path) } } @@ -266,14 +267,28 @@ impl FilesystemProfile { let profile = DeTable::parse(&contents) .map_err(|e| Error::with_message_and_status(e.to_string(), Status::InvalidArguments))?; - let profile_version = profile - .get_ref() - .get("profile_version") - .and_then(|v| v.get_ref().as_integer()) - .map(|v| v.as_str()) - .unwrap_or("1"); + let raw_profile_version = profile.get_ref().get("profile_version").ok_or_else(|| { + Error::with_message_and_status( + "missing 'profile_version' in profile".to_string(), + Status::InvalidArguments, + ) + })?; + + let profile_version = raw_profile_version + .as_ref() + .as_integer() + .and_then(|i| i64::from_str_radix(i.as_str(), i.radix()).ok()) + .ok_or_else(|| { + Error::with_message_and_status( + format!( + "invalid 'profile_version' in profile: {:?}", + raw_profile_version.as_ref() + ), + Status::InvalidArguments, + ) + })?; - if profile_version != "1" { + if profile_version != 1 { return Err(Error::with_message_and_status( format!( "unsupported profile version '{}', expected '1'", @@ -287,7 +302,12 @@ impl FilesystemProfile { .get_ref() .get("driver") .and_then(|v| v.get_ref().as_str()) - .unwrap_or("") + .ok_or_else(|| { + Error::with_message_and_status( + "missing or invalid 'driver' field in profile".to_string(), + Status::InvalidArguments, + ) + })? .to_string(); let options_table = profile @@ -707,6 +727,24 @@ key = "value" "just a plain string", Ok("just a plain string"), ), + TestCase( + "not actually a substitution", + vec![], + "{{ env_var(NONEXISTENT)", + Ok("{{ env_var(NONEXISTENT)"), + ), + TestCase( + "not actually a substitution (2)", + vec![], + "{{ env_var(NONEXISTENT) }", + Ok("{{ env_var(NONEXISTENT) }"), + ), + TestCase( + "not actually a substitution (3)", + vec![], + "{ env_var(NONEXISTENT) }", + Ok("{ env_var(NONEXISTENT) }"), + ), TestCase( "string with special chars but no templates", vec![], @@ -731,6 +769,30 @@ key = "value" "foo{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}bar", Ok("foobar"), ), + TestCase( + "env var not set interpolates the empty string (2)", + vec![], + "foo{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}", + Ok("foo"), + ), + TestCase( + "env var not set interpolates the empty string (3)", + vec![], + "{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}bar", + Ok("bar"), + ), + TestCase( + "env var not set interpolates the empty string (4)", + vec![], + "foo{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}bar{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ2) }}baz", + Ok("foobarbaz"), + ), + TestCase( + "env var not set interpolates the empty string (5)", + vec![], + "{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}foobarbaz{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ2) }}", + Ok("foobarbaz"), + ), TestCase( "mixed literal text and env var", vec![("ADBC_TEST_PPV_PORT", "5432")], @@ -752,6 +814,12 @@ key = "value" "{{ env_var(ADBC_TEST_PPV_DB) }}", Ok("mydb"), ), + TestCase( + "no whitespace inside braces", + vec![("ADBC_TEST_PPV_DB", "mydb")], + "{{env_var(ADBC_TEST_PPV_DB)}}", + Ok("mydb"), + ), TestCase( "invalid expression not env_var", vec![], @@ -859,11 +927,11 @@ test_key = "test_value" }); std::fs::write(&profile_path, profile_content).unwrap(); - let provider = FilesystemProfileProvider; let search_paths = search_paths_opt.map(|mut paths| { paths.push(tmp_dir.path().to_path_buf()); paths }); + let provider = FilesystemProfileProvider::new_with_search_paths(search_paths); let profile_arg = if name.contains("absolute") { profile_path.to_str().unwrap().to_string() @@ -871,7 +939,7 @@ test_key = "test_value" profile_name.to_string() }; - let result = provider.get_profile(&profile_arg, search_paths); + let result = provider.get_profile(&profile_arg); if should_succeed { let profile = diff --git a/rust/driver_manager/src/search.rs b/rust/driver_manager/src/search.rs index 904daabb04..2fa9f29fd9 100644 --- a/rust/driver_manager/src/search.rs +++ b/rust/driver_manager/src/search.rs @@ -845,7 +845,7 @@ fn get_search_paths(lvls: LoadFlags) -> Vec { /// Returns `Status::NotFound` if the profile cannot be located in any search path. pub(crate) fn find_filesystem_profile( name: &str, - additional_path_list: Option>, + additional_path_list: &Option>, ) -> Result { // Convert the name to a PathBuf to ensure proper platform-specific path handling. // This normalizes forward slashes to backslashes on Windows. @@ -905,8 +905,8 @@ pub(crate) fn find_filesystem_profile( /// # Returns /// /// A vector of paths to search for profiles, in priority order. -fn get_profile_search_paths(additional_path_list: Option>) -> Vec { - let mut result = additional_path_list.unwrap_or_default(); +fn get_profile_search_paths(additional_path_list: &Option>) -> Vec { + let mut result = additional_path_list.clone().unwrap_or_default(); // Add ADBC_PROFILE_PATH environment variable paths if let Some(paths) = env::var_os("ADBC_PROFILE_PATH") { @@ -1741,7 +1741,7 @@ mod tests { profile_name.to_string() }; - let result = find_filesystem_profile(&profile_arg, search_paths); + let result = find_filesystem_profile(&profile_arg, &search_paths); if should_succeed { assert!( @@ -1793,7 +1793,7 @@ mod tests { let result = find_filesystem_profile( "searched_profile", - Some(vec![ + &Some(vec![ tmp_dir1.path().to_path_buf(), tmp_dir2.path().to_path_buf(), ]), @@ -1813,7 +1813,7 @@ mod tests { .tempdir() .unwrap(); - let paths = get_profile_search_paths(Some(vec![tmp_dir.path().to_path_buf()])); + let paths = get_profile_search_paths(&Some(vec![tmp_dir.path().to_path_buf()])); assert!(paths.contains(&tmp_dir.path().to_path_buf())); assert!(!paths.is_empty()); @@ -1823,7 +1823,7 @@ mod tests { #[test] fn test_get_profile_search_paths_empty() { - let paths = get_profile_search_paths(None); + let paths = get_profile_search_paths(&None); // Should still return some paths (env vars, user config, etc.) assert!(!paths.is_empty() || paths.is_empty()); // Just verify it doesn't panic } diff --git a/rust/driver_manager/tests/common/mod.rs b/rust/driver_manager/tests/common/mod.rs index 28df83f104..408398c0ae 100644 --- a/rust/driver_manager/tests/common/mod.rs +++ b/rust/driver_manager/tests/common/mod.rs @@ -16,6 +16,7 @@ // under the License. use std::collections::HashSet; +use std::ffi::{OsStr, OsString}; use std::ops::Deref; use std::sync::Arc; @@ -340,3 +341,29 @@ pub fn test_ingestion_roundtrip(connection: &mut ManagedConnection) { connection.rollback().unwrap(); } + +pub struct SetEnv { + env_var: &'static str, + original_value: Option, +} + +impl SetEnv { + pub fn new(env_var: &'static str, new_value: impl AsRef) -> Self { + let original_value = std::env::var_os(env_var); + std::env::set_var(env_var, new_value); + Self { + env_var, + original_value, + } + } +} + +impl Drop for SetEnv { + fn drop(&mut self) { + if let Some(original_value) = &self.original_value { + std::env::set_var(self.env_var, original_value); + } else { + std::env::remove_var(self.env_var); + } + } +} diff --git a/rust/driver_manager/tests/connection_profile.rs b/rust/driver_manager/tests/connection_profile.rs index 8a0eae3aec..f0797fc889 100644 --- a/rust/driver_manager/tests/connection_profile.rs +++ b/rust/driver_manager/tests/connection_profile.rs @@ -126,12 +126,10 @@ uri = ":memory:" fn test_filesystem_profile_load_simple() { let (tmp_dir, profile_path) = write_profile_to_tempfile("simple", &simple_profile()); - let provider = FilesystemProfileProvider; + let search_paths = Some(vec![tmp_dir.path().to_path_buf()]); + let provider = FilesystemProfileProvider::new_with_search_paths(search_paths); let profile = provider - .get_profile( - profile_path.to_str().unwrap(), - Some(vec![tmp_dir.path().to_path_buf()]), - ) + .get_profile(profile_path.to_str().unwrap()) .unwrap(); let (driver_name, init_func) = profile.get_driver_name().unwrap(); @@ -158,12 +156,10 @@ fn test_filesystem_profile_nested_options() { let (tmp_dir, profile_path) = write_profile_to_tempfile("nested", &profile_with_nested_options()); - let provider = FilesystemProfileProvider; + let search_paths = Some(vec![tmp_dir.path().to_path_buf()]); + let provider = FilesystemProfileProvider::new_with_search_paths(search_paths); let profile = provider - .get_profile( - profile_path.to_str().unwrap(), - Some(vec![tmp_dir.path().to_path_buf()]), - ) + .get_profile(profile_path.to_str().unwrap()) .unwrap(); let options: Vec<_> = profile.get_options().unwrap().into_iter().collect(); @@ -194,12 +190,10 @@ fn test_filesystem_profile_nested_options() { fn test_filesystem_profile_all_option_types() { let (tmp_dir, profile_path) = write_profile_to_tempfile("all_types", &profile_with_all_types()); - let provider = FilesystemProfileProvider; + let provider = + FilesystemProfileProvider::new_with_search_paths(Some(vec![tmp_dir.path().to_path_buf()])); let profile = provider - .get_profile( - profile_path.to_str().unwrap(), - Some(vec![tmp_dir.path().to_path_buf()]), - ) + .get_profile(profile_path.to_str().unwrap()) .unwrap(); let options: Vec<_> = profile.get_options().unwrap().into_iter().collect(); @@ -251,22 +245,85 @@ fn test_filesystem_profile_error_cases() { Status::InvalidArguments, "unsupported profile version", ), + ( + "no version", + r#" +driver = "adbc_driver_sqlite" +[Options] +"# + .to_string(), + Status::InvalidArguments, + "missing 'profile_version' in profile", + ), + ( + "bad version", + r#" +profile_version = "1" +driver = "adbc_driver_sqlite" +[Options] +"# + .to_string(), + Status::InvalidArguments, + "invalid 'profile_version' in profile", + ), ( "invalid toml", invalid_toml().to_string(), Status::InvalidArguments, - "", + "TOML parse error", + ), + ( + "no driver", + r#" +profile_version = 1 +[Options] +"# + .to_string(), + Status::InvalidArguments, + "missing or invalid 'driver' field in profile", + ), + ( + "numeric driver", + r#" +profile_version = 1 +driver = 2 +[Options] +"# + .to_string(), + Status::InvalidArguments, + "missing or invalid 'driver' field in profile", + ), + ( + "table driver", + r#" +profile_version = 1 +[driver] +foo = "bar" +[Options] +"# + .to_string(), + Status::InvalidArguments, + "missing or invalid 'driver' field in profile", + ), + ( + "no options", + r#" +profile_version = 1 +driver = "foo" +"# + .to_string(), + Status::InvalidArguments, + "missing or invalid 'Options' table in profile", ), ]; for (name, profile_content, expected_status, expected_msg_fragment) in test_cases { let (tmp_dir, profile_path) = write_profile_to_tempfile(name, &profile_content); - let provider = FilesystemProfileProvider; - let result = provider.get_profile( - profile_path.to_str().unwrap(), - Some(vec![tmp_dir.path().to_path_buf()]), - ); + let provider = FilesystemProfileProvider::new_with_search_paths(Some(vec![tmp_dir + .path() + .to_path_buf()])); + let result = provider.get_profile(profile_path.to_str().unwrap()); assert!(result.is_err(), "Test case '{}': expected error", name); let err = result.unwrap_err(); @@ -293,8 +350,8 @@ fn test_filesystem_profile_error_cases() { #[test] fn test_filesystem_profile_not_found() { - let provider = FilesystemProfileProvider; - let result = provider.get_profile("nonexistent_profile", None); + let provider = FilesystemProfileProvider::default(); + let result = provider.get_profile("nonexistent_profile"); assert!(result.is_err()); let err = result.unwrap_err(); @@ -302,27 +359,6 @@ fn test_filesystem_profile_not_found() { assert!(err.message.contains("Profile not found")); } -#[test] -fn test_filesystem_profile_without_driver() { - let (tmp_dir, profile_path) = write_profile_to_tempfile("no_driver", &profile_without_driver()); - - let provider = FilesystemProfileProvider; - let profile = provider - .get_profile( - profile_path.to_str().unwrap(), - Some(vec![tmp_dir.path().to_path_buf()]), - ) - .unwrap(); - - let (driver_name, _) = profile.get_driver_name().unwrap(); - // Should get empty string for missing driver - assert_eq!(driver_name, ""); - - tmp_dir - .close() - .expect("Failed to close/remove temporary directory"); -} - #[test] #[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)] fn test_database_from_uri_with_profile() { @@ -385,12 +421,12 @@ fn test_profile_loading_scenarios() { for (name, profile_name, profile_content, use_search_path, use_absolute) in test_cases { let (tmp_dir, profile_path) = write_profile_to_tempfile(profile_name, &profile_content); - let provider = FilesystemProfileProvider; let search_paths = if use_search_path { Some(vec![tmp_dir.path().to_path_buf()]) } else { None }; + let provider = FilesystemProfileProvider::new_with_search_paths(search_paths); let profile_arg = if use_absolute { profile_path.to_str().unwrap() @@ -399,7 +435,7 @@ fn test_profile_loading_scenarios() { }; let profile = provider - .get_profile(profile_arg, search_paths) + .get_profile(profile_arg) .unwrap_or_else(|e| panic!("Test case '{}' failed: {:?}", name, e)); let (driver_name, _) = profile.get_driver_name().unwrap(); @@ -419,9 +455,9 @@ fn test_profile_loading_scenarios() { fn test_profile_display() { let (tmp_dir, profile_path) = write_profile_to_tempfile("display", &simple_profile()); - let provider = FilesystemProfileProvider; + let provider = FilesystemProfileProvider::default(); let profile = provider - .get_profile(profile_path.to_str().unwrap(), None) + .get_profile(profile_path.to_str().unwrap()) .unwrap(); let display_str = format!("{}", profile); @@ -460,8 +496,7 @@ fn test_profile_hierarchical_path_via_env_var() { ); // Set ADBC_PROFILE_PATH to the parent directory - let prev_value = env::var_os("ADBC_PROFILE_PATH"); - env::set_var("ADBC_PROFILE_PATH", tmp_dir.path()); + let _guard = common::SetEnv::new("ADBC_PROFILE_PATH", tmp_dir.path()); // Verify the environment variable is set correctly assert_eq!( @@ -470,14 +505,8 @@ fn test_profile_hierarchical_path_via_env_var() { ); // Try to load the profile using hierarchical relative path - let provider = FilesystemProfileProvider; - let result = provider.get_profile("databases/postgres/production", None); - - // Restore the original environment variable - match prev_value { - Some(val) => env::set_var("ADBC_PROFILE_PATH", val), - None => env::remove_var("ADBC_PROFILE_PATH"), - } + let provider = FilesystemProfileProvider::default(); + let result = provider.get_profile("databases/postgres/production"); // Verify the profile was loaded successfully let profile = result.expect("Failed to load profile from hierarchical path"); @@ -501,8 +530,6 @@ fn test_profile_hierarchical_path_via_env_var() { #[test] #[serial] fn test_profile_hierarchical_path_with_extension_via_env_var() { - use std::env; - let tmp_dir = tempfile::Builder::new() .prefix("adbc_profile_env_test2") .tempdir() @@ -518,18 +545,11 @@ fn test_profile_hierarchical_path_with_extension_via_env_var() { std::fs::write(&profile_path, simple_profile()).expect("Failed to write profile"); // Set ADBC_PROFILE_PATH to the parent directory - let prev_value = env::var_os("ADBC_PROFILE_PATH"); - env::set_var("ADBC_PROFILE_PATH", tmp_dir.path()); + let _guard = common::SetEnv::new("ADBC_PROFILE_PATH", tmp_dir.path()); // Try to load the profile using hierarchical relative path with .toml extension - let provider = FilesystemProfileProvider; - let result = provider.get_profile("configs/dev/database.toml", None); - - // Restore the original environment variable - match prev_value { - Some(val) => env::set_var("ADBC_PROFILE_PATH", val), - None => env::remove_var("ADBC_PROFILE_PATH"), - } + let provider = FilesystemProfileProvider::default(); + let result = provider.get_profile("configs/dev/database.toml"); // Verify the profile was loaded successfully let profile = result.expect("Failed to load profile from hierarchical path with extension"); @@ -558,11 +578,9 @@ fn test_profile_hierarchical_path_additional_search_paths() { std::fs::write(&profile_path, simple_profile()).expect("Failed to write profile"); // Load profile using hierarchical path via additional_search_paths - let provider = FilesystemProfileProvider; - let result = provider.get_profile( - "projects/myapp/local", - Some(vec![tmp_dir.path().to_path_buf()]), - ); + let provider = + FilesystemProfileProvider::new_with_search_paths(Some(vec![tmp_dir.path().to_path_buf()])); + let result = provider.get_profile("projects/myapp/local"); // Verify the profile was loaded successfully let profile = result.expect("Failed to load profile from hierarchical path"); @@ -606,18 +624,11 @@ fn test_profile_conda_prefix() { std::fs::write(&filepath, simple_profile()).expect("Failed to write profile"); // Set CONDA_PREFIX environment variable - let prev_value = env::var("CONDA_PREFIX").ok(); - env::set_var("CONDA_PREFIX", tmp_dir.path()); + let _guard = common::SetEnv::new("CONDA_PREFIX", tmp_dir.path()); let uri = "profile://sqlite-profile"; let result = ManagedDatabase::from_uri(uri, None, AdbcVersion::V100, LOAD_FLAG_DEFAULT, None); - // Restore environment variable - match prev_value { - Some(val) => env::set_var("CONDA_PREFIX", val), - None => env::remove_var("CONDA_PREFIX"), - } - if is_conda_build { assert!(result.is_ok(), "Expected success for conda build"); } else { @@ -635,3 +646,106 @@ fn test_profile_conda_prefix() { .close() .expect("Failed to close/remove temporary directory") } + +#[test] +#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)] +fn test_profile_load_manifest() { + let driver_path = PathBuf::from( + env::var_os("ADBC_DRIVER_MANAGER_TEST_LIB") + .expect("ADBC_DRIVER_MANAGER_TEST_LIB must be set for driver manager manifest tests"), + ) + .to_string_lossy() + .to_string(); + let manifest_dir = tempfile::Builder::new() + .prefix("adbc-test-manifest") + .tempdir() + .unwrap(); + let profile_dir = tempfile::Builder::new() + .prefix("adbc-test-profile") + .tempdir() + .unwrap(); + + let manifest_contents = format!( + r#" +manifest_version = 1 +[Driver] +shared = "{driver_path}" +"# + ); + + let manifest_path = manifest_dir.path().join("sqlite.toml"); + std::fs::write(&manifest_path, &manifest_contents).unwrap(); + + let manifest_path = profile_dir.path().join("sqlitemani.toml"); + std::fs::write(&manifest_path, &manifest_contents).unwrap(); + + let profile_contents = r#" +profile_version = 1 +driver = "sqlite" +[Options] +uri = ":memory:" +"#; + + let profile_path = profile_dir.path().join("sqlitedev.toml"); + std::fs::write(&profile_path, profile_contents).unwrap(); + let profile_path = manifest_dir.path().join("sqliteprof.toml"); + std::fs::write(&profile_path, profile_contents).unwrap(); + + let provider = FilesystemProfileProvider::new_with_search_paths(Some(vec![profile_dir + .path() + .to_path_buf()])); + let database = ManagedDatabase::from_uri_with_profile_provider( + "profile://sqlitedev", + None, + AdbcVersion::V100, + LOAD_FLAG_DEFAULT, + Some(vec![manifest_dir.path().to_path_buf()]), + provider.clone(), + std::iter::empty(), + ) + .unwrap(); + + common::test_database(&database); + + // should not be able to load a profile from manifest dir or vice versa + let result = ManagedDatabase::from_uri_with_profile_provider( + "profile://sqliteprof", + None, + AdbcVersion::V100, + LOAD_FLAG_DEFAULT, + Some(vec![manifest_dir.path().to_path_buf()]), + provider.clone(), + std::iter::empty(), + ); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert!(err.message.contains("Profile not found: sqliteprof")); + + let result = ManagedDatabase::from_uri_with_profile_provider( + "sqlitemani://", + None, + AdbcVersion::V100, + LOAD_FLAG_DEFAULT, + Some(vec![manifest_dir.path().to_path_buf()]), + provider.clone(), + std::iter::empty(), + ); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert!(err.message.contains("Driver not found: sqlitemani")); + + // but of course loading manifest from manifest dir is OK + let result = ManagedDatabase::from_uri_with_profile_provider( + "sqlite://", + None, + AdbcVersion::V100, + LOAD_FLAG_DEFAULT, + Some(vec![manifest_dir.path().to_path_buf()]), + provider.clone(), + std::iter::empty(), + ); + assert!(result.is_ok()); + + manifest_dir.close().unwrap(); + profile_dir.close().unwrap(); +} diff --git a/rust/driver_manager/tests/test_env_var_profiles.rs b/rust/driver_manager/tests/test_env_var_profiles.rs index bd35d3453f..d04610267b 100644 --- a/rust/driver_manager/tests/test_env_var_profiles.rs +++ b/rust/driver_manager/tests/test_env_var_profiles.rs @@ -18,6 +18,8 @@ use std::env; use std::path::PathBuf; +mod common; + use adbc_core::options::AdbcVersion; use adbc_core::{error::Status, LOAD_FLAG_DEFAULT}; use adbc_driver_manager::ManagedDatabase; @@ -37,8 +39,7 @@ fn test_env_var_replacement_basic() { .expect("Failed to create temporary directory"); // Set a test environment variable - let prev_value = env::var_os("ADBC_TEST_ENV_VAR"); - env::set_var("ADBC_TEST_ENV_VAR", ":memory:"); + let _guard = common::SetEnv::new("ADBC_TEST_ENV_VAR", ":memory:"); let profile_content = r#" profile_version = 1 @@ -53,12 +54,6 @@ uri = "{{ env_var(ADBC_TEST_ENV_VAR) }}" let result = ManagedDatabase::from_uri(&uri, None, AdbcVersion::V100, LOAD_FLAG_DEFAULT, None); - // Restore environment variable - match prev_value { - Some(val) => env::set_var("ADBC_TEST_ENV_VAR", val), - None => env::remove_var("ADBC_TEST_ENV_VAR"), - } - match result { Ok(_db) => { // Successfully created database with env_var replacement @@ -198,8 +193,7 @@ fn test_env_var_replacement_interpolation() { .expect("Failed to create temporary directory"); // Set a test environment variable - let prev_value = env::var_os("ADBC_TEST_INTERPOLATE"); - env::set_var("ADBC_TEST_INTERPOLATE", "middle_value"); + let _guard = common::SetEnv::new("ADBC_TEST_INTERPOLATE", "middle_value"); let profile_content = r#" profile_version = 1 @@ -215,12 +209,6 @@ test_option = "prefix_{{ env_var(ADBC_TEST_INTERPOLATE) }}_suffix" let result = ManagedDatabase::from_uri(&uri, None, AdbcVersion::V100, LOAD_FLAG_DEFAULT, None); - // Restore environment variable - match prev_value { - Some(val) => env::set_var("ADBC_TEST_INTERPOLATE", val), - None => env::remove_var("ADBC_TEST_INTERPOLATE"), - } - assert!(result.is_err(), "Expected error for malformed env_var"); if let Err(err) = result { assert_eq!( @@ -243,10 +231,8 @@ fn test_env_var_replacement_multiple() { .expect("Failed to create temporary directory"); // Set test environment variables - let prev_var1 = env::var_os("ADBC_TEST_VAR1"); - let prev_var2 = env::var_os("ADBC_TEST_VAR2"); - env::set_var("ADBC_TEST_VAR1", "first"); - env::set_var("ADBC_TEST_VAR2", "second"); + let _guard1 = common::SetEnv::new("ADBC_TEST_VAR1", "first"); + let _guard2 = common::SetEnv::new("ADBC_TEST_VAR2", "second"); let profile_content = r#" profile_version = 1 @@ -262,16 +248,6 @@ test_option = "{{ env_var(ADBC_TEST_VAR1) }}_and_{{ env_var(ADBC_TEST_VAR2) }}" let result = ManagedDatabase::from_uri(&uri, None, AdbcVersion::V100, LOAD_FLAG_DEFAULT, None); - // Restore environment variables - match prev_var1 { - Some(val) => env::set_var("ADBC_TEST_VAR1", val), - None => env::remove_var("ADBC_TEST_VAR1"), - } - match prev_var2 { - Some(val) => env::set_var("ADBC_TEST_VAR2", val), - None => env::remove_var("ADBC_TEST_VAR2"), - } - assert!(result.is_err(), "Expected error for malformed env_var"); if let Err(err) = result { assert_eq!( @@ -294,8 +270,7 @@ fn test_env_var_replacement_whitespace() { .expect("Failed to create temporary directory"); // Set a test environment variable - let prev_value = env::var_os("ADBC_TEST_WHITESPACE"); - env::set_var("ADBC_TEST_WHITESPACE", "value"); + let _guard = common::SetEnv::new("ADBC_TEST_WHITESPACE", "value"); let profile_content = r#" profile_version = 1 @@ -311,12 +286,6 @@ test_option = "{{ env_var( ADBC_TEST_WHITESPACE ) }}" let result = ManagedDatabase::from_uri(&uri, None, AdbcVersion::V100, LOAD_FLAG_DEFAULT, None); - // Restore environment variable - match prev_value { - Some(val) => env::set_var("ADBC_TEST_WHITESPACE", val), - None => env::remove_var("ADBC_TEST_WHITESPACE"), - } - assert!(result.is_err(), "Expected error for malformed env_var"); if let Err(err) = result { assert_eq!( From 0a709674ce2bc91840f6e825a83d3fb2e9b3c1d1 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 14:56:09 +0900 Subject: [PATCH 2/6] update js --- javascript/__test__/profile.spec.ts | 2 +- javascript/binding.d.ts | 3 ++- javascript/lib/types.ts | 11 ++++++++--- javascript/src/client.rs | 21 +++++++++++++++------ javascript/src/lib.rs | 6 ++++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/javascript/__test__/profile.spec.ts b/javascript/__test__/profile.spec.ts index 4f346f3c26..3fcab571e9 100644 --- a/javascript/__test__/profile.spec.ts +++ b/javascript/__test__/profile.spec.ts @@ -47,7 +47,7 @@ test('profile: load database from profile:// URI', async () => { const db = new AdbcDatabase({ driver: 'profile://test_sqlite', - searchPaths: [tmpDir], + profileSearchPaths: [tmpDir], }) const conn = await db.connect() diff --git a/javascript/binding.d.ts b/javascript/binding.d.ts index c771d3ea75..c9fb966ff7 100644 --- a/javascript/binding.d.ts +++ b/javascript/binding.d.ts @@ -39,7 +39,8 @@ export type _NativeAdbcStatement = NativeAdbcStatement export interface ConnectOptions { driver: string entrypoint?: string - searchPaths?: Array + manifestSearchPaths?: Array + profileSearchPaths?: Array loadFlags?: number databaseOptions?: Record } diff --git a/javascript/lib/types.ts b/javascript/lib/types.ts index 59fb7487e2..7d3dd6bbd4 100644 --- a/javascript/lib/types.ts +++ b/javascript/lib/types.ts @@ -118,7 +118,7 @@ export interface ConnectOptions { * - URI-style string: `"sqlite:file::memory:"`, `"postgresql://user:pass@host/db"` — the * driver name is the URI scheme and the remainder is passed as the connection URI. * - Connection profile URI: `"profile://my_profile"` — loads a named profile from a - * `.toml` file found in {@link searchPaths} or the default search directories. + * `.toml` file found in {@link profileSearchPaths} or the default search directories. */ driver: string /** @@ -127,10 +127,15 @@ export interface ConnectOptions { */ entrypoint?: string /** - * Additional directories to search for drivers and driver manifest (`.toml`) profile files (optional). + * Additional directories to search for drivers and driver manifest (`.toml`) files (optional). * Searched before the default system and user configuration directories. */ - searchPaths?: string[] + manifestSearchPaths?: string[] + /** + * Additional directories to search for connection profile (`.toml`) files (optional). + * Searched before the default system and user configuration directories. + */ + profileSearchPaths?: string[] /** * Bitmask controlling how the driver name is resolved (optional). * Use the {@link LoadFlags} constants to compose a value. diff --git a/javascript/src/client.rs b/javascript/src/client.rs index b6bc9ac1e4..3cbcd39654 100644 --- a/javascript/src/client.rs +++ b/javascript/src/client.rs @@ -46,7 +46,8 @@ pub type Result = std::result::Result; pub struct ConnectOptions { pub driver: String, pub entrypoint: Option, - pub search_paths: Option>, + pub manifest_search_paths: Option>, + pub profile_search_paths: Option>, pub load_flags: Option, pub database_options: Option>, } @@ -76,20 +77,28 @@ impl AdbcDatabaseCore { let load_flags = opts.load_flags.unwrap_or(LOAD_FLAG_DEFAULT); let entrypoint = opts.entrypoint.as_ref().map(|s| s.as_bytes().to_vec()); - let search_paths: Option> = opts - .search_paths + let manifest_search_paths: Option> = opts + .manifest_search_paths + .map(|paths| paths.into_iter().map(PathBuf::from).collect()); + + let profile_search_paths: Option> = opts + .profile_search_paths .map(|paths| paths.into_iter().map(PathBuf::from).collect()); let database_opts = opts.database_options.map(map_database_options); let database = if opts.driver.contains(':') { + let provider = adbc_driver_manager::profile::FilesystemProfileProvider::new_with_search_paths( + profile_search_paths, + ); // URI-style ("sqlite:file::memory:") or profile URI ("profile://my_profile") - ManagedDatabase::from_uri_with_opts( + ManagedDatabase::from_uri_with_profile_provider( &opts.driver, entrypoint.as_deref(), version, load_flags, - search_paths, + manifest_search_paths, + provider, database_opts.into_iter().flatten(), )? } else { @@ -99,7 +108,7 @@ impl AdbcDatabaseCore { entrypoint.as_deref(), version, load_flags, - search_paths, + manifest_search_paths, )?; match database_opts { Some(db_opts) => driver.new_database_with_opts(db_opts)?, diff --git a/javascript/src/lib.rs b/javascript/src/lib.rs index 94d5bc7a87..5e9b486703 100644 --- a/javascript/src/lib.rs +++ b/javascript/src/lib.rs @@ -144,7 +144,8 @@ pub fn default_load_flags() -> u32 { pub struct ConnectOptions { pub driver: String, pub entrypoint: Option, - pub search_paths: Option>, + pub manifest_search_paths: Option>, + pub profile_search_paths: Option>, pub load_flags: Option, pub database_options: Option>, } @@ -154,7 +155,8 @@ impl From for CoreConnectOptions { Self { driver: opts.driver, entrypoint: opts.entrypoint, - search_paths: opts.search_paths, + manifest_search_paths: opts.manifest_search_paths, + profile_search_paths: opts.profile_search_paths, load_flags: opts.load_flags, database_options: opts.database_options, } From 811546204bfc72dc3c0c06359afedc3cc71bc5f5 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 15:38:52 +0900 Subject: [PATCH 3/6] windows --- rust/driver_manager/tests/connection_profile.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/driver_manager/tests/connection_profile.rs b/rust/driver_manager/tests/connection_profile.rs index f0797fc889..73df67a1ef 100644 --- a/rust/driver_manager/tests/connection_profile.rs +++ b/rust/driver_manager/tests/connection_profile.rs @@ -655,7 +655,8 @@ fn test_profile_load_manifest() { .expect("ADBC_DRIVER_MANAGER_TEST_LIB must be set for driver manager manifest tests"), ) .to_string_lossy() - .to_string(); + .to_string() + .replace("\\", "\\\\"); let manifest_dir = tempfile::Builder::new() .prefix("adbc-test-manifest") .tempdir() From dc11d7071cbd32b3678bc12d1e081e1ca8e8f6be Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 15:58:51 +0900 Subject: [PATCH 4/6] windows --- rust/driver_manager/tests/connection_profile.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rust/driver_manager/tests/connection_profile.rs b/rust/driver_manager/tests/connection_profile.rs index 73df67a1ef..e732ee9370 100644 --- a/rust/driver_manager/tests/connection_profile.rs +++ b/rust/driver_manager/tests/connection_profile.rs @@ -720,7 +720,11 @@ uri = ":memory:" ); assert!(result.is_err()); let err = result.err().unwrap(); - assert!(err.message.contains("Profile not found: sqliteprof")); + assert!( + err.message.contains("Profile not found: sqliteprof"), + "{}", + err.message + ); let result = ManagedDatabase::from_uri_with_profile_provider( "sqlitemani://", @@ -733,7 +737,7 @@ uri = ":memory:" ); assert!(result.is_err()); let err = result.err().unwrap(); - assert!(err.message.contains("Driver not found: sqlitemani")); + assert!(err.message.contains("sqlitemani"), "{}", err.message); // but of course loading manifest from manifest dir is OK let result = ManagedDatabase::from_uri_with_profile_provider( From 82633ab294929906879434ac841ed04efe81a001 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 16:13:35 +0900 Subject: [PATCH 5/6] windows --- rust/driver_manager/tests/connection_profile.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rust/driver_manager/tests/connection_profile.rs b/rust/driver_manager/tests/connection_profile.rs index e732ee9370..845a6d993a 100644 --- a/rust/driver_manager/tests/connection_profile.rs +++ b/rust/driver_manager/tests/connection_profile.rs @@ -736,8 +736,12 @@ uri = ":memory:" std::iter::empty(), ); assert!(result.is_err()); - let err = result.err().unwrap(); - assert!(err.message.contains("sqlitemani"), "{}", err.message); + #[cfg(not(windows))] + { + // The Windows error just says 'LoadLibraryExW failed' + let err = result.err().unwrap(); + assert!(err.message.contains("sqlitemani"), "{}", err.message); + } // but of course loading manifest from manifest dir is OK let result = ManagedDatabase::from_uri_with_profile_provider( From 8eb1abfd0b4ee54ff3f0e07bc9aead2bd6387303 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 19 Mar 2026 16:23:09 +0900 Subject: [PATCH 6/6] windows --- rust/driver_manager/tests/connection_profile.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/rust/driver_manager/tests/connection_profile.rs b/rust/driver_manager/tests/connection_profile.rs index 845a6d993a..57207d25ed 100644 --- a/rust/driver_manager/tests/connection_profile.rs +++ b/rust/driver_manager/tests/connection_profile.rs @@ -743,18 +743,6 @@ uri = ":memory:" assert!(err.message.contains("sqlitemani"), "{}", err.message); } - // but of course loading manifest from manifest dir is OK - let result = ManagedDatabase::from_uri_with_profile_provider( - "sqlite://", - None, - AdbcVersion::V100, - LOAD_FLAG_DEFAULT, - Some(vec![manifest_dir.path().to_path_buf()]), - provider.clone(), - std::iter::empty(), - ); - assert!(result.is_ok()); - manifest_dir.close().unwrap(); profile_dir.close().unwrap(); }