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 3115969be fix(c/driver_manager): test and fix bugs in profiles (#4080)
3115969be is described below
commit 3115969be9e4adad10badf59abb715f08b0d378b
Author: David Li <[email protected]>
AuthorDate: Sat Mar 14 15:47:35 2026 +0900
fix(c/driver_manager): test and fix bugs in profiles (#4080)
- Fix substitution of undefined env var erasing the entire value instead
of substituting in a blank string.
- Improve error messages.
- Add end-to-end tests with Python.
- Add end-to-end tests in Conda/virtualenv.
- Add end-to-end tests with the user path (`~/.config`, etc.)
This is not complete; see #4082, #4085, #4086, #4087 for things that
also need to be fixed, but this establishes a baseline of tests.
Closes #4024.
---
.github/workflows/native-unix.yml | 3 +
.github/workflows/native-windows.yml | 10 +-
c/driver/framework/connection.h | 3 +-
c/driver/sqlite/sqlite.cc | 57 ++-
c/driver_manager/adbc_driver_manager.cc | 2 +-
c/driver_manager/adbc_driver_manager_internal.h | 4 +-
c/driver_manager/adbc_driver_manager_profiles.cc | 169 +++----
c/driver_manager/adbc_driver_manager_test.cc | 14 +-
ci/scripts/python_venv_test.sh | 22 +
go/adbc/drivermgr/adbc_driver_manager.cc | 2 +-
go/adbc/drivermgr/adbc_driver_manager_internal.h | 4 +-
go/adbc/drivermgr/adbc_driver_manager_profiles.cc | 169 +++----
python/adbc_driver_manager/pyproject.toml | 3 +-
python/adbc_driver_manager/tests/conftest.py | 26 ++
python/adbc_driver_manager/tests/test_profile.py | 543 ++++++++++++++++++++++
15 files changed, 837 insertions(+), 194 deletions(-)
diff --git a/.github/workflows/native-unix.yml
b/.github/workflows/native-unix.yml
index 848b72cd0..2cf25cde7 100644
--- a/.github/workflows/native-unix.yml
+++ b/.github/workflows/native-unix.yml
@@ -589,6 +589,7 @@ jobs:
ADBC_USE_UBSAN: "0"
run: |
export PATH=$RUNNER_TOOL_CACHE/go/${GO_VERSION}/x64/bin:$PATH
+ export _ADBC_IS_CONDA=1
./ci/scripts/python_build.sh "$(pwd)" "$(pwd)/build" "$HOME/local"
- name: Build Panic Dummy
run: |
@@ -603,6 +604,8 @@ jobs:
run: |
cargo build -padbc_dummy
- name: Test Python Driver Manager
+ env:
+ PYTEST_ADDOPTS: "--run-system"
run: |
if [[ $(uname) = "Darwin" ]]; then
export
PANICDUMMY_LIBRARY_PATH=$(pwd)/go/adbc/pkg/libadbc_driver_panicdummy.dylib
diff --git a/.github/workflows/native-windows.yml
b/.github/workflows/native-windows.yml
index 19691dd51..4fdf8bb07 100644
--- a/.github/workflows/native-windows.yml
+++ b/.github/workflows/native-windows.yml
@@ -337,21 +337,15 @@ jobs:
env:
BUILD_ALL: "0"
BUILD_DRIVER_MANAGER: "1"
- run: .\ci\scripts\python_build.ps1 $pwd $pwd\build
- - name: Build Python Driver PostgreSQL
- env:
- BUILD_ALL: "0"
BUILD_DRIVER_POSTGRESQL: "1"
- run: .\ci\scripts\python_build.ps1 $pwd $pwd\build
- - name: Build Python Driver SQLite
- env:
- BUILD_ALL: "0"
BUILD_DRIVER_SQLITE: "1"
+ _ADBC_IS_CONDA: "1"
run: .\ci\scripts\python_build.ps1 $pwd $pwd\build
- name: Test Python Driver Manager
env:
BUILD_ALL: "0"
BUILD_DRIVER_MANAGER: "1"
+ PYTEST_ADDOPTS: "--run-system"
run: .\ci\scripts\python_test.ps1 $pwd $pwd\build
- name: Test Python Driver PostgreSQL
env:
diff --git a/c/driver/framework/connection.h b/c/driver/framework/connection.h
index 783dc8046..46259eb07 100644
--- a/c/driver/framework/connection.h
+++ b/c/driver/framework/connection.h
@@ -165,7 +165,8 @@ class Connection : public ObjectBase {
}
return driver::Option();
}
- return Base::GetOption(key);
+ return status::NotImplemented(Derived::kErrorPrefix, " Unknown connection
option ",
+ key);
}
/// \internal
diff --git a/c/driver/sqlite/sqlite.cc b/c/driver/sqlite/sqlite.cc
index c2a1ae798..e91263b18 100644
--- a/c/driver/sqlite/sqlite.cc
+++ b/c/driver/sqlite/sqlite.cc
@@ -52,6 +52,7 @@ constexpr std::string_view
kConnectionOptionLoadExtensionEntrypoint =
/// The batch size for query results (and for initial type inference)
constexpr std::string_view kStatementOptionBatchRows =
"adbc.sqlite.query.batch_rows";
constexpr std::string_view kStatementOptionBindByName =
"adbc.statement.bind_by_name";
+constexpr int kDefaultBatchSize = 1024;
std::string_view GetColumnText(sqlite3_stmt* stmt, int index) {
return {
@@ -547,6 +548,15 @@ class SqliteDatabase : public
driver::Database<SqliteDatabase> {
return Base::ReleaseImpl();
}
+ Result<driver::Option> GetOption(std::string_view key) override {
+ if (key == "uri") {
+ return driver::Option(uri_);
+ } else if (key == kStatementOptionBatchRows) {
+ return driver::Option(static_cast<int64_t>(batch_size_));
+ }
+ return Base::GetOption(key);
+ }
+
Status SetOptionImpl(std::string_view key, driver::Option value) override {
if (key == "uri") {
if (lifecycle_state_ != driver::LifecycleState::kUninitialized) {
@@ -556,13 +566,32 @@ class SqliteDatabase : public
driver::Database<SqliteDatabase> {
UNWRAP_RESULT(uri, value.AsString());
uri_ = std::move(uri);
return status::Ok();
+ } else if (key == kStatementOptionBatchRows) {
+ if (lifecycle_state_ != driver::LifecycleState::kUninitialized) {
+ return status::fmt::InvalidState(
+ "{} cannot set {} after AdbcDatabaseInit, set it directly on the
statement "
+ "instead",
+ kErrorPrefix, key);
+ }
+ int64_t batch_size;
+ UNWRAP_RESULT(batch_size, value.AsInt());
+ if (batch_size <= 0 || batch_size > std::numeric_limits<int>::max()) {
+ return status::fmt::InvalidArgument(
+ "{} Invalid statement option value {}={} (value is non-positive or
out of "
+ "range of int)",
+ kErrorPrefix, key, value.Format());
+ }
+ batch_size_ = static_cast<int>(batch_size);
+ return status::Ok();
}
return Base::SetOptionImpl(key, value);
}
private:
+ friend class SqliteConnection;
std::string uri_{kDefaultUri};
sqlite3* conn_ = nullptr;
+ int batch_size_ = kDefaultBatchSize;
};
class SqliteConnection : public driver::Connection<SqliteConnection> {
@@ -672,6 +701,7 @@ class SqliteConnection : public
driver::Connection<SqliteConnection> {
Status InitImpl(void* parent) {
auto& db = *reinterpret_cast<SqliteDatabase*>(parent);
UNWRAP_RESULT(conn_, db.OpenConnection());
+ batch_size_ = db.batch_size_;
return status::Ok();
}
@@ -693,6 +723,13 @@ class SqliteConnection : public
driver::Connection<SqliteConnection> {
return SqliteQuery::Execute(conn_, "BEGIN");
}
+ Result<driver::Option> GetOption(std::string_view key) override {
+ if (key == kStatementOptionBatchRows) {
+ return driver::Option(static_cast<int64_t>(batch_size_));
+ }
+ return Base::GetOption(key);
+ }
+
Status SetOptionImpl(std::string_view key, driver::Option value) {
if (key == kConnectionOptionEnableLoadExtension) {
if (!conn_ || lifecycle_state_ != driver::LifecycleState::kInitialized) {
@@ -761,6 +798,8 @@ class SqliteConnection : public
driver::Connection<SqliteConnection> {
}
private:
+ friend class SqliteStatement;
+
Status CheckOpen() const {
if (!conn_) {
return status::InvalidState("connection is not open");
@@ -772,6 +811,7 @@ class SqliteConnection : public
driver::Connection<SqliteConnection> {
// Temporarily hold the extension path (since the path and entrypoint need
// to be set separately)
std::string extension_path_;
+ int batch_size_ = kDefaultBatchSize;
};
class SqliteStatement : public driver::Statement<SqliteStatement> {
@@ -1111,7 +1151,9 @@ class SqliteStatement : public
driver::Statement<SqliteStatement> {
}
Status InitImpl(void* parent) {
- conn_ = reinterpret_cast<SqliteConnection*>(parent)->conn();
+ auto& conn = *reinterpret_cast<SqliteConnection*>(parent);
+ conn_ = conn.conn();
+ batch_size_ = conn.batch_size_;
return Statement::InitImpl(parent);
}
@@ -1151,7 +1193,16 @@ class SqliteStatement : public
driver::Statement<SqliteStatement> {
return Statement::ReleaseImpl();
}
- Status SetOptionImpl(std::string_view key, driver::Option value) {
+ Result<driver::Option> GetOption(std::string_view key) override {
+ if (key == kStatementOptionBatchRows) {
+ return driver::Option(static_cast<int64_t>(batch_size_));
+ } else if (key == kStatementOptionBindByName) {
+ return driver::Option(bind_by_name_ ? "true" : "false");
+ }
+ return Base::GetOption(key);
+ }
+
+ Status SetOptionImpl(std::string_view key, driver::Option value) override {
if (key == kStatementOptionBatchRows) {
int64_t batch_size;
UNWRAP_RESULT(batch_size, value.AsInt());
@@ -1170,7 +1221,7 @@ class SqliteStatement : public
driver::Statement<SqliteStatement> {
return Base::SetOptionImpl(key, std::move(value));
}
- int batch_size_ = 1024;
+ int batch_size_ = kDefaultBatchSize;
bool bind_by_name_ = false;
AdbcSqliteBinder binder_;
sqlite3* conn_ = nullptr;
diff --git a/c/driver_manager/adbc_driver_manager.cc
b/c/driver_manager/adbc_driver_manager.cc
index 2923c35d7..42e904205 100644
--- a/c/driver_manager/adbc_driver_manager.cc
+++ b/c/driver_manager/adbc_driver_manager.cc
@@ -471,7 +471,7 @@ AdbcStatusCode InternalInitializeProfile(TempDatabase* args,
// use try_emplace so we only add the option if there isn't
// already an option with the same name
std::string processed;
- CHECK_STATUS(ProcessProfileValue(values[i], processed, error));
+ CHECK_STATUS(ProcessProfileValue(keys[i], values[i], processed, error));
args->options.try_emplace(keys[i], processed);
}
diff --git a/c/driver_manager/adbc_driver_manager_internal.h
b/c/driver_manager/adbc_driver_manager_internal.h
index 7524b3117..dd0aef128 100644
--- a/c/driver_manager/adbc_driver_manager_internal.h
+++ b/c/driver_manager/adbc_driver_manager_internal.h
@@ -189,8 +189,8 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const
std::wstring& driver_name
#endif
// Profile loading
-AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out,
- struct AdbcError* error);
+AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
+ std::string& out, struct AdbcError* error);
// Initialization
/// Temporary state while the database is being configured.
diff --git a/c/driver_manager/adbc_driver_manager_profiles.cc
b/c/driver_manager/adbc_driver_manager_profiles.cc
index 3e7a8518b..ff390f5fd 100644
--- a/c/driver_manager/adbc_driver_manager_profiles.cc
+++ b/c/driver_manager/adbc_driver_manager_profiles.cc
@@ -20,10 +20,7 @@
#include <windows.h> // Must come first
#endif // defined(_WIN32)
-#include <toml++/toml.hpp>
#include "adbc_driver_manager_internal.h"
-#include "arrow-adbc/adbc.h"
-#include "arrow-adbc/adbc_driver_manager.h"
#include <filesystem>
#include <regex>
@@ -32,6 +29,11 @@
#include <utility>
#include <vector>
+#include <toml++/toml.hpp>
+
+#include "arrow-adbc/adbc.h"
+#include "arrow-adbc/adbc_driver_manager.h"
+
using namespace std::string_literals; // NOLINT [build/namespaces]
namespace {
@@ -42,82 +44,6 @@ static const wchar_t* kAdbcProfilePath =
L"ADBC_PROFILE_PATH";
static const char* kAdbcProfilePath = "ADBC_PROFILE_PATH";
#endif // _WIN32
-static AdbcStatusCode ProcessProfileValueInternal(std::string_view value,
- std::string& out,
- struct AdbcError* error) {
- if (value.empty()) {
- SetError(error, "Profile value is null");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
- auto end_of_last_match = value.begin();
- auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
- auto end = decltype(begin){};
- std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
-
- out.resize(0);
- for (auto itr = begin; itr != end; ++itr) {
- auto match = *itr;
- auto pos_match = match.position();
- auto diff = pos_match - pos_last_match;
- auto start_match = end_of_last_match;
- std::advance(start_match, diff);
- out.append(end_of_last_match, start_match);
-
- const auto content = match[1].str();
- if (content.rfind("env_var(", 0) != 0) {
- SetError(error, "Unsupported interpolation type in profile value: " +
content);
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- if (content[content.size() - 1] != ')') {
- SetError(error, "Malformed env_var() profile value: missing closing
parenthesis");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- const auto env_var_name = content.substr(8, content.size() - 9);
- if (env_var_name.empty()) {
- SetError(error,
- "Malformed env_var() profile value: missing environment
variable name");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
-#ifdef _WIN32
- auto local_env_var = Utf8Decode(std::string(env_var_name));
- DWORD required_size = GetEnvironmentVariableW(local_env_var.c_str(), NULL,
0);
- if (required_size == 0) {
- out = "";
- return ADBC_STATUS_OK;
- }
-
- std::wstring wvalue;
- wvalue.resize(required_size);
- DWORD actual_size =
- GetEnvironmentVariableW(local_env_var.c_str(), wvalue.data(),
required_size);
- // remove null terminator
- wvalue.resize(actual_size);
- const auto env_var_value = Utf8Encode(wvalue);
-#else
- const char* env_value = std::getenv(env_var_name.c_str());
- if (!env_value) {
- out = "";
- return ADBC_STATUS_OK;
- }
- const auto env_var_value = std::string(env_value);
-#endif
- out.append(env_var_value);
-
- auto length_match = match.length();
- pos_last_match = pos_match + length_match;
- end_of_last_match = start_match;
- std::advance(end_of_last_match, length_match);
- }
-
- out.append(end_of_last_match, value.end());
- return ADBC_STATUS_OK;
-}
-
} // namespace
// FilesystemProfile needs external linkage for use in internal header
@@ -278,9 +204,80 @@ struct ProfileVisitor {
};
// Public implementations (non-static for use across translation units)
-AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out,
- struct AdbcError* error) {
- return ProcessProfileValueInternal(value, out, error);
+AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
+ std::string& out, struct AdbcError* error) {
+ if (value.empty()) {
+ out = "";
+ return ADBC_STATUS_OK;
+ }
+
+ static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
+ auto end_of_last_match = value.begin();
+ auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
+ auto end = decltype(begin){};
+ std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
+
+ out.resize(0);
+ for (auto itr = begin; itr != end; ++itr) {
+ auto match = *itr;
+ auto pos_match = match.position();
+ auto diff = pos_match - pos_last_match;
+ auto start_match = end_of_last_match;
+ std::advance(start_match, diff);
+ out.append(end_of_last_match, start_match);
+
+ const auto content = match[1].str();
+ if (content.rfind("env_var(", 0) != 0) {
+ std::string message = "In profile: unsupported interpolation type in key
`" +
+ std::string(key) + "`: `" + content + "`";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ if (content[content.size() - 1] != ')') {
+ std::string message = "In profile: malformed env_var() in key `" +
+ std::string(key) + "`: missing closing
parenthesis";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ const auto env_var_name = content.substr(8, content.size() - 9);
+ if (env_var_name.empty()) {
+ std::string message = "In profile: malformed env_var() in key `" +
+ std::string(key) + "`: missing environment
variable name";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ std::string env_var_value;
+#ifdef _WIN32
+ auto local_env_var = Utf8Decode(std::string(env_var_name));
+ DWORD required_size = GetEnvironmentVariableW(local_env_var.c_str(), NULL,
0);
+ if (required_size != 0) {
+ std::wstring wvalue;
+ wvalue.resize(required_size);
+ DWORD actual_size =
+ GetEnvironmentVariableW(local_env_var.c_str(), wvalue.data(),
required_size);
+ // remove null terminator
+ wvalue.resize(actual_size);
+ env_var_value = Utf8Encode(wvalue);
+ }
+#else
+ const char* env_value = std::getenv(env_var_name.c_str());
+ if (env_value) {
+ env_var_value = std::string(env_value);
+ }
+#endif
+ out.append(env_var_value);
+
+ auto length_match = match.length();
+ pos_last_match = pos_match + length_match;
+ end_of_last_match = start_match;
+ std::advance(end_of_last_match, length_match);
+ }
+
+ out.append(end_of_last_match, value.end());
+ return ADBC_STATUS_OK;
}
AdbcStatusCode LoadProfileFile(const std::filesystem::path& profile_path,
@@ -453,8 +450,10 @@ AdbcStatusCode AdbcProfileProviderFilesystem(const char*
profile_name,
extra_debug_info.end());
if (intermediate_error.error.message) {
std::string error_message = intermediate_error.error.message;
+ // Remove [Driver Manager] prefix so it doesn't get repeated
+ error_message = error_message.substr(17);
AddSearchPathsToError(search_paths, SearchPathType::kProfile,
error_message);
- SetError(error, std::move(error_message));
+ SetError(error, error_message);
}
return status;
}
@@ -463,7 +462,9 @@ AdbcStatusCode AdbcProfileProviderFilesystem(const char*
profile_name,
message += full_path.string();
message += " but: ";
if (intermediate_error.error.message) {
- message += intermediate_error.error.message;
+ std::string m = intermediate_error.error.message;
+ // Remove [Driver Manager] prefix so it doesn't get repeated
+ message += m.substr(17);
} else {
message += "could not load the profile";
}
diff --git a/c/driver_manager/adbc_driver_manager_test.cc
b/c/driver_manager/adbc_driver_manager_test.cc
index fcd3a8d28..f917d9543 100644
--- a/c/driver_manager/adbc_driver_manager_test.cc
+++ b/c/driver_manager/adbc_driver_manager_test.cc
@@ -1811,9 +1811,10 @@ TEST_F(ConnectionProfiles, UseEnvVarMalformed) {
IsOkStatus(&error));
ASSERT_THAT(AdbcDatabaseInit(&database.value, &error),
IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error));
- ASSERT_THAT(error.message,
- ::testing::HasSubstr(
- "Malformed env_var() profile value: missing closing
parenthesis"));
+ ASSERT_THAT(
+ error.message,
+ ::testing::HasSubstr(
+ "In profile: malformed env_var() in key `foo`: missing closing
parenthesis"));
UnsetConfigPath();
}
@@ -1840,10 +1841,9 @@ TEST_F(ConnectionProfiles, UseEnvVarMissingArg) {
IsOkStatus(&error));
ASSERT_THAT(AdbcDatabaseInit(&database.value, &error),
IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error));
- ASSERT_THAT(
- error.message,
- ::testing::HasSubstr(
- "Malformed env_var() profile value: missing environment variable
name"));
+ ASSERT_THAT(error.message,
+ ::testing::HasSubstr("In profile: malformed env_var() in key
`foo`: "
+ "missing environment variable name"));
UnsetConfigPath();
}
diff --git a/ci/scripts/python_venv_test.sh b/ci/scripts/python_venv_test.sh
index ac9507c05..bbee58ba8 100755
--- a/ci/scripts/python_venv_test.sh
+++ b/ci/scripts/python_venv_test.sh
@@ -36,6 +36,14 @@ main() {
name = "SQLite"
[Driver]
shared = "${sqlite_driver}"
+EOF
+
+ mkdir -p "${scratch}/.venv/etc/adbc/profiles/sqlite/"
+ cat >"${scratch}/.venv/etc/adbc/profiles/sqlite/dev.toml" <<EOF
+version = 1
+driver = "sqlite"
+[options]
+uri = "file:///tmp/test.db"
EOF
cat >"${scratch}/test.py" <<EOF
@@ -65,6 +73,20 @@ EOF
"${scratch}"/.venv/bin/python "${scratch}/test2.py"
echo "PASSED: failed manifest contains the proper path in the exception"
+
+ # TODO(https://github.com/apache/arrow-adbc/issues/4087)
+# cat >"${scratch}/test3.py" <<EOF
+# import adbc_driver_manager.dbapi
+
+# with adbc_driver_manager.dbapi.connect(profile="sqlite/dev") as con:
+# with con.cursor() as cur:
+# cur.execute("SELECT 1")
+# assert cur.fetchall() == [(1,)]
+# EOF
+
+# "${scratch}"/.venv/bin/python "${scratch}/test3.py"
+# test -f /tmp/test.db
+# echo "PASSED: find profile"
}
main "$@"
diff --git a/go/adbc/drivermgr/adbc_driver_manager.cc
b/go/adbc/drivermgr/adbc_driver_manager.cc
index 2923c35d7..42e904205 100644
--- a/go/adbc/drivermgr/adbc_driver_manager.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager.cc
@@ -471,7 +471,7 @@ AdbcStatusCode InternalInitializeProfile(TempDatabase* args,
// use try_emplace so we only add the option if there isn't
// already an option with the same name
std::string processed;
- CHECK_STATUS(ProcessProfileValue(values[i], processed, error));
+ CHECK_STATUS(ProcessProfileValue(keys[i], values[i], processed, error));
args->options.try_emplace(keys[i], processed);
}
diff --git a/go/adbc/drivermgr/adbc_driver_manager_internal.h
b/go/adbc/drivermgr/adbc_driver_manager_internal.h
index 7524b3117..dd0aef128 100644
--- a/go/adbc/drivermgr/adbc_driver_manager_internal.h
+++ b/go/adbc/drivermgr/adbc_driver_manager_internal.h
@@ -189,8 +189,8 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const
std::wstring& driver_name
#endif
// Profile loading
-AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out,
- struct AdbcError* error);
+AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
+ std::string& out, struct AdbcError* error);
// Initialization
/// Temporary state while the database is being configured.
diff --git a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
index 3e7a8518b..ff390f5fd 100644
--- a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
@@ -20,10 +20,7 @@
#include <windows.h> // Must come first
#endif // defined(_WIN32)
-#include <toml++/toml.hpp>
#include "adbc_driver_manager_internal.h"
-#include "arrow-adbc/adbc.h"
-#include "arrow-adbc/adbc_driver_manager.h"
#include <filesystem>
#include <regex>
@@ -32,6 +29,11 @@
#include <utility>
#include <vector>
+#include <toml++/toml.hpp>
+
+#include "arrow-adbc/adbc.h"
+#include "arrow-adbc/adbc_driver_manager.h"
+
using namespace std::string_literals; // NOLINT [build/namespaces]
namespace {
@@ -42,82 +44,6 @@ static const wchar_t* kAdbcProfilePath =
L"ADBC_PROFILE_PATH";
static const char* kAdbcProfilePath = "ADBC_PROFILE_PATH";
#endif // _WIN32
-static AdbcStatusCode ProcessProfileValueInternal(std::string_view value,
- std::string& out,
- struct AdbcError* error) {
- if (value.empty()) {
- SetError(error, "Profile value is null");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
- auto end_of_last_match = value.begin();
- auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
- auto end = decltype(begin){};
- std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
-
- out.resize(0);
- for (auto itr = begin; itr != end; ++itr) {
- auto match = *itr;
- auto pos_match = match.position();
- auto diff = pos_match - pos_last_match;
- auto start_match = end_of_last_match;
- std::advance(start_match, diff);
- out.append(end_of_last_match, start_match);
-
- const auto content = match[1].str();
- if (content.rfind("env_var(", 0) != 0) {
- SetError(error, "Unsupported interpolation type in profile value: " +
content);
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- if (content[content.size() - 1] != ')') {
- SetError(error, "Malformed env_var() profile value: missing closing
parenthesis");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
- const auto env_var_name = content.substr(8, content.size() - 9);
- if (env_var_name.empty()) {
- SetError(error,
- "Malformed env_var() profile value: missing environment
variable name");
- return ADBC_STATUS_INVALID_ARGUMENT;
- }
-
-#ifdef _WIN32
- auto local_env_var = Utf8Decode(std::string(env_var_name));
- DWORD required_size = GetEnvironmentVariableW(local_env_var.c_str(), NULL,
0);
- if (required_size == 0) {
- out = "";
- return ADBC_STATUS_OK;
- }
-
- std::wstring wvalue;
- wvalue.resize(required_size);
- DWORD actual_size =
- GetEnvironmentVariableW(local_env_var.c_str(), wvalue.data(),
required_size);
- // remove null terminator
- wvalue.resize(actual_size);
- const auto env_var_value = Utf8Encode(wvalue);
-#else
- const char* env_value = std::getenv(env_var_name.c_str());
- if (!env_value) {
- out = "";
- return ADBC_STATUS_OK;
- }
- const auto env_var_value = std::string(env_value);
-#endif
- out.append(env_var_value);
-
- auto length_match = match.length();
- pos_last_match = pos_match + length_match;
- end_of_last_match = start_match;
- std::advance(end_of_last_match, length_match);
- }
-
- out.append(end_of_last_match, value.end());
- return ADBC_STATUS_OK;
-}
-
} // namespace
// FilesystemProfile needs external linkage for use in internal header
@@ -278,9 +204,80 @@ struct ProfileVisitor {
};
// Public implementations (non-static for use across translation units)
-AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out,
- struct AdbcError* error) {
- return ProcessProfileValueInternal(value, out, error);
+AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
+ std::string& out, struct AdbcError* error) {
+ if (value.empty()) {
+ out = "";
+ return ADBC_STATUS_OK;
+ }
+
+ static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
+ auto end_of_last_match = value.begin();
+ auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
+ auto end = decltype(begin){};
+ std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
+
+ out.resize(0);
+ for (auto itr = begin; itr != end; ++itr) {
+ auto match = *itr;
+ auto pos_match = match.position();
+ auto diff = pos_match - pos_last_match;
+ auto start_match = end_of_last_match;
+ std::advance(start_match, diff);
+ out.append(end_of_last_match, start_match);
+
+ const auto content = match[1].str();
+ if (content.rfind("env_var(", 0) != 0) {
+ std::string message = "In profile: unsupported interpolation type in key
`" +
+ std::string(key) + "`: `" + content + "`";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ if (content[content.size() - 1] != ')') {
+ std::string message = "In profile: malformed env_var() in key `" +
+ std::string(key) + "`: missing closing
parenthesis";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ const auto env_var_name = content.substr(8, content.size() - 9);
+ if (env_var_name.empty()) {
+ std::string message = "In profile: malformed env_var() in key `" +
+ std::string(key) + "`: missing environment
variable name";
+ SetError(error, message);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ std::string env_var_value;
+#ifdef _WIN32
+ auto local_env_var = Utf8Decode(std::string(env_var_name));
+ DWORD required_size = GetEnvironmentVariableW(local_env_var.c_str(), NULL,
0);
+ if (required_size != 0) {
+ std::wstring wvalue;
+ wvalue.resize(required_size);
+ DWORD actual_size =
+ GetEnvironmentVariableW(local_env_var.c_str(), wvalue.data(),
required_size);
+ // remove null terminator
+ wvalue.resize(actual_size);
+ env_var_value = Utf8Encode(wvalue);
+ }
+#else
+ const char* env_value = std::getenv(env_var_name.c_str());
+ if (env_value) {
+ env_var_value = std::string(env_value);
+ }
+#endif
+ out.append(env_var_value);
+
+ auto length_match = match.length();
+ pos_last_match = pos_match + length_match;
+ end_of_last_match = start_match;
+ std::advance(end_of_last_match, length_match);
+ }
+
+ out.append(end_of_last_match, value.end());
+ return ADBC_STATUS_OK;
}
AdbcStatusCode LoadProfileFile(const std::filesystem::path& profile_path,
@@ -453,8 +450,10 @@ AdbcStatusCode AdbcProfileProviderFilesystem(const char*
profile_name,
extra_debug_info.end());
if (intermediate_error.error.message) {
std::string error_message = intermediate_error.error.message;
+ // Remove [Driver Manager] prefix so it doesn't get repeated
+ error_message = error_message.substr(17);
AddSearchPathsToError(search_paths, SearchPathType::kProfile,
error_message);
- SetError(error, std::move(error_message));
+ SetError(error, error_message);
}
return status;
}
@@ -463,7 +462,9 @@ AdbcStatusCode AdbcProfileProviderFilesystem(const char*
profile_name,
message += full_path.string();
message += " but: ";
if (intermediate_error.error.message) {
- message += intermediate_error.error.message;
+ std::string m = intermediate_error.error.message;
+ // Remove [Driver Manager] prefix so it doesn't get repeated
+ message += m.substr(17);
} else {
message += "could not load the profile";
}
diff --git a/python/adbc_driver_manager/pyproject.toml
b/python/adbc_driver_manager/pyproject.toml
index bad5ad046..7d1aa3092 100644
--- a/python/adbc_driver_manager/pyproject.toml
+++ b/python/adbc_driver_manager/pyproject.toml
@@ -28,7 +28,7 @@ dependencies = ["typing-extensions"]
[project.optional-dependencies]
dbapi = ["pandas", "pyarrow>=14.0.1"]
-test = ["duckdb", "pandas", "polars", "pyarrow>=14.0.1", "pytest"]
+test = ["duckdb", "pandas", "polars", "pyarrow>=14.0.1", "pytest>=9"]
[project.urls]
homepage = "https://arrow.apache.org/adbc/"
@@ -48,6 +48,7 @@ markers = [
"panicdummy: tests that require the testing-only panicdummy driver",
"pyarrowless: tests of functionality when PyArrow is NOT installed",
"sqlite: tests that require the SQLite driver",
+ "system: tests that touch user data directories",
]
xfail_strict = true
diff --git a/python/adbc_driver_manager/tests/conftest.py
b/python/adbc_driver_manager/tests/conftest.py
index e08ad3134..cbf35d2a0 100644
--- a/python/adbc_driver_manager/tests/conftest.py
+++ b/python/adbc_driver_manager/tests/conftest.py
@@ -15,6 +15,8 @@
# specific language governing permissions and limitations
# under the License.
+import os
+import pathlib
import typing
import pytest
@@ -22,7 +24,31 @@ import pytest
from adbc_driver_manager import dbapi
+def pytest_addoption(parser):
+ parser.addoption(
+ "--run-system",
+ action="store_true",
+ default=False,
+ help="Run tests that may modify global filesystem paths",
+ )
+
+
+def pytest_collection_modifyitems(config, items):
+ if not config.getoption("--run-system"):
+ mark = pytest.mark.skip(reason="Needs --run-system")
+ for item in items:
+ if "system" in item.keywords:
+ item.add_marker(mark)
+
+
@pytest.fixture
def sqlite() -> typing.Generator[dbapi.Connection, None, None]:
with dbapi.connect(driver="adbc_driver_sqlite") as conn:
yield conn
+
+
[email protected](scope="session")
+def conda_prefix() -> pathlib.Path:
+ if "CONDA_PREFIX" not in os.environ:
+ pytest.skip("only runs in Conda environment")
+ return pathlib.Path(os.environ["CONDA_PREFIX"])
diff --git a/python/adbc_driver_manager/tests/test_profile.py
b/python/adbc_driver_manager/tests/test_profile.py
new file mode 100644
index 000000000..2aea0718a
--- /dev/null
+++ b/python/adbc_driver_manager/tests/test_profile.py
@@ -0,0 +1,543 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import pathlib
+import platform
+import re
+import typing
+import uuid
+
+import pytest
+
+import adbc_driver_manager.dbapi as dbapi
+
+pytestmark = [pytest.mark.sqlite]
+
+
[email protected](scope="module", autouse=True)
+def profile_dir(tmp_path_factory) -> typing.Generator[pathlib.Path, None,
None]:
+ path = tmp_path_factory.mktemp("profile_dir")
+ with pytest.MonkeyPatch().context() as mp:
+ mp.setenv("ADBC_PROFILE_PATH", str(path))
+ yield path
+
+
[email protected](scope="module")
+def sqlitedev(profile_dir) -> str:
+ with (profile_dir / "sqlitedev.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+
+[options]
+""")
+ return "sqlitedev"
+
+
+def test_profile_option(sqlitedev) -> None:
+ # Test loading via "profile" option
+ with dbapi.connect(profile=sqlitedev) as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
+def test_option_env_var(subtests, tmp_path, monkeypatch) -> None:
+ # Test a profile that uses env var substitution for option values
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ # ruff: disable[E501]
+ # fmt: off
+ cases = [
+ # env vars, raw string, rendered value
+
+ # Make sure manager doesn't misinterpret empty string as null (there
+ # is no null in TOML)
+ ({}, "", ""),
+ # No actual substitution
+ ({}, "{{ env_var(NONEXISTENT)", "{{ env_var(NONEXISTENT)"),
+ ({}, "{{ env_var(NONEXISTENT) }", "{{ env_var(NONEXISTENT) }"),
+ ({}, "{ env_var(NONEXISTENT) }", "{ env_var(NONEXISTENT) }"),
+ # Env var does not exist
+ ({}, "{{ env_var(NONEXISTENT) }}", ""),
+ ({}, "{{ env_var(NONEXISTENT) }}bar", "bar"),
+ ({}, "foo{{ env_var(NONEXISTENT) }}", "foo"),
+ ({}, "foo{{ env_var(NONEXISTENT) }}bar", "foobar"),
+ # Multiple env vars do not exist
+ ({}, "foo{{ env_var(NONEXISTENT) }}bar{{ env_var(NONEXISTENT2) }}baz",
"foobarbaz"),
+ # Multiple env vars do not exist in different positions
+ ({}, "{{ env_var(NONEXISTENT) }}foobarbaz{{ env_var(NONEXISTENT2) }}",
"foobarbaz"),
+
+ # Check whitespace sensitivity
+ ({"TESTVALUE": "a"}, "{{env_var(TESTVALUE)}},{{ env_var(TESTVALUE)
}},{{env_var(TESTVALUE) }},{{ env_var(TESTVALUE)}},{{ env_var(TESTVALUE)
}}", "a,a,a,a,a"),
+
+ # Multiple vars
+ ({"TESTVALUE": "a", "TESTVALUE2": "b"},
"{{env_var(TESTVALUE)}}{{env_var(TESTVALUE2)}}", "ab"),
+ ({"TESTVALUE": "a", "TESTVALUE2": "b"},
"foo{{env_var(TESTVALUE)}}bar{{env_var(TESTVALUE2)}}baz", "fooabarbbaz"),
+ ]
+ # fmt: on
+ # ruff: enable[E501]
+
+ for i, (env_vars, raw_value, rendered_value) in enumerate(cases):
+ with subtests.test(i=i, msg=raw_value):
+ with (tmp_path / "subst.toml").open("w") as sink:
+ sink.write(f"""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+adbc.foo.bar = "{raw_value}"
+""")
+
+ with monkeypatch.context() as mp:
+ for k, v in env_vars.items():
+ mp.setenv(k, v)
+
+ expected = re.escape(
+ f"Unknown database option adbc.foo.bar='{rendered_value}'"
+ )
+ with pytest.raises(dbapi.Error, match=expected):
+ with dbapi.connect("profile://subst"):
+ pass
+
+
+def test_option_env_var_multiple(tmp_path, monkeypatch) -> None:
+ # Test that we can set multiple options via env var substitution
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+ with (tmp_path / "subst.toml").open("w") as sink:
+ # On Windows, ensure we get file:///c:/ not file://c:/
+ windows = "/" if platform.system() == "Windows" else ""
+ rest = "{{ env_var(TEST_DIR) }}/{{ env_var(TEST_NUM) }}"
+ batch = "{{ env_var(BATCH_SIZE) }}"
+ sink.write(f"""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+uri = "file://{windows}{rest}.db"
+adbc.sqlite.query.batch_rows = "{batch}"
+""")
+
+ monkeypatch.setenv("TEST_DIR", str(tmp_path.as_posix()))
+ monkeypatch.setenv("TEST_NUM", "42")
+ monkeypatch.setenv("BATCH_SIZE", "1")
+
+ key = "adbc.sqlite.query.batch_rows"
+ with dbapi.connect("profile://subst") as conn:
+ assert conn.adbc_database.get_option_int(key) == 1
+ assert conn.adbc_connection.get_option_int(key) == 1
+
+ assert conn.adbc_database.get_option("uri") == (tmp_path /
"42.db").as_uri()
+
+ with conn.cursor() as cursor:
+ assert cursor.adbc_statement.get_option_int(key) == 1
+ cursor.execute("CREATE TABLE foo (id INTEGER)")
+ cursor.execute("INSERT INTO foo VALUES (1), (2)")
+ cursor.execute("SELECT * FROM foo")
+ with cursor.fetch_record_batch() as reader:
+ assert len(next(reader)) == 1
+ assert len(next(reader)) == 1
+
+
+def test_option_env_var_invalid(subtests, tmp_path, monkeypatch) -> None:
+ # Test that various invalid syntaxes fail
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ for contents, error in [
+ ("{{ }}", "unsupported interpolation type in key `uri`: ``"),
+ ("{{ bar_baz }}", "unsupported interpolation type in key `uri`:
`bar_baz`"),
+ ("{{ rand() }}", "unsupported interpolation type in key `uri`:
`rand()`"),
+ (
+ "{{ env_var(TEST_DIR }}",
+ "malformed env_var() in key `uri`: missing closing parenthesis",
+ ),
+ (
+ "{{ env_var() }}",
+ "malformed env_var() in key `uri`: missing environment variable
name",
+ ),
+ ]:
+ with (tmp_path / "subst.toml").open("w") as sink:
+ sink.write(f"""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+uri = "{contents}"
+ """)
+
+ with subtests.test(msg=contents):
+ with pytest.raises(
+ dbapi.ProgrammingError, match=re.escape(f"In profile: {error}")
+ ):
+ with dbapi.connect("profile://subst"):
+ pass
+
+
[email protected](reason="https://github.com/apache/arrow-adbc/issues/4086")
+def test_option_override(tmp_path, monkeypatch) -> None:
+ # Test that the driver is optional
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ with (tmp_path / "dev.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+adbc.sqlite.query.batch_rows = 7
+""")
+
+ key = "adbc.sqlite.query.batch_rows"
+ with dbapi.connect("profile://dev") as conn:
+ assert conn.adbc_database.get_option_int(key) == 7
+
+ with dbapi.connect("profile://dev", db_kwargs={key: "42"}) as conn:
+ assert conn.adbc_database.get_option_int(key) == 42
+
+
+def test_uri(sqlitedev) -> None:
+ # Test loading via profile:// URI
+ with dbapi.connect(f"profile://{sqlitedev}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
+def test_driver_optional(subtests, tmp_path, monkeypatch) -> None:
+ # Test that the driver is optional
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ with (tmp_path / "nodriver.toml").open("w") as sink:
+ sink.write("""
+version = 1
+[options]
+""")
+
+ with subtests.test(msg="missing driver"):
+ with pytest.raises(dbapi.ProgrammingError, match="Must set 'driver'
option"):
+ with dbapi.connect("profile://nodriver"):
+ pass
+
+ # TODO(https://github.com/apache/arrow-adbc/issues/4085): do we want to
allow this?
+ # with subtests.test(msg="uri"):
+ # with dbapi.connect("adbc_driver_sqlite", uri="profile://nodriver")
as conn:
+ # with conn.cursor() as cursor:
+ # cursor.execute("SELECT sqlite_version()")
+ # assert cursor.fetchone() is not None
+
+ with subtests.test(msg="profile"):
+ with dbapi.connect("adbc_driver_sqlite", profile="nodriver") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
+# TODO(https://github.com/apache/arrow-adbc/issues/4085): do we want to allow
this?
+
+# @pytest.mark.xfail
+# def test_driver_override(subtests, tmp_path, monkeypatch) -> None:
+# # Test that the driver can be overridden by an option
+# monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+# with subtests.test(msg="with override (URI)"):
+# with dbapi.connect("adbc_driver_sqlite", "profile://nonexistent") as
conn:
+# with conn.cursor() as cursor:
+# cursor.execute("SELECT sqlite_version()")
+# assert cursor.fetchone() is not None
+
+# with subtests.test(msg="with override (profile)"):
+# with dbapi.connect("adbc_driver_sqlite", profile="nonexistent") as
conn:
+# with conn.cursor() as cursor:
+# cursor.execute("SELECT sqlite_version()")
+# assert cursor.fetchone() is not None
+
+
+def test_driver_invalid(subtests, tmp_path, monkeypatch) -> None:
+ # Test invalid values for the driver
+ # TODO(lidavidm): give a more specific error
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ with (tmp_path / "nodriver.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = 2
+[options]
+""")
+
+ with subtests.test(msg="numeric driver"):
+ with pytest.raises(dbapi.ProgrammingError, match="Must set 'driver'
option"):
+ with dbapi.connect("profile://nodriver"):
+ pass
+
+ with (tmp_path / "nodriver.toml").open("w") as sink:
+ sink.write("""
+version = 1
+[driver]
+foo = "bar"
+[options]
+""")
+
+ with subtests.test(msg="table driver"):
+ with pytest.raises(dbapi.ProgrammingError, match="Must set 'driver'
option"):
+ with dbapi.connect("profile://nodriver"):
+ pass
+
+
+def test_version_invalid(tmp_path, monkeypatch) -> None:
+ # Test that invalid versions are rejected
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ with (tmp_path / "badversion.toml").open("w") as sink:
+ sink.write("""
+driver = "adbc_driver_sqlite"
+[options]
+""")
+ with pytest.raises(
+ dbapi.ProgrammingError, match="Profile version is not an integer"
+ ):
+ with dbapi.connect("profile://badversion"):
+ pass
+
+ with (tmp_path / "badversion.toml").open("w") as sink:
+ sink.write("""
+driver = "adbc_driver_sqlite"
+version = "1"
+[options]
+""")
+ with pytest.raises(
+ dbapi.ProgrammingError, match="Profile version is not an integer"
+ ):
+ with dbapi.connect("profile://badversion"):
+ pass
+
+ with (tmp_path / "badversion.toml").open("w") as sink:
+ sink.write("""
+driver = "adbc_driver_sqlite"
+version = 9001
+[options]
+""")
+ with pytest.raises(
+ dbapi.ProgrammingError, match="Profile version '9001' is not supported"
+ ):
+ with dbapi.connect("profile://badversion"):
+ pass
+
+
+def test_reject_malformed(tmp_path, monkeypatch) -> None:
+ # Test that invalid profiles are rejected
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ with (tmp_path / "nodriver.toml").open("w") as sink:
+ sink.write("""
+version = 1
+[options]
+""")
+ with pytest.raises(dbapi.ProgrammingError, match="Must set 'driver'
option"):
+ with dbapi.connect("profile://nodriver"):
+ pass
+
+ with (tmp_path / "nooptions.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+""")
+ with pytest.raises(dbapi.ProgrammingError, match="Profile options is not a
table"):
+ with dbapi.connect("profile://nooptions"):
+ pass
+
+ with (tmp_path / "unknownkeys.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+[foobar]
+""")
+ # Unknown keys is OK, though
+ with dbapi.connect("profile://unknownkeys"):
+ pass
+
+
+def test_driver_options(tmp_path, monkeypatch) -> None:
+ # Test that options are properly applied
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+
+ # On Windows, ensure we get file:///c:/ not file://c:/
+ windows = "/" if platform.system() == "Windows" else ""
+ 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
+driver = "adbc_driver_sqlite"
+[options]
+uri = "{uri}"
+""")
+ with dbapi.connect("profile://sqlitetest") as conn:
+ assert conn.adbc_database.get_option("uri") == uri
+
+
+def test_load_driver_manifest(tmp_path, monkeypatch) -> None:
+ # Test a profile that references a manifest
+ manifest_path = tmp_path / "manifest"
+ profile_path = tmp_path / "profile"
+ manifest_path.mkdir()
+ profile_path.mkdir()
+ monkeypatch.setenv("ADBC_DRIVER_PATH", str(manifest_path))
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(profile_path))
+
+ with (manifest_path / "sqlitemanifest.toml").open("w") as sink:
+ sink.write("""
+manifest_version = 1
+[Driver]
+shared = "adbc_driver_sqlite"
+""")
+
+ with (profile_path / "proddata.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "sqlitemanifest"
+[options]
+""")
+ with dbapi.connect("profile://proddata") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
+def test_subdir(monkeypatch, tmp_path) -> None:
+ # Test that we can search in subdirectories
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+ subdir = tmp_path / "sqlite" / "prod"
+ subdir.mkdir(parents=True)
+
+ with (subdir / "sqlitetest.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+ with dbapi.connect("profile://sqlite/prod/sqlitetest") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
+def test_absolute(monkeypatch, tmp_path) -> None:
+ # Test that we can load profiles by absolute path
+ monkeypatch.setenv("ADBC_PROFILE_PATH", str(tmp_path))
+ subdir = tmp_path / "sqlite" / "staging"
+ subdir.mkdir(parents=True)
+
+ with (subdir / "sqlitetest.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+
+ path = (subdir / "sqlitetest.toml").absolute().as_posix()
+ with dbapi.connect(f"profile://{path}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
[email protected]
+def test_user_path() -> None:
+ if platform.system() == "Darwin":
+ path = pathlib.Path.home() / "Library/Application
Support/ADBC/Profiles"
+ elif platform.system() == "Linux":
+ path = pathlib.Path.home() / ".config/adbc/profiles"
+ elif platform.system() == "Windows":
+ path = pathlib.Path(os.environ["LOCALAPPDATA"]) / "ADBC/Profiles"
+ else:
+ pytest.skip(f"Unsupported platform {platform.system()}")
+
+ subdir = str(uuid.uuid4())
+ profile = str(uuid.uuid4())
+ subpath = path / subdir
+ path.mkdir(exist_ok=True, parents=True)
+ subpath.mkdir(exist_ok=True)
+
+ with (path / f"{profile}.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+
+ with (subpath / f"{profile}.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+
+ try:
+ with dbapi.connect(f"profile://{profile}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+ with dbapi.connect(f"profile://{subdir}/{profile}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+ finally:
+ (path / f"{profile}.toml").unlink()
+ (subpath / f"{profile}.toml").unlink()
+ subpath.rmdir()
+
+
+def test_conda(conda_prefix) -> None:
+ path = conda_prefix / "etc/adbc/profiles/"
+ path.mkdir(exist_ok=True, parents=True)
+ profile = str(uuid.uuid4())
+
+ with (path / f"{profile}.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+ try:
+ with dbapi.connect(f"profile://{profile}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+ finally:
+ (path / f"{profile}.toml").unlink()
+
+
+def test_conda_subdir(conda_prefix) -> None:
+ subdir = str(uuid.uuid4())
+ path = conda_prefix / f"etc/adbc/profiles/{subdir}"
+ path.mkdir(exist_ok=True, parents=True)
+ profile = str(uuid.uuid4())
+
+ with (path / f"{profile}.toml").open("w") as sink:
+ sink.write("""
+version = 1
+driver = "adbc_driver_sqlite"
+[options]
+""")
+ try:
+ with dbapi.connect(f"profile://{subdir}/{profile}") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+ finally:
+ (path / f"{profile}.toml").unlink()
+ path.rmdir()
+
+
+# For virtualenv tests: see Compose job python-venv