This is an automated email from the ASF dual-hosted git repository.
zeroshade 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 2519191fb feat: load driver from uri (#3694)
2519191fb is described below
commit 2519191fbba848fe776dcdac9ca70f09486e4d66
Author: Matt Topol <[email protected]>
AuthorDate: Wed Nov 19 13:15:19 2025 -0500
feat: load driver from uri (#3694)
Allow loading a driver via the driver manager using only a URI
---------
Co-authored-by: David Li <[email protected]>
---
c/driver_manager/adbc_driver_manager.cc | 66 ++++++++++-
c/driver_manager/adbc_driver_manager_test.cc | 80 +++++++++++++
go/adbc/drivermgr/adbc_driver_manager.cc | 66 ++++++++++-
python/adbc_driver_manager/tests/test_manifest.py | 21 ++++
rust/driver_manager/src/lib.rs | 124 ++++++++++++++++++++-
rust/driver_manager/tests/driver_manager_sqlite.rs | 15 ++-
6 files changed, 358 insertions(+), 14 deletions(-)
diff --git a/c/driver_manager/adbc_driver_manager.cc
b/c/driver_manager/adbc_driver_manager.cc
index 29a4b6a0f..b2fdb173f 100644
--- a/c/driver_manager/adbc_driver_manager.cc
+++ b/c/driver_manager/adbc_driver_manager.cc
@@ -62,6 +62,14 @@ ADBC_EXPORT
std::filesystem::path InternalAdbcSystemConfigDir();
#endif // !defined(_WIN32)
+struct ParseDriverUriResult {
+ std::string_view driver;
+ std::optional<std::string_view> uri;
+};
+
+ADBC_EXPORT
+std::optional<ParseDriverUriResult>
InternalAdbcParseDriverUri(std::string_view& str);
+
namespace {
/// \brief Where a search path came from (for error reporting)
@@ -1540,6 +1548,33 @@ std::string
InternalAdbcDriverManagerDefaultEntrypoint(const std::string& driver
return entrypoint;
}
+ADBC_EXPORT
+std::optional<ParseDriverUriResult>
InternalAdbcParseDriverUri(std::string_view& str) {
+ std::string::size_type pos = str.find(":");
+ if (pos == std::string::npos) {
+ return std::nullopt;
+ }
+
+ std::string_view d = str.substr(0, pos);
+ if (str.size() <= pos + 1) {
+ return ParseDriverUriResult{d, std::nullopt};
+ }
+
+#ifdef _WIN32
+ if (std::filesystem::exists(std::filesystem::path(str))) {
+ // No scheme, just a path
+ return ParseDriverUriResult{str, std::nullopt};
+ }
+#endif
+
+ if (str[pos + 1] == '/') { // scheme is also driver
+ return ParseDriverUriResult{d, str};
+ }
+
+ // driver:scheme:.....
+ return ParseDriverUriResult{d, str.substr(pos + 1)};
+}
+
// Direct implementations of API methods
int AdbcErrorGetDetailCount(const struct AdbcError* error) {
@@ -1691,16 +1726,35 @@ AdbcStatusCode AdbcDatabaseSetOption(struct
AdbcDatabase* database, const char*
TempDatabase* args = reinterpret_cast<TempDatabase*>(database->private_data);
if (std::strcmp(key, "driver") == 0) {
std::string_view v{value};
- std::string::size_type pos = v.find("://");
- if (pos != std::string::npos) {
- std::string_view d = v.substr(0, pos);
- args->driver = std::string{d};
- args->options["uri"] = std::string{v};
+ auto result = InternalAdbcParseDriverUri(v);
+ if (!result) {
+ args->driver = std::string{v};
} else {
- args->driver = value;
+ args->driver = std::string{result->driver};
+ if (result->uri) {
+ args->options["uri"] = std::string{*result->uri};
+ }
}
} else if (std::strcmp(key, "entrypoint") == 0) {
args->entrypoint = value;
+ } else if (std::strcmp(key, "uri") == 0) {
+ if (!args->driver.empty()) { // if driver is already set, just set uri
+ args->options[key] = value;
+ } else {
+ std::string_view v{value};
+ auto result = InternalAdbcParseDriverUri(v);
+ if (!result) {
+ SetError(error, "Invalid URI: missing scheme");
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ args->driver = std::string{result->driver};
+ if (!result->uri) {
+ SetError(error, "Invalid URI: " + std::string{value});
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+ args->options["uri"] = std::string{*result->uri};
+ }
} else {
args->options[key] = value;
}
diff --git a/c/driver_manager/adbc_driver_manager_test.cc
b/c/driver_manager/adbc_driver_manager_test.cc
index 85a53c04a..e52e717f1 100644
--- a/c/driver_manager/adbc_driver_manager_test.cc
+++ b/c/driver_manager/adbc_driver_manager_test.cc
@@ -40,6 +40,13 @@ std::string InternalAdbcDriverManagerDefaultEntrypoint(const
std::string& filena
std::vector<std::filesystem::path> InternalAdbcParsePath(const
std::string_view path);
std::filesystem::path InternalAdbcUserConfigDir();
+struct ParseDriverUriResult {
+ std::string_view driver;
+ std::optional<std::string_view> uri;
+};
+
+std::optional<ParseDriverUriResult>
InternalAdbcParseDriverUri(std::string_view& str);
+
// Tests of the SQLite example driver, except using the driver manager
namespace adbc {
@@ -413,6 +420,51 @@ TEST(AdbcDriverManagerInternal, InternalAdbcParsePath) {
EXPECT_THAT(output, ::testing::ElementsAreArray(paths));
}
+TEST(AdbcDriverManagerInternal, InternalAdbcParseDriverUri) {
+ std::vector<std::pair<std::string, std::optional<ParseDriverUriResult>>>
uris = {
+ {"sqlite", std::nullopt},
+ {"sqlite:", {{"sqlite", std::nullopt}}},
+ {"sqlite:file::memory:", {{"sqlite", "file::memory:"}}},
+ {"sqlite:file::memory:?cache=shared", {{"sqlite",
"file::memory:?cache=shared"}}},
+ {"postgresql://a:b@localhost:9999/nonexistent",
+ {{"postgresql", "postgresql://a:b@localhost:9999/nonexistent"}}}};
+
+#ifdef _WIN32
+ auto temp_dir = std::filesystem::temp_directory_path() /
"adbc_driver_manager_tests";
+ std::filesystem::create_directories(temp_dir);
+ std::string temp_driver_path = temp_dir.string() + "\\driver.dll";
+ std::ofstream temp_driver_file(temp_driver_path);
+ temp_driver_file << "placeholder";
+ temp_driver_file.close();
+
+ uris.push_back({temp_driver_path, {{temp_driver_path, std::nullopt}}});
+#endif
+
+ auto cmp = [](std::optional<ParseDriverUriResult> a,
+ std::optional<ParseDriverUriResult> b) {
+ if (!a.has_value()) {
+ EXPECT_FALSE(b.has_value());
+ return;
+ }
+ EXPECT_EQ(a->driver, b->driver);
+ if (!a->uri) {
+ EXPECT_FALSE(b->uri);
+ } else {
+ EXPECT_EQ(*a->uri, *b->uri);
+ }
+ };
+
+ for (const auto& [uri, expected] : uris) {
+ std::string_view uri_view = uri;
+ auto result = InternalAdbcParseDriverUri(uri_view);
+ cmp(result, expected);
+ }
+
+#ifdef _WIN32
+ std::filesystem::remove_all(temp_dir);
+#endif
+}
+
class DriverManifest : public ::testing::Test {
public:
void SetUp() override {
@@ -1110,4 +1162,32 @@ shared = "adbc_driver_postgresql")";
ASSERT_TRUE(std::filesystem::remove(filepath));
}
+TEST_F(DriverManifest, DriverFromUri) {
+ auto filepath = temp_dir / "sqlite.toml";
+ std::ofstream test_manifest_file(filepath);
+ ASSERT_TRUE(test_manifest_file.is_open());
+ test_manifest_file << R"([Driver]
+shared = "adbc_driver_sqlite")";
+ test_manifest_file.close();
+
+ const std::string uri = "sqlite:file::memory:";
+ for (const auto& driver_option : {"driver", "uri"}) {
+ SCOPED_TRACE(driver_option);
+ SCOPED_TRACE(uri);
+ adbc_validation::Handle<struct AdbcDatabase> database;
+ ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error));
+ ASSERT_THAT(
+ AdbcDatabaseSetOption(&database.value, driver_option, uri.c_str(),
&error),
+ IsOkStatus(&error));
+ std::string search_path = temp_dir.string();
+ ASSERT_THAT(AdbcDriverManagerDatabaseSetAdditionalSearchPathList(
+ &database.value, search_path.data(), &error),
+ IsOkStatus(&error));
+ ASSERT_THAT(AdbcDatabaseInit(&database.value, &error),
+ IsStatus(ADBC_STATUS_OK, &error));
+ }
+
+ ASSERT_TRUE(std::filesystem::remove(filepath));
+}
+
} // namespace adbc
diff --git a/go/adbc/drivermgr/adbc_driver_manager.cc
b/go/adbc/drivermgr/adbc_driver_manager.cc
index 29a4b6a0f..b2fdb173f 100644
--- a/go/adbc/drivermgr/adbc_driver_manager.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager.cc
@@ -62,6 +62,14 @@ ADBC_EXPORT
std::filesystem::path InternalAdbcSystemConfigDir();
#endif // !defined(_WIN32)
+struct ParseDriverUriResult {
+ std::string_view driver;
+ std::optional<std::string_view> uri;
+};
+
+ADBC_EXPORT
+std::optional<ParseDriverUriResult>
InternalAdbcParseDriverUri(std::string_view& str);
+
namespace {
/// \brief Where a search path came from (for error reporting)
@@ -1540,6 +1548,33 @@ std::string
InternalAdbcDriverManagerDefaultEntrypoint(const std::string& driver
return entrypoint;
}
+ADBC_EXPORT
+std::optional<ParseDriverUriResult>
InternalAdbcParseDriverUri(std::string_view& str) {
+ std::string::size_type pos = str.find(":");
+ if (pos == std::string::npos) {
+ return std::nullopt;
+ }
+
+ std::string_view d = str.substr(0, pos);
+ if (str.size() <= pos + 1) {
+ return ParseDriverUriResult{d, std::nullopt};
+ }
+
+#ifdef _WIN32
+ if (std::filesystem::exists(std::filesystem::path(str))) {
+ // No scheme, just a path
+ return ParseDriverUriResult{str, std::nullopt};
+ }
+#endif
+
+ if (str[pos + 1] == '/') { // scheme is also driver
+ return ParseDriverUriResult{d, str};
+ }
+
+ // driver:scheme:.....
+ return ParseDriverUriResult{d, str.substr(pos + 1)};
+}
+
// Direct implementations of API methods
int AdbcErrorGetDetailCount(const struct AdbcError* error) {
@@ -1691,16 +1726,35 @@ AdbcStatusCode AdbcDatabaseSetOption(struct
AdbcDatabase* database, const char*
TempDatabase* args = reinterpret_cast<TempDatabase*>(database->private_data);
if (std::strcmp(key, "driver") == 0) {
std::string_view v{value};
- std::string::size_type pos = v.find("://");
- if (pos != std::string::npos) {
- std::string_view d = v.substr(0, pos);
- args->driver = std::string{d};
- args->options["uri"] = std::string{v};
+ auto result = InternalAdbcParseDriverUri(v);
+ if (!result) {
+ args->driver = std::string{v};
} else {
- args->driver = value;
+ args->driver = std::string{result->driver};
+ if (result->uri) {
+ args->options["uri"] = std::string{*result->uri};
+ }
}
} else if (std::strcmp(key, "entrypoint") == 0) {
args->entrypoint = value;
+ } else if (std::strcmp(key, "uri") == 0) {
+ if (!args->driver.empty()) { // if driver is already set, just set uri
+ args->options[key] = value;
+ } else {
+ std::string_view v{value};
+ auto result = InternalAdbcParseDriverUri(v);
+ if (!result) {
+ SetError(error, "Invalid URI: missing scheme");
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+
+ args->driver = std::string{result->driver};
+ if (!result->uri) {
+ SetError(error, "Invalid URI: " + std::string{value});
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
+ args->options["uri"] = std::string{*result->uri};
+ }
} else {
args->options[key] = value;
}
diff --git a/python/adbc_driver_manager/tests/test_manifest.py
b/python/adbc_driver_manager/tests/test_manifest.py
index 0b4e62bfa..70e374efc 100644
--- a/python/adbc_driver_manager/tests/test_manifest.py
+++ b/python/adbc_driver_manager/tests/test_manifest.py
@@ -41,6 +41,27 @@ shared = "adbc_driver_sqlite"
assert cursor.fetchone() is not None
[email protected]
+def test_manifest_implicit_uri(tmp_path, monkeypatch) -> None:
+ with (tmp_path / "testdriver.toml").open("w") as sink:
+ sink.write(
+ """
+name = "my driver"
+version = "0.1.0"
+
+[Driver]
+shared = "adbc_driver_sqlite"
+ """
+ )
+
+ monkeypatch.setenv("ADBC_DRIVER_PATH", str(tmp_path))
+
+ with adbc_driver_manager.dbapi.connect("testdriver:file::memory:") as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT sqlite_version()")
+ assert cursor.fetchone() is not None
+
+
def test_manifest_indirect_unknown_driver(tmp_path, monkeypatch) -> None:
with (tmp_path / "testdriver2.toml").open("w") as sink:
sink.write(
diff --git a/rust/driver_manager/src/lib.rs b/rust/driver_manager/src/lib.rs
index 6fed38d04..173b60d06 100644
--- a/rust/driver_manager/src/lib.rs
+++ b/rust/driver_manager/src/lib.rs
@@ -121,7 +121,7 @@ use toml::de::DeTable;
use adbc_core::{
constants,
error::{AdbcStatusCode, Error, Result, Status},
- options::{self, AdbcVersion, InfoCode, OptionValue},
+ options::{self, AdbcVersion, InfoCode, OptionDatabase, OptionValue},
Connection, Database, Driver, LoadFlags, Optionable, PartitionedResult,
Statement,
LOAD_FLAG_ALLOW_RELATIVE_PATHS, LOAD_FLAG_SEARCH_ENV,
LOAD_FLAG_SEARCH_SYSTEM,
LOAD_FLAG_SEARCH_USER,
@@ -890,6 +890,47 @@ pub struct ManagedDatabase {
}
impl ManagedDatabase {
+ pub fn from_uri(
+ uri: &str,
+ entrypoint: Option<&[u8]>,
+ version: AdbcVersion,
+ load_flags: LoadFlags,
+ additional_search_paths: Option<Vec<PathBuf>>,
+ ) -> Result<Self> {
+ Self::from_uri_with_opts(
+ uri,
+ entrypoint,
+ version,
+ load_flags,
+ additional_search_paths,
+ std::iter::empty(),
+ )
+ }
+
+ pub fn from_uri_with_opts(
+ uri: &str,
+ entrypoint: Option<&[u8]>,
+ version: AdbcVersion,
+ load_flags: LoadFlags,
+ additional_search_paths: Option<Vec<PathBuf>>,
+ opts: impl IntoIterator<Item = (<Self as Optionable>::Option,
OptionValue)>,
+ ) -> Result<Self> {
+ let (driver, final_uri) = parse_driver_uri(uri)?;
+
+ let mut drv = ManagedDriver::load_from_name(
+ driver,
+ entrypoint,
+ version,
+ load_flags,
+ additional_search_paths,
+ )?;
+
+ drv.new_database_with_opts(opts.into_iter().chain(std::iter::once((
+ OptionDatabase::Uri,
+ OptionValue::String(final_uri.to_string()),
+ ))))
+ }
+
fn ffi_driver(&self) -> &adbc_ffi::FFI_AdbcDriver {
&self.inner.driver.driver
}
@@ -1876,6 +1917,30 @@ fn get_search_paths(lvls: LoadFlags) -> Vec<PathBuf> {
result
}
+fn parse_driver_uri(uri: &str) -> Result<(&str, &str)> {
+ let idx = uri.find(":").ok_or(Error::with_message_and_status(
+ format!("Invalid URI: {uri}"),
+ Status::InvalidArguments,
+ ))?;
+
+ let driver = &uri[..idx];
+ if uri.len() <= idx + 2 {
+ return Ok((driver, uri));
+ }
+
+ #[cfg(target_os = "windows")]
+ if let Ok(true) = std::fs::exists(uri) {
+ return Ok((uri, ""));
+ }
+
+ if &uri[idx..idx + 2] == ":/" {
+ // scheme is also driver
+ return Ok((driver, uri));
+ }
+
+ Ok((driver, &uri[idx + 1..]))
+}
+
const fn arch_triplet() -> (&'static str, &'static str, &'static str) {
#[cfg(target_arch = "x86_64")]
const ARCH: &str = "amd64";
@@ -2399,4 +2464,61 @@ mod tests {
assert_eq!(search_paths, Vec::<PathBuf>::new());
}
}
+
+ #[test]
+ fn test_parse_driver_uri() {
+ let cases = vec![
+ ("sqlite", Err(Status::InvalidArguments)),
+ ("sqlite:", Ok(("sqlite", "sqlite:"))),
+ ("sqlite:file::memory:", Ok(("sqlite", "file::memory:"))),
+ (
+ "sqlite:file::memory:?cache=shared",
+ Ok(("sqlite", "file::memory:?cache=shared")),
+ ),
+ (
+ "postgresql://a:b@localhost:9999/nonexistent",
+ Ok(("postgresql",
"postgresql://a:b@localhost:9999/nonexistent")),
+ ),
+ ];
+
+ for (input, expected) in cases {
+ let result = parse_driver_uri(input);
+ match expected {
+ Ok((exp_driver, exp_conn)) => {
+ let (driver, conn) = result.expect("Expected Ok result");
+ assert_eq!(driver, exp_driver);
+ assert_eq!(conn, exp_conn);
+ }
+ Err(exp_status) => {
+ let err = result.expect_err("Expected Err result");
+ assert_eq!(err.status, exp_status);
+ }
+ }
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ #[test]
+ fn test_parse_driver_uri_windows_file() {
+ let tmp_dir = Builder::new()
+ .prefix("adbc_tests")
+ .tempdir()
+ .expect("Failed to create temporary directory for driver manager
manifest test");
+
+ let temp_db_path = tmp_dir.path().join("test.dll");
+ if let Some(parent) = temp_db_path.parent() {
+ std::fs::create_dir_all(parent)
+ .expect("Failed to create parent directory for manifest");
+ }
+ std::fs::write(&temp_db_path, b"").expect("Failed to create temporary
database file");
+
+ let (driver, conn) =
parse_driver_uri(temp_db_path.to_str().unwrap()).unwrap();
+
+ assert_eq!(driver, temp_db_path.to_str().unwrap());
+ assert_eq!(conn, "");
+
+ tmp_dir
+ .close()
+ .expect("Failed to close/remove temporary directory");
+ }
}
diff --git a/rust/driver_manager/tests/driver_manager_sqlite.rs
b/rust/driver_manager/tests/driver_manager_sqlite.rs
index 8f9a654ab..b00f9a555 100644
--- a/rust/driver_manager/tests/driver_manager_sqlite.rs
+++ b/rust/driver_manager/tests/driver_manager_sqlite.rs
@@ -19,7 +19,7 @@ use arrow_schema::{Field, Schema};
use adbc_core::options::{AdbcVersion, OptionConnection, OptionDatabase};
use adbc_core::{error::Status, Driver, Optionable};
-use adbc_core::{Connection, Database, Statement};
+use adbc_core::{Connection, Database, Statement, LOAD_FLAG_DEFAULT};
use adbc_driver_manager::{ManagedDatabase, ManagedDriver};
mod common;
@@ -50,6 +50,19 @@ fn test_database() {
common::test_database(&database);
}
+#[test]
+fn test_database_implicit_uri() {
+ let database = ManagedDatabase::from_uri(
+ "adbc_driver_sqlite:file::memory:",
+ None,
+ AdbcVersion::V100,
+ LOAD_FLAG_DEFAULT,
+ None,
+ )
+ .unwrap();
+ common::test_database(&database);
+}
+
#[test]
fn test_database_get_option() {
let mut driver = get_driver();