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 ) }}"
"#;