This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new 056dc6871 feat(rust/driver_manager): reconcile with C++ driver manager
(#4121)
056dc6871 is described below
commit 056dc6871fed522c22f1f77bd54ca989b27c319e
Author: David Li <[email protected]>
AuthorDate: Mon Mar 23 09:18:16 2026 +0900
feat(rust/driver_manager): reconcile with C++ driver manager (#4121)
Closes #4089.
---
docs/source/format/connection_profiles.rst | 83 +++---
docs/source/format/driver_manifests.rst | 6 +-
docs/source/glossary.rst | 5 +
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 +-
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 ++
rust/driver_manager/tests/connection_profile.rs | 277 +++++++++++++++------
rust/driver_manager/tests/test_env_var_profiles.rs | 45 +---
14 files changed, 416 insertions(+), 212 deletions(-)
diff --git a/docs/source/format/connection_profiles.rst
b/docs/source/format/connection_profiles.rst
index 87d8ac142..aaabc49e9 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 database 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 database 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
+ # The driver is optional, but if not provided it must be set by the
application.
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", "<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 to
apply to the ``AdbcDatabase`` upon creation. 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 ``<profile_name>.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 1d4896298..77753f81a 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 843493a98..ab87f4c6f 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/javascript/__test__/profile.spec.ts
b/javascript/__test__/profile.spec.ts
index 4f346f3c2..3fcab571e 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 b6891e90a..2740d1655 100644
--- a/javascript/binding.d.ts
+++ b/javascript/binding.d.ts
@@ -43,7 +43,8 @@ export type _NativeAdbcStatement = NativeAdbcStatement
export interface ConnectOptions {
driver: string
entrypoint?: string
- searchPaths?: Array<string>
+ manifestSearchPaths?: Array<string>
+ profileSearchPaths?: Array<string>
loadFlags?: number
databaseOptions?: Record<string, string>
}
diff --git a/javascript/lib/types.ts b/javascript/lib/types.ts
index 43b64856a..6a6b3ba12 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 be3e627e4..66c695568 100644
--- a/javascript/src/client.rs
+++ b/javascript/src/client.rs
@@ -48,7 +48,8 @@ pub type Result<T> = std::result::Result<T, ClientError>;
pub struct ConnectOptions {
pub driver: String,
pub entrypoint: Option<String>,
- pub search_paths: Option<Vec<String>>,
+ pub manifest_search_paths: Option<Vec<String>>,
+ pub profile_search_paths: Option<Vec<String>>,
pub load_flags: Option<u32>,
pub database_options: Option<HashMap<String, String>>,
}
@@ -78,20 +79,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<Vec<PathBuf>> = opts
- .search_paths
+ let manifest_search_paths: Option<Vec<PathBuf>> = opts
+ .manifest_search_paths
+ .map(|paths| paths.into_iter().map(PathBuf::from).collect());
+
+ let profile_search_paths: Option<Vec<PathBuf>> = 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 {
@@ -101,7 +110,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 136e9292a..92bee8663 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<String>,
- pub search_paths: Option<Vec<String>>,
+ pub manifest_search_paths: Option<Vec<String>>,
+ pub profile_search_paths: Option<Vec<String>>,
pub load_flags: Option<u32>,
pub database_options: Option<HashMap<String, String>>,
}
@@ -154,7 +155,8 @@ impl From<ConnectOptions> 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,
}
diff --git a/rust/driver_manager/src/lib.rs b/rust/driver_manager/src/lib.rs
index efd8cb91e..47970de48 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<Vec<PathBuf>>,
opts: impl IntoIterator<Item = (<Self as Optionable>::Option,
OptionValue)>,
) -> Result<Self> {
- 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 8e93ff19f..6697383f0 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<Vec<PathBuf>>,
- ) -> Result<Self::Profile>;
+ fn get_profile(&self, name: &str) -> Result<Self::Profile>;
}
/// 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<Vec<PathBuf>>,
+}
+
+impl FilesystemProfileProvider {
+ /// Search the given paths (if any) for profiles.
+ pub fn new_with_search_paths(additional_paths: Option<Vec<PathBuf>>) ->
Self {
+ Self { additional_paths }
+ }
+}
impl ConnectionProfileProvider for FilesystemProfileProvider {
type Profile = FilesystemProfile;
- fn get_profile(
- &self,
- name: &str,
- additional_path_list: Option<Vec<PathBuf>>,
- ) -> Result<Self::Profile> {
- let profile_path = find_filesystem_profile(name,
additional_path_list)?;
+ fn get_profile(&self, name: &str) -> Result<Self::Profile> {
+ 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 904daabb0..2fa9f29fd 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<PathBuf> {
/// 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<Vec<PathBuf>>,
+ additional_path_list: &Option<Vec<PathBuf>>,
) -> Result<PathBuf> {
// 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<PathBuf>>) ->
Vec<PathBuf> {
- let mut result = additional_path_list.unwrap_or_default();
+fn get_profile_search_paths(additional_path_list: &Option<Vec<PathBuf>>) ->
Vec<PathBuf> {
+ 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 28df83f10..408398c0a 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<OsString>,
+}
+
+impl SetEnv {
+ pub fn new(env_var: &'static str, new_value: impl AsRef<OsStr>) -> 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 8a0eae3ae..57207d25e 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,103 @@ 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()
+ .replace("\\", "\\\\");
+ 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"),
+ "{}",
+ err.message
+ );
+
+ 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());
+ #[cfg(not(windows))]
+ {
+ // The Windows error just says 'LoadLibraryExW failed'
+ let err = result.err().unwrap();
+ assert!(err.message.contains("sqlitemani"), "{}", err.message);
+ }
+
+ 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 bd35d3453..d04610267 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!(