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 da58c591e feat(c/driver_manager,rust/driver_manager): improve 
profile/manifest consistency (#4083)
da58c591e is described below

commit da58c591ed89b29c9096e4ebc0fe99d369e2bc88
Author: Matt Topol <[email protected]>
AuthorDate: Sat Mar 14 09:21:05 2026 -0400

    feat(c/driver_manager,rust/driver_manager): improve profile/manifest 
consistency (#4083)
    
    fixes #4082
    
    For connection profiles: switch `version` to `profile_version` for
    consistency with manifests using `manifest_version`. Also update
    `[options]` to `[Options]`.
    
    Also fixes the handling of env var replacement in profiles not to bail
    when the env_var isn't set or is an empty string.
    
    ---------
    
    Co-authored-by: David Li <[email protected]>
---
 c/driver_manager/adbc_driver_manager_profiles.cc   |   6 +-
 c/driver_manager/adbc_driver_manager_test.cc       | 106 +++++++++++---
 c/include/arrow-adbc/adbc_driver_manager.h         |   4 +-
 ci/scripts/python_venv_test.sh                     |   4 +-
 docs/source/format/connection_profiles.rst         |  34 ++---
 go/adbc/drivermgr/adbc_driver_manager_profiles.cc  |   6 +-
 go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h |   4 +-
 javascript/__test__/profile.spec.ts                |   2 +-
 python/adbc_driver_manager/tests/test_profile.py   |  84 +++++------
 rust/driver_manager/src/profile.rs                 | 159 +++++++++++++++++++--
 rust/driver_manager/tests/connection_profile.rs    |  95 +++++++++---
 rust/driver_manager/tests/test_env_var_profiles.rs |  28 ++--
 12 files changed, 397 insertions(+), 135 deletions(-)

diff --git a/c/driver_manager/adbc_driver_manager_profiles.cc 
b/c/driver_manager/adbc_driver_manager_profiles.cc
index aebf7408f..703eddaf9 100644
--- a/c/driver_manager/adbc_driver_manager_profiles.cc
+++ b/c/driver_manager/adbc_driver_manager_profiles.cc
@@ -295,14 +295,14 @@ AdbcStatusCode LoadProfileFile(const 
std::filesystem::path& profile_path,
   }
 
   profile.path = profile_path;
-  if (!config["version"].is_integer()) {
+  if (!config["profile_version"].is_integer()) {
     std::string message =
         "Profile version is not an integer in profile '" + 
profile_path.string() + "'";
     SetError(error, std::move(message));
     return ADBC_STATUS_INVALID_ARGUMENT;
   }
 
-  const auto version = config["version"].value_or(int64_t(1));
+  const auto version = config["profile_version"].value_or(int64_t(1));
   switch (version) {
     case 1:
       break;
@@ -317,7 +317,7 @@ AdbcStatusCode LoadProfileFile(const std::filesystem::path& 
profile_path,
 
   profile.driver = config["driver"].value_or(""s);
 
-  auto options = config.at_path("options");
+  auto options = config.at_path("Options");
   if (!options.is_table()) {
     std::string message =
         "Profile options is not a table in profile '" + profile_path.string() 
+ "'";
diff --git a/c/driver_manager/adbc_driver_manager_test.cc 
b/c/driver_manager/adbc_driver_manager_test.cc
index 378065dd2..3265429e7 100644
--- a/c/driver_manager/adbc_driver_manager_test.cc
+++ b/c/driver_manager/adbc_driver_manager_test.cc
@@ -1480,9 +1480,9 @@ class ConnectionProfiles : public ::testing::Test {
     std::filesystem::create_directories(temp_dir);
 
     simple_profile = toml::table{
-        {"version", 1},
+        {"profile_version", 1},
         {"driver", "adbc_driver_sqlite"},
-        {"options",
+        {"Options",
          toml::table{
              {"uri", "file::memory:"},
          }},
@@ -1646,7 +1646,7 @@ TEST_F(ConnectionProfiles, DriverProfileOption) {
 TEST_F(ConnectionProfiles, ExtraStringOption) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = simple_profile;
-  profile["options"].as_table()->insert("foo", "bar");
+  profile["Options"].as_table()->insert("foo", "bar");
   std::ofstream test_manifest_file(filepath);
   ASSERT_TRUE(test_manifest_file.is_open());
   test_manifest_file << profile;
@@ -1668,7 +1668,7 @@ TEST_F(ConnectionProfiles, ExtraStringOption) {
 TEST_F(ConnectionProfiles, ExtraIntOption) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = simple_profile;
-  profile["options"].as_table()->insert("foo", int64_t(42));
+  profile["Options"].as_table()->insert("foo", int64_t(42));
   std::ofstream test_manifest_file(filepath);
   ASSERT_TRUE(test_manifest_file.is_open());
   test_manifest_file << profile;
@@ -1690,7 +1690,7 @@ TEST_F(ConnectionProfiles, ExtraIntOption) {
 TEST_F(ConnectionProfiles, ExtraDoubleOption) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = simple_profile;
-  profile["options"].as_table()->insert("foo", 42.0);
+  profile["Options"].as_table()->insert("foo", 42.0);
   std::ofstream test_manifest_file(filepath);
   ASSERT_TRUE(test_manifest_file.is_open());
   test_manifest_file << profile;
@@ -1712,9 +1712,9 @@ TEST_F(ConnectionProfiles, ExtraDoubleOption) {
 TEST_F(ConnectionProfiles, DotSeparatedKey) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo.bar.baz = "bar"
   )");
 
@@ -1740,9 +1740,9 @@ TEST_F(ConnectionProfiles, DotSeparatedKey) {
 TEST_F(ConnectionProfiles, UseEnvVar) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "{{ env_var(ADBC_PROFILE_PATH) }}"
   )|");
 
@@ -1768,9 +1768,9 @@ TEST_F(ConnectionProfiles, UseEnvVar) {
 TEST_F(ConnectionProfiles, UseEnvVarNotExist) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "{{ env_var(FOOBAR_ENV_VAR_THAT_DOES_NOT_EXIST) }}"
   )|");
 
@@ -1792,12 +1792,40 @@ TEST_F(ConnectionProfiles, UseEnvVarNotExist) {
   UnsetConfigPath();
 }
 
+TEST_F(ConnectionProfiles, UseEnvVarNotExistNoBail) {
+  auto filepath = temp_dir / "profile.toml";
+  toml::table profile = toml::parse(R"|(
+    profile_version = 1
+    driver = "adbc_driver_sqlite"
+    [Options]
+    foo = "foo{{ env_var(FOOBAR_ENV_VAR_THAT_DOES_NOT_EXIST) }}bar"
+  )|");
+
+  std::ofstream test_manifest_file(filepath);
+  ASSERT_TRUE(test_manifest_file.is_open());
+  test_manifest_file << profile;
+  test_manifest_file.close();
+
+  adbc_validation::Handle<struct AdbcDatabase> database;
+
+  // find profile by name using ADBC_PROFILE_PATH
+  SetConfigPath(temp_dir.string().c_str());
+  ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error));
+  ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", "profile", 
&error),
+              IsOkStatus(&error));
+  ASSERT_THAT(AdbcDatabaseInit(&database.value, &error),
+              IsStatus(ADBC_STATUS_NOT_IMPLEMENTED, &error));
+  ASSERT_THAT(error.message,
+              ::testing::HasSubstr("Unknown database option foo='foobar'"));
+  UnsetConfigPath();
+}
+
 TEST_F(ConnectionProfiles, UseEnvVarMalformed) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "{{ env_var(ENV_VAR_WITHOUT_CLOSING_PAREN }}"
   )|");
 
@@ -1825,9 +1853,9 @@ TEST_F(ConnectionProfiles, UseEnvVarMalformed) {
 TEST_F(ConnectionProfiles, UseEnvVarMissingArg) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "{{ env_var() }}"
   )|");
 
@@ -1854,9 +1882,9 @@ TEST_F(ConnectionProfiles, UseEnvVarMissingArg) {
 TEST_F(ConnectionProfiles, UseEnvVarInterpolation) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "super {{ env_var(ADBC_PROFILE_PATH) }} duper"
   )|");
 
@@ -1882,9 +1910,9 @@ TEST_F(ConnectionProfiles, UseEnvVarInterpolation) {
 TEST_F(ConnectionProfiles, UseEnvVarInterpolationMultiple) {
   auto filepath = temp_dir / "profile.toml";
   toml::table profile = toml::parse(R"|(
-    version = 1
+    profile_version = 1
     driver = "adbc_driver_sqlite"
-    [options]
+    [Options]
     foo = "super {{ env_var(ADBC_PROFILE_PATH) }} duper {{ 
env_var(ADBC_PROFILE_PATH) }} end"
   )|");
 
@@ -1937,6 +1965,46 @@ TEST_F(ConnectionProfiles, ProfileNotFound) {
   UnsetConfigPath();
 }
 
+TEST_F(ConnectionProfiles, CondaProfileTest) {
+#if ADBC_CONDA_BUILD
+  constexpr bool is_conda_build = true;
+#else
+  constexpr bool is_conda_build = false;
+#endif  // ADBC_CONDA_BUILD
+
+  std::cerr << "ADBC_CONDA_BUILD: " << (is_conda_build ? "defined" : "not 
defined")
+            << std::endl;
+
+  auto filepath = temp_dir / "etc" / "adbc" / "profiles" / "sqlite-test.toml";
+  std::filesystem::create_directories(filepath.parent_path());
+  std::ofstream test_profile_file(filepath);
+  ASSERT_TRUE(test_profile_file.is_open());
+  test_profile_file << simple_profile;
+  test_profile_file.close();
+
+#ifdef _WIN32
+  ASSERT_EQ(0, ::_wputenv_s(L"CONDA_PREFIX", temp_dir.native().c_str()));
+#else
+  ASSERT_EQ(0, ::setenv("CONDA_PREFIX", temp_dir.native().c_str(), 1));
+#endif  // _WIN32
+
+  adbc_validation::Handle<struct AdbcDatabase> database;
+
+  // absolute path to the profile
+  ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error));
+  ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", "sqlite-test", 
&error),
+              IsOkStatus(&error));
+  if constexpr (is_conda_build) {
+    ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error));
+  } else {
+    ASSERT_THAT(AdbcDatabaseInit(&database.value, &error),
+                IsStatus(ADBC_STATUS_NOT_FOUND, &error));
+    ASSERT_THAT(error.message,
+                ::testing::HasSubstr("not enabled at build time: Conda 
prefix"));
+  }
+  ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), 
IsOkStatus(&error));
+}
+
 TEST_F(ConnectionProfiles, CustomProfileProvider) {
   adbc_validation::Handle<struct AdbcDatabase> database;
 
diff --git a/c/include/arrow-adbc/adbc_driver_manager.h 
b/c/include/arrow-adbc/adbc_driver_manager.h
index 9418e0072..f839f1d54 100644
--- a/c/include/arrow-adbc/adbc_driver_manager.h
+++ b/c/include/arrow-adbc/adbc_driver_manager.h
@@ -321,10 +321,10 @@ typedef AdbcStatusCode (*AdbcConnectionProfileProvider)(
 ///
 /// For file-based profiles the expected format is as follows:
 /// ```toml
-/// version = 1
+/// profile_version = 1
 /// driver = "driver_name"
 ///
-/// [options]
+/// [Options]
 /// option1 = "value1"
 /// option2 = 42
 /// option3 = 3.14
diff --git a/ci/scripts/python_venv_test.sh b/ci/scripts/python_venv_test.sh
index bbee58ba8..a5c0a8efa 100755
--- a/ci/scripts/python_venv_test.sh
+++ b/ci/scripts/python_venv_test.sh
@@ -40,9 +40,9 @@ EOF
 
     mkdir -p "${scratch}/.venv/etc/adbc/profiles/sqlite/"
     cat >"${scratch}/.venv/etc/adbc/profiles/sqlite/dev.toml" <<EOF
-version = 1
+profile_version = 1
 driver = "sqlite"
-[options]
+[Options]
 uri = "file:///tmp/test.db"
 EOF
 
diff --git a/docs/source/format/connection_profiles.rst 
b/docs/source/format/connection_profiles.rst
index 228ff448b..c9336b8c9 100644
--- a/docs/source/format/connection_profiles.rst
+++ b/docs/source/format/connection_profiles.rst
@@ -74,10 +74,10 @@ Filesystem-based profiles use TOML format with the 
following structure:
 
 .. code-block:: toml
 
-   version = 1
+   profile_version = 1
    driver = "snowflake"
 
-   [options]
+   [Options]
    # String options
    adbc.snowflake.sql.account = "mycompany"
    adbc.snowflake.sql.warehouse = "COMPUTE_WH"
@@ -93,14 +93,14 @@ Filesystem-based profiles use TOML format with the 
following structure:
    # Boolean options (converted to "true" or "false" strings)
    adbc.snowflake.sql.client_session_keep_alive = true
 
-version
--------
+profile_version
+---------------
 
 - **Required**: Yes
 - **Type**: Integer
 - **Supported values**: ``1``
 
-The ``version`` field specifies the profile format version. Currently, only 
version 1 is supported.
+The ``profile_version`` field specifies the profile format version. Currently, 
only version 1 is supported.
 This will enable future changes while maintaining backward compatibility.
 
 driver
@@ -122,7 +122,7 @@ 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. 
Options can be of the following types:
 
 **String values**
    Applied using ``AdbcDatabaseSetOption()``
@@ -175,15 +175,15 @@ Profile values can reference environment variables using 
the ``{{ env_var() }}``
 
 .. code-block:: toml
 
-   version = 1
+   profile_version = 1
    driver = "adbc_driver_snowflake"
 
-   [options]
+   [Options]
    adbc.snowflake.sql.account = "{{ env_var(SNOWFLAKE_ACCOUNT) }}"
    adbc.snowflake.sql.auth_token = "{{ env_var(SNOWFLAKE_TOKEN) }}"
    adbc.snowflake.sql.warehouse = "COMPUTE_WH"
 
-When the driver manager encounters ``{{ env_var(VAR_NAME) }}``, it replaces 
the value with the contents of environment variable ``VAR_NAME``. If the 
environment variable is not set, the value becomes an empty string.
+When the driver manager encounters ``{{ env_var(VAR_NAME) }}``, it replaces 
the placeholder with the contents of environment variable ``VAR_NAME``. If the 
environment variable is not set, the placeholder is replaced with an empty 
string and processing of the rest of the value continues (e.g. ``"foo{{ 
env_var(MISSING) }}bar"`` becomes ``"foobar"``).
 
 Profile Search Locations
 =========================
@@ -227,10 +227,10 @@ File: ``~/.config/adbc/profiles/snowflake_prod.toml``
 
 .. code-block:: toml
 
-   version = 1
+   profile_version = 1
    driver = "snowflake"
 
-   [options]
+   [Options]
    adbc.snowflake.sql.account = "{{ env_var(SNOWFLAKE_ACCOUNT) }}"
    adbc.snowflake.sql.auth_token = "{{ env_var(SNOWFLAKE_TOKEN) }}"
    adbc.snowflake.sql.warehouse = "PRODUCTION_WH"
@@ -260,10 +260,10 @@ File: ``~/.config/adbc/profiles/postgres_dev.toml``
 
 .. code-block:: toml
 
-   version = 1
+   profile_version = 1
    driver = "postgresql"
 
-   [options]
+   [Options]
    uri = "postgresql://localhost:5432/dev_db?sslmode=disable"
    username = "dev_user"
    password = "{{ env_var(POSTGRES_DEV_PASSWORD) }}"
@@ -277,10 +277,10 @@ File: ``~/.config/adbc/profiles/default_timeouts.toml``
 
 .. code-block:: toml
 
-   version = 1
+   profile_version = 1
    # No driver specified - can be used with any driver
 
-   [options]
+   [Options]
    adbc.connection.timeout = 30.0
    adbc.statement.timeout = 60.0
 
@@ -303,7 +303,7 @@ Option Precedence
 Options are applied in the following order (later overrides earlier):
 
 1. Driver defaults
-2. Profile options (from ``[options]`` section)
+2. Profile options (from ``[Options]`` section)
 3. Options set via ``AdbcDatabaseSetOption()`` before ``AdbcDatabaseInit()``
 
 Example:
@@ -434,7 +434,7 @@ Store credentials separately from code:
 
 .. code-block:: toml
 
-   [options]
+   [Options]
    adbc.snowflake.sql.account = "mycompany"
    adbc.snowflake.sql.auth_token = "{{ env_var(SNOWFLAKE_TOKEN) }}"
 
diff --git a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc 
b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
index aebf7408f..703eddaf9 100644
--- a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
@@ -295,14 +295,14 @@ AdbcStatusCode LoadProfileFile(const 
std::filesystem::path& profile_path,
   }
 
   profile.path = profile_path;
-  if (!config["version"].is_integer()) {
+  if (!config["profile_version"].is_integer()) {
     std::string message =
         "Profile version is not an integer in profile '" + 
profile_path.string() + "'";
     SetError(error, std::move(message));
     return ADBC_STATUS_INVALID_ARGUMENT;
   }
 
-  const auto version = config["version"].value_or(int64_t(1));
+  const auto version = config["profile_version"].value_or(int64_t(1));
   switch (version) {
     case 1:
       break;
@@ -317,7 +317,7 @@ AdbcStatusCode LoadProfileFile(const std::filesystem::path& 
profile_path,
 
   profile.driver = config["driver"].value_or(""s);
 
-  auto options = config.at_path("options");
+  auto options = config.at_path("Options");
   if (!options.is_table()) {
     std::string message =
         "Profile options is not a table in profile '" + profile_path.string() 
+ "'";
diff --git a/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h 
b/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h
index 9418e0072..f839f1d54 100644
--- a/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h
+++ b/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h
@@ -321,10 +321,10 @@ typedef AdbcStatusCode (*AdbcConnectionProfileProvider)(
 ///
 /// For file-based profiles the expected format is as follows:
 /// ```toml
-/// version = 1
+/// profile_version = 1
 /// driver = "driver_name"
 ///
-/// [options]
+/// [Options]
 /// option1 = "value1"
 /// option2 = 42
 /// option3 = 3.14
diff --git a/javascript/__test__/profile.spec.ts 
b/javascript/__test__/profile.spec.ts
index 85d17821b..4f346f3c2 100644
--- a/javascript/__test__/profile.spec.ts
+++ b/javascript/__test__/profile.spec.ts
@@ -42,7 +42,7 @@ test('profile: load database from profile:// URI', async () 
=> {
     const toml = (p: string) => p.replaceAll('\\', '/')
     writeFileSync(
       join(tmpDir, 'test_sqlite.toml'),
-      `version = 1\ndriver = "${toml(driver)}"\n\n[options]\nuri = 
"${toml(dbPath)}"\n`,
+      `profile_version = 1\ndriver = "${toml(driver)}"\n\n[Options]\nuri = 
"${toml(dbPath)}"\n`,
     )
 
     const db = new AdbcDatabase({
diff --git a/python/adbc_driver_manager/tests/test_profile.py 
b/python/adbc_driver_manager/tests/test_profile.py
index 2aea0718a..a49f3afe5 100644
--- a/python/adbc_driver_manager/tests/test_profile.py
+++ b/python/adbc_driver_manager/tests/test_profile.py
@@ -41,10 +41,10 @@ def profile_dir(tmp_path_factory) -> 
typing.Generator[pathlib.Path, None, None]:
 def sqlitedev(profile_dir) -> str:
     with (profile_dir / "sqlitedev.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 """)
     return "sqlitedev"
 
@@ -97,9 +97,9 @@ def test_option_env_var(subtests, tmp_path, monkeypatch) -> 
None:
         with subtests.test(i=i, msg=raw_value):
             with (tmp_path / "subst.toml").open("w") as sink:
                 sink.write(f"""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 adbc.foo.bar = "{raw_value}"
 """)
 
@@ -124,9 +124,9 @@ def test_option_env_var_multiple(tmp_path, monkeypatch) -> 
None:
         rest = "{{ env_var(TEST_DIR) }}/{{ env_var(TEST_NUM) }}"
         batch = "{{ env_var(BATCH_SIZE) }}"
         sink.write(f"""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 uri = "file://{windows}{rest}.db"
 adbc.sqlite.query.batch_rows = "{batch}"
 """)
@@ -171,9 +171,9 @@ def test_option_env_var_invalid(subtests, tmp_path, 
monkeypatch) -> None:
     ]:
         with (tmp_path / "subst.toml").open("w") as sink:
             sink.write(f"""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 uri = "{contents}"
     """)
 
@@ -192,9 +192,9 @@ def test_option_override(tmp_path, monkeypatch) -> None:
 
     with (tmp_path / "dev.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 adbc.sqlite.query.batch_rows = 7
 """)
 
@@ -220,8 +220,8 @@ def test_driver_optional(subtests, tmp_path, monkeypatch) 
-> None:
 
     with (tmp_path / "nodriver.toml").open("w") as sink:
         sink.write("""
-version = 1
-[options]
+profile_version = 1
+[Options]
 """)
 
     with subtests.test(msg="missing driver"):
@@ -269,9 +269,9 @@ def test_driver_invalid(subtests, tmp_path, monkeypatch) -> 
None:
 
     with (tmp_path / "nodriver.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = 2
-[options]
+[Options]
 """)
 
     with subtests.test(msg="numeric driver"):
@@ -281,10 +281,10 @@ driver = 2
 
     with (tmp_path / "nodriver.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 [driver]
 foo = "bar"
-[options]
+[Options]
 """)
 
     with subtests.test(msg="table driver"):
@@ -300,7 +300,7 @@ def test_version_invalid(tmp_path, monkeypatch) -> None:
     with (tmp_path / "badversion.toml").open("w") as sink:
         sink.write("""
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
     with pytest.raises(
         dbapi.ProgrammingError, match="Profile version is not an integer"
@@ -311,8 +311,8 @@ driver = "adbc_driver_sqlite"
     with (tmp_path / "badversion.toml").open("w") as sink:
         sink.write("""
 driver = "adbc_driver_sqlite"
-version = "1"
-[options]
+profile_version = "1"
+[Options]
 """)
     with pytest.raises(
         dbapi.ProgrammingError, match="Profile version is not an integer"
@@ -323,8 +323,8 @@ version = "1"
     with (tmp_path / "badversion.toml").open("w") as sink:
         sink.write("""
 driver = "adbc_driver_sqlite"
-version = 9001
-[options]
+profile_version = 9001
+[Options]
 """)
     with pytest.raises(
         dbapi.ProgrammingError, match="Profile version '9001' is not supported"
@@ -339,8 +339,8 @@ def test_reject_malformed(tmp_path, monkeypatch) -> None:
 
     with (tmp_path / "nodriver.toml").open("w") as sink:
         sink.write("""
-version = 1
-[options]
+profile_version = 1
+[Options]
 """)
     with pytest.raises(dbapi.ProgrammingError, match="Must set 'driver' 
option"):
         with dbapi.connect("profile://nodriver"):
@@ -348,7 +348,7 @@ version = 1
 
     with (tmp_path / "nooptions.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 """)
     with pytest.raises(dbapi.ProgrammingError, match="Profile options is not a 
table"):
@@ -357,9 +357,9 @@ driver = "adbc_driver_sqlite"
 
     with (tmp_path / "unknownkeys.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 [foobar]
 """)
     # Unknown keys is OK, though
@@ -376,9 +376,9 @@ def test_driver_options(tmp_path, monkeypatch) -> None:
     uri = f"file://{windows}{tmp_path.resolve().absolute().as_posix()}/foo.db"
     with (tmp_path / "sqlitetest.toml").open("w") as sink:
         sink.write(f"""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 uri = "{uri}"
 """)
     with dbapi.connect("profile://sqlitetest") as conn:
@@ -403,9 +403,9 @@ shared = "adbc_driver_sqlite"
 
     with (profile_path / "proddata.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "sqlitemanifest"
-[options]
+[Options]
 """)
     with dbapi.connect("profile://proddata") as conn:
         with conn.cursor() as cursor:
@@ -421,9 +421,9 @@ def test_subdir(monkeypatch, tmp_path) -> None:
 
     with (subdir / "sqlitetest.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
     with dbapi.connect("profile://sqlite/prod/sqlitetest") as conn:
         with conn.cursor() as cursor:
@@ -439,9 +439,9 @@ def test_absolute(monkeypatch, tmp_path) -> None:
 
     with (subdir / "sqlitetest.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
 
     path = (subdir / "sqlitetest.toml").absolute().as_posix()
@@ -470,16 +470,16 @@ def test_user_path() -> None:
 
     with (path / f"{profile}.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
 
     with (subpath / f"{profile}.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
 
     try:
@@ -505,9 +505,9 @@ def test_conda(conda_prefix) -> None:
 
     with (path / f"{profile}.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
     try:
         with dbapi.connect(f"profile://{profile}") as conn:
@@ -526,9 +526,9 @@ def test_conda_subdir(conda_prefix) -> None:
 
     with (path / f"{profile}.toml").open("w") as sink:
         sink.write("""
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options]
+[Options]
 """)
     try:
         with dbapi.connect(f"profile://{subdir}/{profile}") as conn:
diff --git a/rust/driver_manager/src/profile.rs 
b/rust/driver_manager/src/profile.rs
index 5d4946e3e..8e93ff19f 100644
--- a/rust/driver_manager/src/profile.rs
+++ b/rust/driver_manager/src/profile.rs
@@ -220,15 +220,15 @@ fn process_options(
 /// Profile files must be valid TOML with the following structure:
 ///
 /// ```toml
-/// version = 1
+/// profile_version = 1
 /// driver = "driver_name"
 ///
-/// [options]
+/// [Options]
 /// option_key = "option_value"
 /// nested.key = "nested_value"
 /// ```
 ///
-/// Currently, only version 1 profiles are supported.
+/// Currently, only profile_version 1 profiles are supported.
 #[derive(Debug)]
 pub struct FilesystemProfile {
     profile_path: PathBuf,
@@ -268,7 +268,7 @@ impl FilesystemProfile {
 
         let profile_version = profile
             .get_ref()
-            .get("version")
+            .get("profile_version")
             .and_then(|v| v.get_ref().as_integer())
             .map(|v| v.as_str())
             .unwrap_or("1");
@@ -292,11 +292,11 @@ impl FilesystemProfile {
 
         let options_table = profile
             .get_ref()
-            .get("options")
+            .get("Options")
             .and_then(|v| v.get_ref().as_table())
             .ok_or_else(|| {
                 Error::with_message_and_status(
-                    "missing or invalid 'options' table in 
profile".to_string(),
+                    "missing or invalid 'Options' table in 
profile".to_string(),
                     Status::InvalidArguments,
                 )
             })?;
@@ -589,10 +589,10 @@ deep = "value"
                 "invalid_version_high.toml",
                 Some(
                     r#"
-version = 99
+profile_version = 99
 driver = "test_driver"
 
-[options]
+[Options]
 key = "value"
 "#,
                 ),
@@ -603,10 +603,10 @@ key = "value"
                 "version_zero.toml",
                 Some(
                     r#"
-version = 0
+profile_version = 0
 driver = "test_driver"
 
-[options]
+[Options]
 key = "value"
 "#,
                 ),
@@ -617,10 +617,10 @@ key = "value"
                 "version_two.toml",
                 Some(
                     r#"
-version = 2
+profile_version = 2
 driver = "test_driver"
 
-[options]
+[Options]
 key = "value"
 "#,
                 ),
@@ -689,13 +689,144 @@ key = "value"
         }
     }
 
+    #[test]
+    fn test_process_profile_value() {
+        // (name, env_vars_to_set, input, expected_ok / expected_err_fragment)
+        struct TestCase<'a>(
+            &'a str,
+            Vec<(&'a str, &'a str)>,
+            &'a str,
+            std::result::Result<&'a str, &'a str>,
+        );
+
+        let test_cases: Vec<TestCase> = vec![
+            TestCase("empty string", vec![], "", Ok("")),
+            TestCase(
+                "plain string no templates",
+                vec![],
+                "just a plain string",
+                Ok("just a plain string"),
+            ),
+            TestCase(
+                "string with special chars but no templates",
+                vec![],
+                "host=localhost port=5432",
+                Ok("host=localhost port=5432"),
+            ),
+            TestCase(
+                "env var present",
+                vec![("ADBC_TEST_PPV_HOST", "myhost.example.com")],
+                "{{ env_var(ADBC_TEST_PPV_HOST) }}",
+                Ok("myhost.example.com"),
+            ),
+            TestCase(
+                "env var not set returns empty string",
+                vec![],
+                "{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}",
+                Ok(""),
+            ),
+            TestCase(
+                "env var not set interpolates the empty string",
+                vec![],
+                "foo{{ env_var(ADBC_TEST_PPV_NONEXISTENT_XYZ) }}bar",
+                Ok("foobar"),
+            ),
+            TestCase(
+                "mixed literal text and env var",
+                vec![("ADBC_TEST_PPV_PORT", "5432")],
+                "host=localhost port={{ env_var(ADBC_TEST_PPV_PORT) }}",
+                Ok("host=localhost port=5432"),
+            ),
+            TestCase(
+                "multiple env var replacements",
+                vec![
+                    ("ADBC_TEST_PPV_USER", "alice"),
+                    ("ADBC_TEST_PPV_PASS", "secret"),
+                ],
+                "{{ env_var(ADBC_TEST_PPV_USER) }}:{{ 
env_var(ADBC_TEST_PPV_PASS) }}",
+                Ok("alice:secret"),
+            ),
+            TestCase(
+                "extra whitespace inside braces",
+                vec![("ADBC_TEST_PPV_DB", "mydb")],
+                "{{  env_var(ADBC_TEST_PPV_DB)  }}",
+                Ok("mydb"),
+            ),
+            TestCase(
+                "invalid expression not env_var",
+                vec![],
+                "{{ something_invalid }}",
+                Err("invalid profile replacement expression"),
+            ),
+            TestCase(
+                "empty env var name",
+                vec![],
+                "{{ env_var() }}",
+                Err("empty environment variable name"),
+            ),
+            TestCase(
+                "empty env var name with whitespace",
+                vec![],
+                "{{ env_var(   ) }}",
+                Err("empty environment variable name"),
+            ),
+        ];
+
+        for TestCase(name, env_vars, input, expected) in test_cases {
+            for (k, v) in &env_vars {
+                std::env::set_var(k, v);
+            }
+
+            let result = process_profile_value(input);
+
+            match expected {
+                Ok(expected_str) => match result.unwrap_or_else(|e| {
+                    panic!("Test case '{}': expected Ok but got Err: {:?}", 
name, e)
+                }) {
+                    OptionValue::String(s) => {
+                        assert_eq!(s, expected_str, "Test case '{}': string 
mismatch", name)
+                    }
+                    other => panic!(
+                        "Test case '{}': expected OptionValue::String, got 
{:?}",
+                        name, other
+                    ),
+                },
+                Err(err_fragment) => {
+                    assert!(
+                        result.is_err(),
+                        "Test case '{}': expected Err but got Ok",
+                        name
+                    );
+                    let err = result.unwrap_err();
+                    assert_eq!(
+                        err.status,
+                        Status::InvalidArguments,
+                        "Test case '{}': wrong status",
+                        name
+                    );
+                    assert!(
+                        err.message.contains(err_fragment),
+                        "Test case '{}': expected {:?} in error message, got 
{:?}",
+                        name,
+                        err_fragment,
+                        err.message
+                    );
+                }
+            }
+
+            for (k, _) in &env_vars {
+                std::env::remove_var(k);
+            }
+        }
+    }
+
     #[test]
     fn test_filesystem_profile_provider() {
         let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "test_driver"
 
-[options]
+[Options]
 test_key = "test_value"
 "#;
 
diff --git a/rust/driver_manager/tests/connection_profile.rs 
b/rust/driver_manager/tests/connection_profile.rs
index e1bdd353f..8a0eae3ae 100644
--- a/rust/driver_manager/tests/connection_profile.rs
+++ b/rust/driver_manager/tests/connection_profile.rs
@@ -24,6 +24,7 @@ use adbc_driver_manager::profile::{
 };
 use adbc_driver_manager::ManagedDatabase;
 use serial_test::serial;
+use std::env;
 
 mod common;
 
@@ -41,10 +42,10 @@ fn write_profile_to_tempfile(profile_name: &str, content: 
&str) -> (tempfile::Te
 
 fn simple_profile() -> String {
     r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 "#
     .to_string()
@@ -52,15 +53,15 @@ uri = ":memory:"
 
 fn profile_with_nested_options() -> String {
     r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
-[options.connection]
+[Options.connection]
 timeout = 30
 retry = true
-[options.connection.pool]
+[Options.connection.pool]
 max_size = 10
 min_size = 2
 idle_timeout = 300.5
@@ -70,10 +71,10 @@ idle_timeout = 300.5
 
 fn profile_with_all_types() -> String {
     r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 string_opt = "test_value"
 int_opt = 42
@@ -85,9 +86,9 @@ bool_opt = true
 
 fn profile_without_driver() -> String {
     r#"
-version = 1
+profile_version = 1
 
-[options]
+[Options]
 uri = ":memory:"
 "#
     .to_string()
@@ -95,7 +96,7 @@ uri = ":memory:"
 
 fn profile_without_options() -> String {
     r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 "#
     .to_string()
@@ -103,10 +104,10 @@ driver = "adbc_driver_sqlite"
 
 fn profile_with_unsupported_version() -> String {
     r#"
-version = 2
+profile_version = 2
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 "#
     .to_string()
@@ -114,9 +115,9 @@ uri = ":memory:"
 
 fn invalid_toml() -> &'static str {
     r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
-[options
+[Options
 uri = ":memory:"
 "#
 }
@@ -242,7 +243,7 @@ fn test_filesystem_profile_error_cases() {
             "without options",
             profile_without_options(),
             Status::InvalidArguments,
-            "missing or invalid 'options' table in profile",
+            "missing or invalid 'Options' table in profile",
         ),
         (
             "unsupported version",
@@ -572,3 +573,65 @@ fn 
test_profile_hierarchical_path_additional_search_paths() {
         .close()
         .expect("Failed to close/remove temporary directory");
 }
+
+#[test]
+fn test_profile_conda_prefix() {
+    #[cfg(conda_build)]
+    let is_conda_build = true;
+    #[cfg(not(conda_build))]
+    let is_conda_build = false;
+
+    eprintln!(
+        "Is conda build: {}",
+        if is_conda_build {
+            "defined"
+        } else {
+            "not defined"
+        }
+    );
+    let tmp_dir = tempfile::Builder::new()
+        .prefix("adbc_profile_conda_prefix_test")
+        .tempdir()
+        .expect("Failed to create temporary directory");
+
+    let filepath = tmp_dir
+        .path()
+        .join("etc")
+        .join("adbc")
+        .join("profiles")
+        .join("sqlite-profile.toml");
+
+    std::fs::create_dir_all(filepath.parent().unwrap())
+        .expect("Failed to create directories for conda prefix test");
+    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 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 {
+        assert!(result.is_err(), "Expected error for non-conda build");
+        if let Err(err) = result {
+            assert!(
+                err.message.contains("Profile not found: sqlite-profile"),
+                "Expected 'Profile file does not exist' error, got: {}",
+                err.message
+            );
+        }
+    }
+
+    tmp_dir
+        .close()
+        .expect("Failed to close/remove temporary directory")
+}
diff --git a/rust/driver_manager/tests/test_env_var_profiles.rs 
b/rust/driver_manager/tests/test_env_var_profiles.rs
index 04a972d41..bd35d3453 100644
--- a/rust/driver_manager/tests/test_env_var_profiles.rs
+++ b/rust/driver_manager/tests/test_env_var_profiles.rs
@@ -41,10 +41,10 @@ fn test_env_var_replacement_basic() {
     env::set_var("ADBC_TEST_ENV_VAR", ":memory:");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = "{{ env_var(ADBC_TEST_ENV_VAR) }}"
 "#;
 
@@ -88,10 +88,10 @@ fn test_env_var_replacement_empty() {
     env::remove_var("ADBC_NONEXISTENT_VAR_12345");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "{{ env_var(ADBC_NONEXISTENT_VAR_12345) }}"
 "#;
@@ -123,10 +123,10 @@ fn test_env_var_replacement_missing_closing_paren() {
         .expect("Failed to create temporary directory");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "{{ env_var(SOME_VAR }}"
 "#;
@@ -161,10 +161,10 @@ fn test_env_var_replacement_missing_arg() {
         .expect("Failed to create temporary directory");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "{{ env_var() }}"
 "#;
@@ -202,10 +202,10 @@ fn test_env_var_replacement_interpolation() {
     env::set_var("ADBC_TEST_INTERPOLATE", "middle_value");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "prefix_{{ env_var(ADBC_TEST_INTERPOLATE) }}_suffix"
 "#;
@@ -249,10 +249,10 @@ fn test_env_var_replacement_multiple() {
     env::set_var("ADBC_TEST_VAR2", "second");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "{{ env_var(ADBC_TEST_VAR1) }}_and_{{ env_var(ADBC_TEST_VAR2) }}"
 "#;
@@ -298,10 +298,10 @@ fn test_env_var_replacement_whitespace() {
     env::set_var("ADBC_TEST_WHITESPACE", "value");
 
     let profile_content = r#"
-version = 1
+profile_version = 1
 driver = "adbc_driver_sqlite"
 
-[options]
+[Options]
 uri = ":memory:"
 test_option = "{{   env_var(  ADBC_TEST_WHITESPACE  )   }}"
 "#;


Reply via email to